mirror of
https://github.com/MikaylaFischler/cc-mek-scada.git
synced 2024-08-30 18:22:34 +00:00
Merge pull request #301 from MikaylaFischler/rtu-speaker-system
RTU Speaker System and Pocket Diagnostics
This commit is contained in:
commit
451f804f87
@ -83,6 +83,8 @@ function iocontrol.init(conf, comms)
|
|||||||
scram_ack = __generic_ack,
|
scram_ack = __generic_ack,
|
||||||
ack_alarms_ack = __generic_ack,
|
ack_alarms_ack = __generic_ack,
|
||||||
|
|
||||||
|
alarm_tones = { false, false, false, false, false, false, false, false },
|
||||||
|
|
||||||
ps = psil.create(),
|
ps = psil.create(),
|
||||||
|
|
||||||
induction_ps_tbl = {},
|
induction_ps_tbl = {},
|
||||||
@ -664,6 +666,16 @@ function iocontrol.update_facility_status(status)
|
|||||||
end
|
end
|
||||||
|
|
||||||
fac.ps.publish("rtu_count", fac.rtu_count)
|
fac.ps.publish("rtu_count", fac.rtu_count)
|
||||||
|
|
||||||
|
-- alarm tone commands
|
||||||
|
|
||||||
|
if (type(status[3]) == "table") and (#status[3] == 8) then
|
||||||
|
fac.alarm_tones = status[3]
|
||||||
|
sounder.set(fac.alarm_tones)
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "alarm tones not a table or length mismatch")
|
||||||
|
valid = false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
@ -1013,9 +1025,6 @@ function iocontrol.update_unit_statuses(statuses)
|
|||||||
io.facility.ps.publish("sna_count", sna_count_sum)
|
io.facility.ps.publish("sna_count", sna_count_sum)
|
||||||
io.facility.ps.publish("pu_rate", pu_rate)
|
io.facility.ps.publish("pu_rate", pu_rate)
|
||||||
io.facility.ps.publish("po_rate", po_rate)
|
io.facility.ps.publish("po_rate", po_rate)
|
||||||
|
|
||||||
-- update alarm sounder
|
|
||||||
sounder.eval(io.units)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
|
@ -2,269 +2,25 @@
|
|||||||
-- Alarm Sounder
|
-- Alarm Sounder
|
||||||
--
|
--
|
||||||
|
|
||||||
|
local audio = require("scada-common.audio")
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
local types = require("scada-common.types")
|
|
||||||
local util = require("scada-common.util")
|
|
||||||
|
|
||||||
local ALARM = types.ALARM
|
|
||||||
local ALARM_STATE = types.ALARM_STATE
|
|
||||||
|
|
||||||
---@class sounder
|
---@class sounder
|
||||||
local sounder = {}
|
local sounder = {}
|
||||||
|
|
||||||
-- note: max samples = 0x20000 (128 * 1024 samples)
|
|
||||||
|
|
||||||
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
|
||||||
local _DRATE = 48000 -- 48kHz audio
|
|
||||||
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
|
||||||
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
|
||||||
|
|
||||||
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
|
||||||
|
|
||||||
local alarm_ctl = {
|
local alarm_ctl = {
|
||||||
speaker = nil,
|
speaker = nil,
|
||||||
volume = 0.5,
|
volume = 0.5,
|
||||||
playing = false,
|
stream = audio.new_stream()
|
||||||
num_active = 0,
|
|
||||||
next_block = 1,
|
|
||||||
-- split audio up into 0.5s samples so specific components can be ended quicker
|
|
||||||
quad_buffer = { {}, {}, {}, {} }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
|
|
||||||
|
|
||||||
local T_340Hz_Int_2Hz = 1
|
|
||||||
local T_544Hz_440Hz_Alt = 2
|
|
||||||
local T_660Hz_Int_125ms = 3
|
|
||||||
local T_745Hz_Int_1Hz = 4
|
|
||||||
local T_800Hz_Int = 5
|
|
||||||
local T_800Hz_1000Hz_Alt = 6
|
|
||||||
local T_1000Hz_Int = 7
|
|
||||||
local T_1800Hz_Int_4Hz = 8
|
|
||||||
|
|
||||||
local TONES = {
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 340Hz @ 2Hz Intermittent
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 544Hz 100mS / 440Hz 400mS Alternating
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 660Hz @ 125ms On 125ms Off
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 745Hz @ 1Hz Intermittent
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800Hz @ 0.25s On 1.75s Off
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800/1000Hz @ 0.25s Alternating
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } }, -- 1KHz 1s on, 1s off Intermittent
|
|
||||||
{ active = false, component = { {}, {}, {}, {} } } -- 1.8KHz @ 4Hz Intermittent
|
|
||||||
}
|
|
||||||
|
|
||||||
-- calculate how many samples are in the given number of milliseconds
|
|
||||||
---@nodiscard
|
|
||||||
---@param ms integer milliseconds
|
|
||||||
---@return integer samples
|
|
||||||
local function ms_to_samples(ms) return math.floor(ms * 48) end
|
|
||||||
|
|
||||||
--#region Tone Generation (the Maths)
|
|
||||||
|
|
||||||
-- 340Hz @ 2Hz Intermittent
|
|
||||||
local function gen_tone_1()
|
|
||||||
local t, dt = 0, _2_PI * 340 / _DRATE
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[1].component[1][i] = val
|
|
||||||
TONES[1].component[3][i] = val
|
|
||||||
TONES[1].component[2][i] = 0
|
|
||||||
TONES[1].component[4][i] = 0
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 544Hz 100mS / 440Hz 400mS Alternating
|
|
||||||
local function gen_tone_2()
|
|
||||||
local t1, dt1 = 0, _2_PI * 544 / _DRATE
|
|
||||||
local t2, dt2 = 0, _2_PI * 440 / _DRATE
|
|
||||||
local alternate_at = ms_to_samples(100)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local value
|
|
||||||
|
|
||||||
if i <= alternate_at then
|
|
||||||
value = math.floor(math.sin(t1) * _MAX_VAL)
|
|
||||||
t1 = (t1 + dt1) % _2_PI
|
|
||||||
else
|
|
||||||
value = math.floor(math.sin(t2) * _MAX_VAL)
|
|
||||||
t2 = (t2 + dt2) % _2_PI
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[2].component[1][i] = value
|
|
||||||
TONES[2].component[2][i] = value
|
|
||||||
TONES[2].component[3][i] = value
|
|
||||||
TONES[2].component[4][i] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 660Hz @ 125ms On 125ms Off
|
|
||||||
local function gen_tone_3()
|
|
||||||
local elapsed_samples = 0
|
|
||||||
local alternate_after = ms_to_samples(125)
|
|
||||||
local alternate_at = alternate_after
|
|
||||||
local mode = true
|
|
||||||
|
|
||||||
local t, dt = 0, _2_PI * 660 / _DRATE
|
|
||||||
|
|
||||||
for set = 1, 4 do
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
if mode then
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[3].component[set][i] = val
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
else
|
|
||||||
t = 0
|
|
||||||
TONES[3].component[set][i] = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
if elapsed_samples == alternate_at then
|
|
||||||
mode = not mode
|
|
||||||
alternate_at = elapsed_samples + alternate_after
|
|
||||||
end
|
|
||||||
|
|
||||||
elapsed_samples = elapsed_samples + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 745Hz @ 1Hz Intermittent
|
|
||||||
local function gen_tone_4()
|
|
||||||
local t, dt = 0, _2_PI * 745 / _DRATE
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[4].component[1][i] = val
|
|
||||||
TONES[4].component[3][i] = val
|
|
||||||
TONES[4].component[2][i] = 0
|
|
||||||
TONES[4].component[4][i] = 0
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 800Hz @ 0.25s On 1.75s Off
|
|
||||||
local function gen_tone_5()
|
|
||||||
local t, dt = 0, _2_PI * 800 / _DRATE
|
|
||||||
local stop_at = ms_to_samples(250)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
|
|
||||||
if i > stop_at then
|
|
||||||
TONES[5].component[1][i] = val
|
|
||||||
else
|
|
||||||
TONES[5].component[1][i] = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[5].component[2][i] = 0
|
|
||||||
TONES[5].component[3][i] = 0
|
|
||||||
TONES[5].component[4][i] = 0
|
|
||||||
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 1000/800Hz @ 0.25s Alternating
|
|
||||||
local function gen_tone_6()
|
|
||||||
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
|
|
||||||
local t2, dt2 = 0, _2_PI * 800 / _DRATE
|
|
||||||
|
|
||||||
local alternate_at = ms_to_samples(250)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val
|
|
||||||
if i <= alternate_at then
|
|
||||||
val = math.floor(math.sin(t1) * _MAX_VAL)
|
|
||||||
t1 = (t1 + dt1) % _2_PI
|
|
||||||
else
|
|
||||||
val = math.floor(math.sin(t2) * _MAX_VAL)
|
|
||||||
t2 = (t2 + dt2) % _2_PI
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[6].component[1][i] = val
|
|
||||||
TONES[6].component[2][i] = val
|
|
||||||
TONES[6].component[3][i] = val
|
|
||||||
TONES[6].component[4][i] = val
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 1KHz 1s on, 1s off Intermittent
|
|
||||||
local function gen_tone_7()
|
|
||||||
local t, dt = 0, _2_PI * 1000 / _DRATE
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
TONES[7].component[1][i] = val
|
|
||||||
TONES[7].component[2][i] = val
|
|
||||||
TONES[7].component[3][i] = 0
|
|
||||||
TONES[7].component[4][i] = 0
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 1800Hz @ 4Hz Intermittent
|
|
||||||
local function gen_tone_8()
|
|
||||||
local t, dt = 0, _2_PI * 1800 / _DRATE
|
|
||||||
|
|
||||||
local off_at = ms_to_samples(250)
|
|
||||||
|
|
||||||
for i = 1, _05s_SAMPLES do
|
|
||||||
local val = 0
|
|
||||||
|
|
||||||
if i <= off_at then
|
|
||||||
val = math.floor(math.sin(t) * _MAX_VAL)
|
|
||||||
t = (t + dt) % _2_PI
|
|
||||||
end
|
|
||||||
|
|
||||||
TONES[8].component[1][i] = val
|
|
||||||
TONES[8].component[2][i] = val
|
|
||||||
TONES[8].component[3][i] = val
|
|
||||||
TONES[8].component[4][i] = val
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--#endregion
|
|
||||||
|
|
||||||
-- hard audio limiter
|
|
||||||
---@nodiscard
|
|
||||||
---@param output number output level
|
|
||||||
---@return number limited -128.0 to 127.0
|
|
||||||
local function limit(output)
|
|
||||||
return math.max(-128, math.min(127, output))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- zero the alarm audio buffer
|
|
||||||
local function zero()
|
|
||||||
for i = 1, 4 do
|
|
||||||
for s = 1, _05s_SAMPLES do alarm_ctl.quad_buffer[i][s] = 0 end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- add an alarm to the output buffer
|
|
||||||
---@param alarm_idx integer tone ID
|
|
||||||
local function add(alarm_idx)
|
|
||||||
alarm_ctl.num_active = alarm_ctl.num_active + 1
|
|
||||||
TONES[alarm_idx].active = true
|
|
||||||
|
|
||||||
for i = 1, 4 do
|
|
||||||
for s = 1, _05s_SAMPLES do
|
|
||||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] + TONES[alarm_idx].component[i][s])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- start audio or continue audio on buffer empty
|
-- start audio or continue audio on buffer empty
|
||||||
---@return boolean success successfully added buffer to audio output
|
---@return boolean success successfully added buffer to audio output
|
||||||
local function play()
|
local function play()
|
||||||
if not alarm_ctl.playing then
|
if not alarm_ctl.playing then
|
||||||
alarm_ctl.playing = true
|
alarm_ctl.playing = true
|
||||||
alarm_ctl.next_block = 1
|
|
||||||
|
|
||||||
return sounder.continue()
|
return sounder.continue()
|
||||||
else
|
else return true end
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- initialize the annunciator alarm system
|
-- initialize the annunciator alarm system
|
||||||
@ -273,23 +29,10 @@ end
|
|||||||
function sounder.init(speaker, volume)
|
function sounder.init(speaker, volume)
|
||||||
alarm_ctl.speaker = speaker
|
alarm_ctl.speaker = speaker
|
||||||
alarm_ctl.speaker.stop()
|
alarm_ctl.speaker.stop()
|
||||||
|
|
||||||
alarm_ctl.volume = volume
|
alarm_ctl.volume = volume
|
||||||
alarm_ctl.playing = false
|
alarm_ctl.stream.stop()
|
||||||
alarm_ctl.num_active = 0
|
|
||||||
alarm_ctl.next_block = 1
|
|
||||||
|
|
||||||
zero()
|
audio.generate_tones()
|
||||||
|
|
||||||
-- generate tones
|
|
||||||
gen_tone_1()
|
|
||||||
gen_tone_2()
|
|
||||||
gen_tone_3()
|
|
||||||
gen_tone_4()
|
|
||||||
gen_tone_5()
|
|
||||||
gen_tone_6()
|
|
||||||
gen_tone_7()
|
|
||||||
gen_tone_8()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- reconnect the speaker peripheral
|
-- reconnect the speaker peripheral
|
||||||
@ -297,173 +40,40 @@ end
|
|||||||
function sounder.reconnect(speaker)
|
function sounder.reconnect(speaker)
|
||||||
alarm_ctl.speaker = speaker
|
alarm_ctl.speaker = speaker
|
||||||
alarm_ctl.playing = false
|
alarm_ctl.playing = false
|
||||||
alarm_ctl.next_block = 1
|
alarm_ctl.stream.stop()
|
||||||
alarm_ctl.num_active = 0
|
|
||||||
for id = 1, #TONES do TONES[id].active = false end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check alarm state to enable/disable alarms
|
-- set alarm tones
|
||||||
---@param units table|nil unit list or nil to use test mode
|
---@param states table alarm tone commands from supervisor
|
||||||
function sounder.eval(units)
|
function sounder.set(states)
|
||||||
local changed = false
|
-- set tone states
|
||||||
local any_active = false
|
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
|
||||||
local new_states = { false, false, false, false, false, false, false, false }
|
|
||||||
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
|
||||||
|
|
||||||
if units ~= nil then
|
-- re-compute output if needed, then play audio if available
|
||||||
-- check all alarms for all units
|
if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
|
||||||
for i = 1, #units do
|
if alarm_ctl.stream.has_next_block() then play() else sounder.stop() end
|
||||||
local unit = units[i] ---@type ioctl_unit
|
|
||||||
for id = 1, #unit.alarms do
|
|
||||||
alarms[id] = alarms[id] or (unit.alarms[id] == ALARM_STATE.TRIPPED)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
alarms = test_alarms
|
|
||||||
end
|
|
||||||
|
|
||||||
-- containment breach is worst case CRITICAL alarm, this takes priority
|
|
||||||
if alarms[ALARM.ContainmentBreach] then
|
|
||||||
new_states[T_1800Hz_Int_4Hz] = true
|
|
||||||
else
|
|
||||||
-- critical damage is highest priority CRITICAL level alarm
|
|
||||||
if alarms[ALARM.CriticalDamage] then
|
|
||||||
new_states[T_660Hz_Int_125ms] = true
|
|
||||||
else
|
|
||||||
-- EMERGENCY level alarms + URGENT over temp
|
|
||||||
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
|
|
||||||
new_states[T_544Hz_440Hz_Alt] = true
|
|
||||||
-- URGENT level turbine trip
|
|
||||||
elseif alarms[ALARM.TurbineTrip] then
|
|
||||||
new_states[T_745Hz_Int_1Hz] = true
|
|
||||||
-- URGENT level reactor lost
|
|
||||||
elseif alarms[ALARM.ReactorLost] then
|
|
||||||
new_states[T_340Hz_Int_2Hz] = true
|
|
||||||
-- TIMELY level alarms
|
|
||||||
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
|
||||||
new_states[T_800Hz_Int] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check RPS transient URGENT level alarm
|
|
||||||
if alarms[ALARM.RPSTransient] then
|
|
||||||
new_states[T_1000Hz_Int] = true
|
|
||||||
-- disable really painful audio combination
|
|
||||||
new_states[T_340Hz_Int_2Hz] = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- radiation is a big concern, always play this CRITICAL level alarm if active
|
|
||||||
if alarms[ALARM.ContainmentRadiation] then
|
|
||||||
new_states[T_800Hz_1000Hz_Alt] = true
|
|
||||||
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
|
|
||||||
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
|
|
||||||
if new_states[T_1000Hz_Int] and alarms[ALARM.ReactorLost] then new_states[T_340Hz_Int_2Hz] = true end
|
|
||||||
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
|
||||||
new_states[T_745Hz_Int_1Hz] = false
|
|
||||||
new_states[T_800Hz_Int] = false
|
|
||||||
new_states[T_1000Hz_Int] = false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- check if any changed, check if any active, update active flags
|
|
||||||
for id = 1, #TONES do
|
|
||||||
if new_states[id] ~= TONES[id].active then
|
|
||||||
TONES[id].active = new_states[id]
|
|
||||||
changed = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if TONES[id].active then any_active = true end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- zero and re-add tones if changed
|
|
||||||
if changed then
|
|
||||||
zero()
|
|
||||||
|
|
||||||
for id = 1, #TONES do
|
|
||||||
if TONES[id].active then add(id) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if any_active then play() else sounder.stop() end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- stop all audio and clear output buffer
|
-- stop all audio and clear output buffer
|
||||||
function sounder.stop()
|
function sounder.stop()
|
||||||
alarm_ctl.playing = false
|
alarm_ctl.playing = false
|
||||||
alarm_ctl.speaker.stop()
|
alarm_ctl.speaker.stop()
|
||||||
alarm_ctl.next_block = 1
|
alarm_ctl.stream.stop()
|
||||||
alarm_ctl.num_active = 0
|
|
||||||
for id = 1, #TONES do TONES[id].active = false end
|
|
||||||
zero()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- continue audio on buffer empty
|
-- continue audio on buffer empty
|
||||||
---@return boolean success successfully added buffer to audio output
|
---@return boolean success successfully added buffer to audio output
|
||||||
function sounder.continue()
|
function sounder.continue()
|
||||||
|
local success = false
|
||||||
|
|
||||||
if alarm_ctl.playing then
|
if alarm_ctl.playing then
|
||||||
if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then
|
if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
|
||||||
local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume)
|
success = alarm_ctl.speaker.playAudio(alarm_ctl.stream.get_next_block(), alarm_ctl.volume)
|
||||||
|
if not success then log.error("SOUNDER: error playing audio") end
|
||||||
alarm_ctl.next_block = alarm_ctl.next_block + 1
|
end
|
||||||
if alarm_ctl.next_block > 4 then alarm_ctl.next_block = 1 end
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
log.debug("SOUNDER: error playing audio")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return success
|
return success
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--#region Test Functions
|
|
||||||
|
|
||||||
function sounder.test_1() add(1) play() end -- play tone T_340Hz_Int_2Hz
|
|
||||||
function sounder.test_2() add(2) play() end -- play tone T_544Hz_440Hz_Alt
|
|
||||||
function sounder.test_3() add(3) play() end -- play tone T_660Hz_Int_125ms
|
|
||||||
function sounder.test_4() add(4) play() end -- play tone T_745Hz_Int_1Hz
|
|
||||||
function sounder.test_5() add(5) play() end -- play tone T_800Hz_Int
|
|
||||||
function sounder.test_6() add(6) play() end -- play tone T_800Hz_1000Hz_Alt
|
|
||||||
function sounder.test_7() add(7) play() end -- play tone T_1000Hz_Int
|
|
||||||
function sounder.test_8() add(8) play() end -- play tone T_1800Hz_Int_4Hz
|
|
||||||
|
|
||||||
function sounder.test_breach(active) test_alarms[ALARM.ContainmentBreach] = active end ---@param active boolean
|
|
||||||
function sounder.test_rad(active) test_alarms[ALARM.ContainmentRadiation] = active end ---@param active boolean
|
|
||||||
function sounder.test_lost(active) test_alarms[ALARM.ReactorLost] = active end ---@param active boolean
|
|
||||||
function sounder.test_crit(active) test_alarms[ALARM.CriticalDamage] = active end ---@param active boolean
|
|
||||||
function sounder.test_dmg(active) test_alarms[ALARM.ReactorDamage] = active end ---@param active boolean
|
|
||||||
function sounder.test_overtemp(active) test_alarms[ALARM.ReactorOverTemp] = active end ---@param active boolean
|
|
||||||
function sounder.test_hightemp(active) test_alarms[ALARM.ReactorHighTemp] = active end ---@param active boolean
|
|
||||||
function sounder.test_wasteleak(active) test_alarms[ALARM.ReactorWasteLeak] = active end ---@param active boolean
|
|
||||||
function sounder.test_highwaste(active) test_alarms[ALARM.ReactorHighWaste] = active end ---@param active boolean
|
|
||||||
function sounder.test_rps(active) test_alarms[ALARM.RPSTransient] = active end ---@param active boolean
|
|
||||||
function sounder.test_rcs(active) test_alarms[ALARM.RCSTransient] = active end ---@param active boolean
|
|
||||||
function sounder.test_turbinet(active) test_alarms[ALARM.TurbineTrip] = active end ---@param active boolean
|
|
||||||
|
|
||||||
-- power rescaling limiter test
|
|
||||||
function sounder.test_power_scale()
|
|
||||||
local start = util.time_ms()
|
|
||||||
|
|
||||||
zero()
|
|
||||||
|
|
||||||
for id = 1, #TONES do
|
|
||||||
if TONES[id].active then
|
|
||||||
for i = 1, 4 do
|
|
||||||
for s = 1, _05s_SAMPLES do
|
|
||||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] +
|
|
||||||
(TONES[id].component[i][s] / math.sqrt(alarm_ctl.num_active)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
|
|
||||||
end
|
|
||||||
|
|
||||||
--#endregion
|
|
||||||
|
|
||||||
return sounder
|
return sounder
|
||||||
|
@ -22,7 +22,7 @@ local sounder = require("coordinator.sounder")
|
|||||||
|
|
||||||
local apisessions = require("coordinator.session.apisessions")
|
local apisessions = require("coordinator.session.apisessions")
|
||||||
|
|
||||||
local COORDINATOR_VERSION = "v0.21.2"
|
local COORDINATOR_VERSION = "v0.22.0"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
|
|||||||
|
|
||||||
local core = {}
|
local core = {}
|
||||||
|
|
||||||
core.version = "1.0.2"
|
core.version = "1.1.0"
|
||||||
|
|
||||||
core.flasher = flasher
|
core.flasher = flasher
|
||||||
core.events = events
|
core.events = events
|
||||||
|
@ -20,6 +20,7 @@ local element = {}
|
|||||||
|
|
||||||
---@alias graphics_args graphics_args_generic
|
---@alias graphics_args graphics_args_generic
|
||||||
---|waiting_args
|
---|waiting_args
|
||||||
|
---|app_button_args
|
||||||
---|checkbox_args
|
---|checkbox_args
|
||||||
---|hazard_button_args
|
---|hazard_button_args
|
||||||
---|multi_button_args
|
---|multi_button_args
|
||||||
@ -515,9 +516,10 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
|
|
||||||
-- FUNCTION CALLBACKS --
|
-- FUNCTION CALLBACKS --
|
||||||
|
|
||||||
-- handle a monitor touch or mouse click
|
-- handle a monitor touch or mouse click if this element is visible
|
||||||
---@param event mouse_interaction mouse interaction event
|
---@param event mouse_interaction mouse interaction event
|
||||||
function public.handle_mouse(event)
|
function public.handle_mouse(event)
|
||||||
|
if protected.window.isVisible() then
|
||||||
local x_ini, y_ini = event.initial.x, event.initial.y
|
local x_ini, y_ini = event.initial.x, event.initial.y
|
||||||
|
|
||||||
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
local ini_in = protected.in_window_bounds(x_ini, y_ini)
|
||||||
@ -530,6 +532,7 @@ function element.new(args, child_offset_x, child_offset_y)
|
|||||||
for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
|
for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- draw the element given new data
|
-- draw the element given new data
|
||||||
---@vararg any new data
|
---@vararg any new data
|
||||||
|
130
graphics/elements/controls/app.lua
Normal file
130
graphics/elements/controls/app.lua
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
-- App Button 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 app_button_args
|
||||||
|
---@field text string app icon text
|
||||||
|
---@field title string app title text
|
||||||
|
---@field callback function function to call on touch
|
||||||
|
---@field app_fg_bg cpair app icon foreground/background colors
|
||||||
|
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||||
|
---@field parent graphics_element
|
||||||
|
---@field id? string element id
|
||||||
|
---@field x? integer 1 if omitted
|
||||||
|
---@field y? integer auto incremented if omitted
|
||||||
|
---@field fg_bg? cpair foreground/background colors
|
||||||
|
---@field hidden? boolean true to hide on initial draw
|
||||||
|
|
||||||
|
-- new app button
|
||||||
|
---@param args app_button_args
|
||||||
|
---@return graphics_element element, element_id id
|
||||||
|
local function app_button(args)
|
||||||
|
assert(type(args.text) == "string", "graphics.elements.controls.app: text is a required field")
|
||||||
|
assert(type(args.title) == "string", "graphics.elements.controls.app: title is a required field")
|
||||||
|
assert(type(args.callback) == "function", "graphics.elements.controls.app: callback is a required field")
|
||||||
|
assert(type(args.app_fg_bg) == "table", "graphics.elements.controls.app: app_fg_bg is a required field")
|
||||||
|
|
||||||
|
args.height = 4
|
||||||
|
args.width = 5
|
||||||
|
|
||||||
|
-- create new graphics element base object
|
||||||
|
local e = element.new(args)
|
||||||
|
|
||||||
|
-- write app title, centered
|
||||||
|
e.window.setCursorPos(1, 4)
|
||||||
|
e.window.setCursorPos(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
|
||||||
|
e.window.write(args.title)
|
||||||
|
|
||||||
|
-- draw the app button
|
||||||
|
local function draw()
|
||||||
|
local fgd = args.app_fg_bg.fgd
|
||||||
|
local bkg = args.app_fg_bg.bkg
|
||||||
|
|
||||||
|
if e.value then
|
||||||
|
fgd = args.active_fg_bg.fgd
|
||||||
|
bkg = args.active_fg_bg.bkg
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw icon
|
||||||
|
e.window.setCursorPos(1, 1)
|
||||||
|
e.window.setTextColor(fgd)
|
||||||
|
e.window.setBackgroundColor(bkg)
|
||||||
|
e.window.write("\x9f\x83\x83\x83")
|
||||||
|
e.window.setTextColor(bkg)
|
||||||
|
e.window.setBackgroundColor(fgd)
|
||||||
|
e.window.write("\x90")
|
||||||
|
e.window.setTextColor(fgd)
|
||||||
|
e.window.setBackgroundColor(bkg)
|
||||||
|
e.window.setCursorPos(1, 2)
|
||||||
|
e.window.write("\x95 ")
|
||||||
|
e.window.setTextColor(bkg)
|
||||||
|
e.window.setBackgroundColor(fgd)
|
||||||
|
e.window.write("\x95")
|
||||||
|
e.window.setCursorPos(1, 3)
|
||||||
|
e.window.write("\x82\x8f\x8f\x8f\x81")
|
||||||
|
|
||||||
|
-- write the icon text
|
||||||
|
e.window.setCursorPos(3, 2)
|
||||||
|
e.window.setTextColor(fgd)
|
||||||
|
e.window.setBackgroundColor(bkg)
|
||||||
|
e.window.write(args.text)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw the app button as pressed (if active_fg_bg set)
|
||||||
|
local function show_pressed()
|
||||||
|
if e.enabled and args.active_fg_bg ~= nil then
|
||||||
|
e.value = true
|
||||||
|
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||||
|
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||||
|
draw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- draw the app button as unpressed (if active_fg_bg set)
|
||||||
|
local function show_unpressed()
|
||||||
|
if e.enabled and args.active_fg_bg ~= nil then
|
||||||
|
e.value = false
|
||||||
|
e.window.setTextColor(e.fg_bg.fgd)
|
||||||
|
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||||
|
draw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- handle mouse interaction
|
||||||
|
---@param event mouse_interaction mouse event
|
||||||
|
function e.handle_mouse(event)
|
||||||
|
if e.enabled then
|
||||||
|
if event.type == CLICK_TYPE.TAP then
|
||||||
|
show_pressed()
|
||||||
|
-- show as unpressed in 0.25 seconds
|
||||||
|
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||||
|
args.callback()
|
||||||
|
elseif event.type == CLICK_TYPE.DOWN then
|
||||||
|
show_pressed()
|
||||||
|
elseif event.type == CLICK_TYPE.UP then
|
||||||
|
show_unpressed()
|
||||||
|
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||||
|
args.callback()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the value (true simulates pressing the app button)
|
||||||
|
---@param val boolean new value
|
||||||
|
function e.set_value(val)
|
||||||
|
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- initial draw
|
||||||
|
draw()
|
||||||
|
|
||||||
|
return e.complete()
|
||||||
|
end
|
||||||
|
|
||||||
|
return app_button
|
@ -1,35 +0,0 @@
|
|||||||
--
|
|
||||||
-- Core I/O - Pocket Central I/O Management
|
|
||||||
--
|
|
||||||
|
|
||||||
local psil = require("scada-common.psil")
|
|
||||||
|
|
||||||
local coreio = {}
|
|
||||||
|
|
||||||
---@class pocket_core_io
|
|
||||||
local io = {
|
|
||||||
ps = psil.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
---@enum POCKET_LINK_STATE
|
|
||||||
local LINK_STATE = {
|
|
||||||
UNLINKED = 0,
|
|
||||||
SV_LINK_ONLY = 1,
|
|
||||||
API_LINK_ONLY = 2,
|
|
||||||
LINKED = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
coreio.LINK_STATE = LINK_STATE
|
|
||||||
|
|
||||||
-- get the core PSIL
|
|
||||||
function coreio.core_ps()
|
|
||||||
return io.ps
|
|
||||||
end
|
|
||||||
|
|
||||||
-- set network link state
|
|
||||||
---@param state POCKET_LINK_STATE
|
|
||||||
function coreio.report_link_state(state)
|
|
||||||
io.ps.publish("link_state", state)
|
|
||||||
end
|
|
||||||
|
|
||||||
return coreio
|
|
106
pocket/iocontrol.lua
Normal file
106
pocket/iocontrol.lua
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
--
|
||||||
|
-- I/O Control for Pocket Integration with Supervisor & Coordinator
|
||||||
|
--
|
||||||
|
|
||||||
|
local psil = require("scada-common.psil")
|
||||||
|
|
||||||
|
local types = require("scada-common.types")
|
||||||
|
|
||||||
|
local ALARM = types.ALARM
|
||||||
|
|
||||||
|
local iocontrol = {}
|
||||||
|
|
||||||
|
---@class pocket_ioctl
|
||||||
|
local io = {
|
||||||
|
ps = psil.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum POCKET_LINK_STATE
|
||||||
|
local LINK_STATE = {
|
||||||
|
UNLINKED = 0,
|
||||||
|
SV_LINK_ONLY = 1,
|
||||||
|
API_LINK_ONLY = 2,
|
||||||
|
LINKED = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
---@enum NAV_PAGE
|
||||||
|
local NAV_PAGE = {
|
||||||
|
HOME = 1,
|
||||||
|
UNITS = 2,
|
||||||
|
REACTORS = 3,
|
||||||
|
BOILERS = 4,
|
||||||
|
TURBINES = 5,
|
||||||
|
DIAG = 6,
|
||||||
|
D_ALARMS = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
iocontrol.LINK_STATE = LINK_STATE
|
||||||
|
iocontrol.NAV_PAGE = NAV_PAGE
|
||||||
|
|
||||||
|
-- initialize facility-independent components of pocket iocontrol
|
||||||
|
---@param comms pocket_comms
|
||||||
|
function iocontrol.init_core(comms)
|
||||||
|
---@class pocket_ioctl_diag
|
||||||
|
io.diag = {}
|
||||||
|
|
||||||
|
-- alarm testing
|
||||||
|
io.diag.tone_test = {
|
||||||
|
test_1 = function (state) comms.diag__set_alarm_tone(1, state) end,
|
||||||
|
test_2 = function (state) comms.diag__set_alarm_tone(2, state) end,
|
||||||
|
test_3 = function (state) comms.diag__set_alarm_tone(3, state) end,
|
||||||
|
test_4 = function (state) comms.diag__set_alarm_tone(4, state) end,
|
||||||
|
test_5 = function (state) comms.diag__set_alarm_tone(5, state) end,
|
||||||
|
test_6 = function (state) comms.diag__set_alarm_tone(6, state) end,
|
||||||
|
test_7 = function (state) comms.diag__set_alarm_tone(7, state) end,
|
||||||
|
test_8 = function (state) comms.diag__set_alarm_tone(8, state) end,
|
||||||
|
stop_tones = function () comms.diag__set_alarm_tone(0, false) end,
|
||||||
|
|
||||||
|
test_breach = function (state) comms.diag__set_alarm(ALARM.ContainmentBreach, state) end,
|
||||||
|
test_rad = function (state) comms.diag__set_alarm(ALARM.ContainmentRadiation, state) end,
|
||||||
|
test_lost = function (state) comms.diag__set_alarm(ALARM.ReactorLost, state) end,
|
||||||
|
test_crit = function (state) comms.diag__set_alarm(ALARM.CriticalDamage, state) end,
|
||||||
|
test_dmg = function (state) comms.diag__set_alarm(ALARM.ReactorDamage, state) end,
|
||||||
|
test_overtemp = function (state) comms.diag__set_alarm(ALARM.ReactorOverTemp, state) end,
|
||||||
|
test_hightemp = function (state) comms.diag__set_alarm(ALARM.ReactorHighTemp, state) end,
|
||||||
|
test_wasteleak = function (state) comms.diag__set_alarm(ALARM.ReactorWasteLeak, state) end,
|
||||||
|
test_highwaste = function (state) comms.diag__set_alarm(ALARM.ReactorHighWaste, state) end,
|
||||||
|
test_rps = function (state) comms.diag__set_alarm(ALARM.RPSTransient, state) end,
|
||||||
|
test_rcs = function (state) comms.diag__set_alarm(ALARM.RCSTransient, state) end,
|
||||||
|
test_turbinet = function (state) comms.diag__set_alarm(ALARM.TurbineTrip, state) end,
|
||||||
|
stop_alarms = function () comms.diag__set_alarm(0, false) end,
|
||||||
|
|
||||||
|
get_tone_states = function () comms.diag__get_alarm_tones() end,
|
||||||
|
|
||||||
|
ready_warn = nil, ---@type graphics_element
|
||||||
|
tone_buttons = {},
|
||||||
|
alarm_buttons = {},
|
||||||
|
tone_indicators = {} -- indicators to update from supervisor tone states
|
||||||
|
}
|
||||||
|
|
||||||
|
---@class pocket_nav
|
||||||
|
io.nav = {
|
||||||
|
page = NAV_PAGE.HOME, ---@type NAV_PAGE
|
||||||
|
sub_pages = { NAV_PAGE.HOME, NAV_PAGE.UNITS, NAV_PAGE.REACTORS, NAV_PAGE.BOILERS, NAV_PAGE.TURBINES, NAV_PAGE.DIAG },
|
||||||
|
tasks = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- add a task to be performed periodically while on a given page
|
||||||
|
---@param page NAV_PAGE page to add task to
|
||||||
|
---@param task function function to execute
|
||||||
|
function io.nav.register_task(page, task)
|
||||||
|
if io.nav.tasks[page] == nil then io.nav.tasks[page] = {} end
|
||||||
|
table.insert(io.nav.tasks[page], task)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- initialize facility-dependent components of pocket iocontrol
|
||||||
|
function iocontrol.init_fac() end
|
||||||
|
|
||||||
|
-- set network link state
|
||||||
|
---@param state POCKET_LINK_STATE
|
||||||
|
function iocontrol.report_link_state(state) io.ps.publish("link_state", state) end
|
||||||
|
|
||||||
|
-- get the IO controller database
|
||||||
|
function iocontrol.get_db() return io end
|
||||||
|
|
||||||
|
return iocontrol
|
@ -2,15 +2,14 @@ local comms = require("scada-common.comms")
|
|||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
local util = require("scada-common.util")
|
local util = require("scada-common.util")
|
||||||
|
|
||||||
local coreio = require("pocket.coreio")
|
local iocontrol = require("pocket.iocontrol")
|
||||||
|
|
||||||
local PROTOCOL = comms.PROTOCOL
|
local PROTOCOL = comms.PROTOCOL
|
||||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||||
-- local CAPI_TYPE = comms.CAPI_TYPE
|
|
||||||
|
|
||||||
local LINK_STATE = coreio.LINK_STATE
|
local LINK_STATE = iocontrol.LINK_STATE
|
||||||
|
|
||||||
local pocket = {}
|
local pocket = {}
|
||||||
|
|
||||||
@ -79,20 +78,6 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
self.api.seq_num = self.api.seq_num + 1
|
self.api.seq_num = self.api.seq_num + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
-- send a packet to the coordinator API
|
|
||||||
-----@param msg_type CAPI_TYPE
|
|
||||||
-----@param msg table
|
|
||||||
-- local function _send_api(msg_type, msg)
|
|
||||||
-- local s_pkt = comms.scada_packet()
|
|
||||||
-- local pkt = comms.capi_packet()
|
|
||||||
|
|
||||||
-- pkt.make(msg_type, msg)
|
|
||||||
-- s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable())
|
|
||||||
|
|
||||||
-- nic.transmit(crd_channel, pkt_channel, s_pkt)
|
|
||||||
-- self.api.seq_num = self.api.seq_num + 1
|
|
||||||
-- end
|
|
||||||
|
|
||||||
-- attempt supervisor connection establishment
|
-- attempt supervisor connection establishment
|
||||||
local function _send_sv_establish()
|
local function _send_sv_establish()
|
||||||
_send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
_send_sv(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
|
||||||
@ -147,7 +132,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
-- attempt to re-link if any of the dependent links aren't active
|
-- attempt to re-link if any of the dependent links aren't active
|
||||||
function public.link_update()
|
function public.link_update()
|
||||||
if not self.sv.linked then
|
if not self.sv.linked then
|
||||||
coreio.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
|
iocontrol.report_link_state(util.trinary(self.api.linked, LINK_STATE.API_LINK_ONLY, LINK_STATE.UNLINKED))
|
||||||
|
|
||||||
if self.establish_delay_counter <= 0 then
|
if self.establish_delay_counter <= 0 then
|
||||||
_send_sv_establish()
|
_send_sv_establish()
|
||||||
@ -156,7 +141,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
self.establish_delay_counter = self.establish_delay_counter - 1
|
self.establish_delay_counter = self.establish_delay_counter - 1
|
||||||
end
|
end
|
||||||
elseif not self.api.linked then
|
elseif not self.api.linked then
|
||||||
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||||
|
|
||||||
if self.establish_delay_counter <= 0 then
|
if self.establish_delay_counter <= 0 then
|
||||||
_send_api_establish()
|
_send_api_establish()
|
||||||
@ -166,10 +151,29 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
-- linked, all good!
|
-- linked, all good!
|
||||||
coreio.report_link_state(LINK_STATE.LINKED)
|
iocontrol.report_link_state(LINK_STATE.LINKED)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- supervisor get active alarm tones
|
||||||
|
function public.diag__get_alarm_tones()
|
||||||
|
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_GET, {}) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- supervisor test alarm tones by tone
|
||||||
|
---@param id TONE|0 tone ID, or 0 to stop all
|
||||||
|
---@param state boolean tone state
|
||||||
|
function public.diag__set_alarm_tone(id, state)
|
||||||
|
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_TONE_SET, { id, state }) end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- supervisor test alarm tones by alarm
|
||||||
|
---@param id ALARM|0 alarm ID, 0 to stop all
|
||||||
|
---@param state boolean alarm state
|
||||||
|
function public.diag__set_alarm(id, state)
|
||||||
|
if self.sv.linked then _send_sv(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
|
||||||
|
end
|
||||||
|
|
||||||
-- parse a packet
|
-- parse a packet
|
||||||
---@param side string
|
---@param side string
|
||||||
---@param sender integer
|
---@param sender integer
|
||||||
@ -205,6 +209,8 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
-- handle a packet
|
-- handle a packet
|
||||||
---@param packet mgmt_frame|capi_frame|nil
|
---@param packet mgmt_frame|capi_frame|nil
|
||||||
function public.handle_packet(packet)
|
function public.handle_packet(packet)
|
||||||
|
local diag = iocontrol.get_db().diag
|
||||||
|
|
||||||
if packet ~= nil then
|
if packet ~= nil then
|
||||||
local l_chan = packet.scada_frame.local_channel()
|
local l_chan = packet.scada_frame.local_channel()
|
||||||
local r_chan = packet.scada_frame.remote_channel()
|
local r_chan = packet.scada_frame.remote_channel()
|
||||||
@ -231,47 +237,9 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
-- feed watchdog on valid sequence number
|
-- feed watchdog on valid sequence number
|
||||||
api_watchdog.feed()
|
api_watchdog.feed()
|
||||||
|
|
||||||
if protocol == PROTOCOL.COORD_API then
|
if protocol == PROTOCOL.SCADA_MGMT then
|
||||||
---@cast packet capi_frame
|
|
||||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
|
||||||
---@cast packet mgmt_frame
|
---@cast packet mgmt_frame
|
||||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
if self.api.linked then
|
||||||
-- connection with coordinator established
|
|
||||||
if packet.length == 1 then
|
|
||||||
local est_ack = packet.data[1]
|
|
||||||
|
|
||||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
|
||||||
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)
|
|
||||||
else
|
|
||||||
coreio.report_link_state(LINK_STATE.API_LINK_ONLY)
|
|
||||||
end
|
|
||||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
|
||||||
if self.api.last_est_ack ~= est_ack then
|
|
||||||
log.info("coordinator connection denied")
|
|
||||||
end
|
|
||||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
|
||||||
if self.api.last_est_ack ~= est_ack then
|
|
||||||
log.info("coordinator connection denied due to collision")
|
|
||||||
end
|
|
||||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
|
||||||
if self.api.last_est_ack ~= est_ack then
|
|
||||||
log.info("coordinator comms version mismatch")
|
|
||||||
end
|
|
||||||
else
|
|
||||||
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
|
|
||||||
end
|
|
||||||
|
|
||||||
self.api.last_est_ack = est_ack
|
|
||||||
else
|
|
||||||
log.debug("coordinator SCADA_MGMT establish packet length mismatch")
|
|
||||||
end
|
|
||||||
elseif self.api.linked then
|
|
||||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
-- keep alive request received, echo back
|
-- keep alive request received, echo back
|
||||||
if packet.length == 1 then
|
if packet.length == 1 then
|
||||||
@ -298,6 +266,42 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
else
|
else
|
||||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
|
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
|
||||||
end
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||||
|
-- connection with coordinator established
|
||||||
|
if packet.length == 1 then
|
||||||
|
local est_ack = packet.data[1]
|
||||||
|
|
||||||
|
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||||
|
log.info("coordinator connection established")
|
||||||
|
self.establish_delay_counter = 0
|
||||||
|
self.api.linked = true
|
||||||
|
self.api.addr = src_addr
|
||||||
|
|
||||||
|
if self.sv.linked then
|
||||||
|
iocontrol.report_link_state(LINK_STATE.LINKED)
|
||||||
|
else
|
||||||
|
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||||
|
if self.api.last_est_ack ~= est_ack then
|
||||||
|
log.info("coordinator connection denied")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||||
|
if self.api.last_est_ack ~= est_ack then
|
||||||
|
log.info("coordinator connection denied due to collision")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||||
|
if self.api.last_est_ack ~= est_ack then
|
||||||
|
log.info("coordinator comms version mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("coordinator SCADA_MGMT establish packet reply unsupported")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.api.last_est_ack = est_ack
|
||||||
|
else
|
||||||
|
log.debug("coordinator SCADA_MGMT establish packet length mismatch")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
|
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
|
||||||
end
|
end
|
||||||
@ -325,43 +329,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
-- handle packet
|
-- handle packet
|
||||||
if protocol == PROTOCOL.SCADA_MGMT then
|
if protocol == PROTOCOL.SCADA_MGMT then
|
||||||
---@cast packet mgmt_frame
|
---@cast packet mgmt_frame
|
||||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
if self.sv.linked then
|
||||||
-- connection with supervisor established
|
|
||||||
if packet.length == 1 then
|
|
||||||
local est_ack = packet.data[1]
|
|
||||||
|
|
||||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
|
||||||
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)
|
|
||||||
else
|
|
||||||
coreio.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
|
||||||
end
|
|
||||||
elseif est_ack == ESTABLISH_ACK.DENY then
|
|
||||||
if self.sv.last_est_ack ~= est_ack then
|
|
||||||
log.info("supervisor connection denied")
|
|
||||||
end
|
|
||||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
|
||||||
if self.sv.last_est_ack ~= est_ack then
|
|
||||||
log.info("supervisor connection denied due to collision")
|
|
||||||
end
|
|
||||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
|
||||||
if self.sv.last_est_ack ~= est_ack then
|
|
||||||
log.info("supervisor comms version mismatch")
|
|
||||||
end
|
|
||||||
else
|
|
||||||
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
|
||||||
end
|
|
||||||
|
|
||||||
self.sv.last_est_ack = est_ack
|
|
||||||
else
|
|
||||||
log.debug("supervisor SCADA_MGMT establish packet length mismatch")
|
|
||||||
end
|
|
||||||
elseif self.sv.linked then
|
|
||||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
-- keep alive request received, echo back
|
-- keep alive request received, echo back
|
||||||
if packet.length == 1 then
|
if packet.length == 1 then
|
||||||
@ -385,9 +353,90 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
|
|||||||
self.sv.r_seq_num = nil
|
self.sv.r_seq_num = nil
|
||||||
self.sv.addr = comms.BROADCAST
|
self.sv.addr = comms.BROADCAST
|
||||||
log.info("supervisor server connection closed by remote host")
|
log.info("supervisor server connection closed by remote host")
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then
|
||||||
|
if packet.length == 8 then
|
||||||
|
for i = 1, #packet.data do
|
||||||
|
diag.tone_test.tone_indicators[i].update(packet.data[i] == true)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA diag alarm states packet length mismatch")
|
||||||
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then
|
||||||
|
if packet.length == 1 and packet.data[1] == false then
|
||||||
|
diag.tone_test.ready_warn.set_value("testing denied")
|
||||||
|
log.debug("supervisor SCADA diag tone set failed")
|
||||||
|
elseif packet.length == 2 and type(packet.data[2]) == "table" then
|
||||||
|
local ready = packet.data[1]
|
||||||
|
local states = packet.data[2]
|
||||||
|
|
||||||
|
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
|
||||||
|
|
||||||
|
for i = 1, #states do
|
||||||
|
if diag.tone_test.tone_buttons[i] ~= nil then
|
||||||
|
diag.tone_test.tone_buttons[i].set_value(states[i] == true)
|
||||||
|
diag.tone_test.tone_indicators[i].update(states[i] == true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA diag tone set packet length/type mismatch")
|
||||||
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then
|
||||||
|
if packet.length == 1 and packet.data[1] == false then
|
||||||
|
diag.tone_test.ready_warn.set_value("testing denied")
|
||||||
|
log.debug("supervisor SCADA diag alarm set failed")
|
||||||
|
elseif packet.length == 2 and type(packet.data[2]) == "table" then
|
||||||
|
local ready = packet.data[1]
|
||||||
|
local states = packet.data[2]
|
||||||
|
|
||||||
|
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
|
||||||
|
|
||||||
|
for i = 1, #states do
|
||||||
|
if diag.tone_test.alarm_buttons[i] ~= nil then
|
||||||
|
diag.tone_test.alarm_buttons[i].set_value(states[i] == true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA diag alarm set packet length/type mismatch")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
|
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
|
||||||
end
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||||
|
-- connection with supervisor established
|
||||||
|
if packet.length == 1 then
|
||||||
|
local est_ack = packet.data[1]
|
||||||
|
|
||||||
|
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||||
|
log.info("supervisor connection established")
|
||||||
|
self.establish_delay_counter = 0
|
||||||
|
self.sv.linked = true
|
||||||
|
self.sv.addr = src_addr
|
||||||
|
|
||||||
|
if self.api.linked then
|
||||||
|
iocontrol.report_link_state(LINK_STATE.LINKED)
|
||||||
|
else
|
||||||
|
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.DENY then
|
||||||
|
if self.sv.last_est_ack ~= est_ack then
|
||||||
|
log.info("supervisor connection denied")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||||
|
if self.sv.last_est_ack ~= est_ack then
|
||||||
|
log.info("supervisor connection denied due to collision")
|
||||||
|
end
|
||||||
|
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||||
|
if self.sv.last_est_ack ~= est_ack then
|
||||||
|
log.info("supervisor comms version mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA_MGMT establish packet reply unsupported")
|
||||||
|
end
|
||||||
|
|
||||||
|
self.sv.last_est_ack = est_ack
|
||||||
|
else
|
||||||
|
log.debug("supervisor SCADA_MGMT establish packet length mismatch")
|
||||||
|
end
|
||||||
else
|
else
|
||||||
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
|
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
|
||||||
end
|
end
|
||||||
|
@ -14,11 +14,11 @@ local util = require("scada-common.util")
|
|||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local config = require("pocket.config")
|
local config = require("pocket.config")
|
||||||
local coreio = require("pocket.coreio")
|
local iocontrol = require("pocket.iocontrol")
|
||||||
local pocket = require("pocket.pocket")
|
local pocket = require("pocket.pocket")
|
||||||
local renderer = require("pocket.renderer")
|
local renderer = require("pocket.renderer")
|
||||||
|
|
||||||
local POCKET_VERSION = "alpha-v0.5.2"
|
local POCKET_VERSION = "v0.6.0-alpha"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
@ -73,7 +73,7 @@ local function main()
|
|||||||
network.init_mac(config.AUTH_KEY)
|
network.init_mac(config.AUTH_KEY)
|
||||||
end
|
end
|
||||||
|
|
||||||
coreio.report_link_state(coreio.LINK_STATE.UNLINKED)
|
iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
|
||||||
|
|
||||||
-- get the communications modem
|
-- get the communications modem
|
||||||
local modem = ppm.get_wireless_modem()
|
local modem = ppm.get_wireless_modem()
|
||||||
@ -104,6 +104,9 @@ local function main()
|
|||||||
local MAIN_CLOCK = 0.5
|
local MAIN_CLOCK = 0.5
|
||||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||||
|
|
||||||
|
-- init I/O control
|
||||||
|
iocontrol.init_core(pocket_comms)
|
||||||
|
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
-- start the UI
|
-- start the UI
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
@ -128,6 +131,9 @@ local function main()
|
|||||||
conn_wd.api.feed()
|
conn_wd.api.feed()
|
||||||
log.debug("startup> conn watchdog started")
|
log.debug("startup> conn watchdog started")
|
||||||
|
|
||||||
|
local io_db = iocontrol.get_db()
|
||||||
|
local nav = io_db.nav
|
||||||
|
|
||||||
-- main event loop
|
-- main event loop
|
||||||
while true do
|
while true do
|
||||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||||
@ -140,6 +146,13 @@ local function main()
|
|||||||
-- relink if necessary
|
-- relink if necessary
|
||||||
pocket_comms.link_update()
|
pocket_comms.link_update()
|
||||||
|
|
||||||
|
-- update any tasks for the active page
|
||||||
|
if (type(nav.tasks[nav.page]) == "table") then
|
||||||
|
for i = 1, #nav.tasks[nav.page] do
|
||||||
|
nav.tasks[nav.page][i]()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
loop_clock.start()
|
loop_clock.start()
|
||||||
elseif conn_wd.sv.is_timer(param1) then
|
elseif conn_wd.sv.is_timer(param1) then
|
||||||
-- supervisor watchdog timeout
|
-- supervisor watchdog timeout
|
||||||
|
@ -2,17 +2,18 @@
|
|||||||
-- Pocket GUI Root
|
-- Pocket GUI Root
|
||||||
--
|
--
|
||||||
|
|
||||||
local coreio = require("pocket.coreio")
|
local iocontrol = require("pocket.iocontrol")
|
||||||
|
|
||||||
local style = require("pocket.ui.style")
|
local style = require("pocket.ui.style")
|
||||||
|
|
||||||
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
local conn_waiting = require("pocket.ui.components.conn_waiting")
|
||||||
|
|
||||||
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 boiler_page = require("pocket.ui.pages.boiler_page")
|
||||||
|
local diag_page = require("pocket.ui.pages.diag_page")
|
||||||
|
local home_page = require("pocket.ui.pages.home_page")
|
||||||
|
local reactor_page = require("pocket.ui.pages.reactor_page")
|
||||||
local turbine_page = require("pocket.ui.pages.turbine_page")
|
local turbine_page = require("pocket.ui.pages.turbine_page")
|
||||||
|
local unit_page = require("pocket.ui.pages.unit_page")
|
||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
@ -22,6 +23,9 @@ local TextBox = require("graphics.elements.textbox")
|
|||||||
|
|
||||||
local Sidebar = require("graphics.elements.controls.sidebar")
|
local Sidebar = require("graphics.elements.controls.sidebar")
|
||||||
|
|
||||||
|
local LINK_STATE = iocontrol.LINK_STATE
|
||||||
|
local NAV_PAGE = iocontrol.NAV_PAGE
|
||||||
|
|
||||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
local cpair = core.cpair
|
local cpair = core.cpair
|
||||||
@ -29,6 +33,9 @@ local cpair = core.cpair
|
|||||||
-- create new main view
|
-- create new main view
|
||||||
---@param main graphics_element main displaybox
|
---@param main graphics_element main displaybox
|
||||||
local function init(main)
|
local function init(main)
|
||||||
|
local nav = iocontrol.get_db().nav
|
||||||
|
local ps = iocontrol.get_db().ps
|
||||||
|
|
||||||
-- window header message
|
-- window header message
|
||||||
TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header}
|
TextBox{parent=main,y=1,text="",alignment=TEXT_ALIGN.LEFT,height=1,fg_bg=style.header}
|
||||||
|
|
||||||
@ -45,10 +52,10 @@ local function init(main)
|
|||||||
|
|
||||||
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes}
|
local root_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes=root_panes}
|
||||||
|
|
||||||
root_pane.register(coreio.core_ps(), "link_state", function (state)
|
root_pane.register(ps, "link_state", function (state)
|
||||||
if state == coreio.LINK_STATE.UNLINKED or state == coreio.LINK_STATE.API_LINK_ONLY then
|
if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
|
||||||
root_pane.set_value(1)
|
root_pane.set_value(1)
|
||||||
elseif state == coreio.LINK_STATE.SV_LINK_ONLY then
|
elseif state == LINK_STATE.SV_LINK_ONLY then
|
||||||
root_pane.set_value(2)
|
root_pane.set_value(2)
|
||||||
else
|
else
|
||||||
root_pane.set_value(3)
|
root_pane.set_value(3)
|
||||||
@ -81,19 +88,36 @@ local function init(main)
|
|||||||
{
|
{
|
||||||
char = "T",
|
char = "T",
|
||||||
color = cpair(colors.black,colors.white)
|
color = cpair(colors.black,colors.white)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
char = "D",
|
||||||
|
color = cpair(colors.black,colors.orange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
local pane_1 = home_page(page_div)
|
local panes = { home_page(page_div), unit_page(page_div), reactor_page(page_div), boiler_page(page_div), turbine_page(page_div), diag_page(page_div) }
|
||||||
local pane_2 = unit_page(page_div)
|
|
||||||
local pane_3 = reactor_page(page_div)
|
|
||||||
local pane_4 = boiler_page(page_div)
|
|
||||||
local pane_5 = turbine_page(page_div)
|
|
||||||
local panes = { pane_1, pane_2, pane_3, pane_4, pane_5 }
|
|
||||||
|
|
||||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||||
|
|
||||||
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=page_pane.set_value}
|
local function navigate_sidebar(page)
|
||||||
|
if page == 1 then
|
||||||
|
nav.page = nav.sub_pages[NAV_PAGE.HOME]
|
||||||
|
elseif page == 2 then
|
||||||
|
nav.page = nav.sub_pages[NAV_PAGE.UNITS]
|
||||||
|
elseif page == 3 then
|
||||||
|
nav.page = nav.sub_pages[NAV_PAGE.REACTORS]
|
||||||
|
elseif page == 4 then
|
||||||
|
nav.page = nav.sub_pages[NAV_PAGE.BOILERS]
|
||||||
|
elseif page == 5 then
|
||||||
|
nav.page = nav.sub_pages[NAV_PAGE.TURBINES]
|
||||||
|
elseif page == 6 then
|
||||||
|
nav.page = nav.sub_pages[NAV_PAGE.DIAG]
|
||||||
|
end
|
||||||
|
|
||||||
|
page_pane.set_value(page)
|
||||||
|
end
|
||||||
|
|
||||||
|
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=navigate_sidebar}
|
||||||
end
|
end
|
||||||
|
|
||||||
return init
|
return init
|
||||||
|
147
pocket/ui/pages/diag_page.lua
Normal file
147
pocket/ui/pages/diag_page.lua
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
local iocontrol = require("pocket.iocontrol")
|
||||||
|
|
||||||
|
local core = require("graphics.core")
|
||||||
|
|
||||||
|
local Div = require("graphics.elements.div")
|
||||||
|
local MultiPane = require("graphics.elements.multipane")
|
||||||
|
local TextBox = require("graphics.elements.textbox")
|
||||||
|
|
||||||
|
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||||
|
|
||||||
|
local App = require("graphics.elements.controls.app")
|
||||||
|
local Checkbox = require("graphics.elements.controls.checkbox")
|
||||||
|
local PushButton = require("graphics.elements.controls.push_button")
|
||||||
|
local SwitchButton = require("graphics.elements.controls.switch_button")
|
||||||
|
|
||||||
|
local cpair = core.cpair
|
||||||
|
|
||||||
|
local NAV_PAGE = iocontrol.NAV_PAGE
|
||||||
|
|
||||||
|
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||||
|
|
||||||
|
-- new diagnostics page view
|
||||||
|
---@param root graphics_element parent
|
||||||
|
local function new_view(root)
|
||||||
|
local db = iocontrol.get_db()
|
||||||
|
|
||||||
|
local main = Div{parent=root,x=1,y=1}
|
||||||
|
|
||||||
|
local diag_home = Div{parent=main,x=1,y=1}
|
||||||
|
|
||||||
|
TextBox{parent=diag_home,text="Diagnostic Apps",x=1,y=2,height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
|
||||||
|
local alarm_test = Div{parent=main,x=1,y=1}
|
||||||
|
|
||||||
|
local panes = { diag_home, alarm_test }
|
||||||
|
|
||||||
|
local page_pane = MultiPane{parent=main,x=1,y=1,panes=panes}
|
||||||
|
|
||||||
|
local function navigate_diag()
|
||||||
|
page_pane.set_value(1)
|
||||||
|
db.nav.page = NAV_PAGE.DIAG
|
||||||
|
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.DIAG
|
||||||
|
end
|
||||||
|
|
||||||
|
local function navigate_alarm()
|
||||||
|
page_pane.set_value(2)
|
||||||
|
db.nav.page = NAV_PAGE.D_ALARMS
|
||||||
|
db.nav.sub_pages[NAV_PAGE.DIAG] = NAV_PAGE.D_ALARMS
|
||||||
|
end
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
-- Alarm Testing Page --
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
db.nav.register_task(NAV_PAGE.D_ALARMS, db.diag.tone_test.get_tone_states)
|
||||||
|
|
||||||
|
local ttest = db.diag.tone_test
|
||||||
|
|
||||||
|
local c_wht_gray = cpair(colors.white, colors.gray)
|
||||||
|
local c_red_gray = cpair(colors.red, colors.gray)
|
||||||
|
local c_yel_gray = cpair(colors.yellow, colors.gray)
|
||||||
|
local c_blue_gray = cpair(colors.blue, colors.gray)
|
||||||
|
|
||||||
|
local audio = Div{parent=alarm_test,x=1,y=1}
|
||||||
|
|
||||||
|
TextBox{parent=audio,y=1,text="Alarm Sounder Tests",height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
|
||||||
|
ttest.ready_warn = TextBox{parent=audio,y=2,text="",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.yellow,colors.black)}
|
||||||
|
|
||||||
|
PushButton{parent=audio,x=13,y=18,text="\x11 BACK",min_width=8,fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=c_wht_gray,callback=navigate_diag}
|
||||||
|
|
||||||
|
local tones = Div{parent=audio,x=2,y=3,height=10,width=8,fg_bg=cpair(colors.black,colors.yellow)}
|
||||||
|
|
||||||
|
TextBox{parent=tones,text="Tones",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
|
||||||
|
|
||||||
|
local test_btns = {}
|
||||||
|
test_btns[1] = SwitchButton{parent=tones,text="TEST 1",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_1}
|
||||||
|
test_btns[2] = SwitchButton{parent=tones,text="TEST 2",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_2}
|
||||||
|
test_btns[3] = SwitchButton{parent=tones,text="TEST 3",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_3}
|
||||||
|
test_btns[4] = SwitchButton{parent=tones,text="TEST 4",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_4}
|
||||||
|
test_btns[5] = SwitchButton{parent=tones,text="TEST 5",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_5}
|
||||||
|
test_btns[6] = SwitchButton{parent=tones,text="TEST 6",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_6}
|
||||||
|
test_btns[7] = SwitchButton{parent=tones,text="TEST 7",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_7}
|
||||||
|
test_btns[8] = SwitchButton{parent=tones,text="TEST 8",min_width=8,active_fg_bg=c_wht_gray,callback=ttest.test_8}
|
||||||
|
|
||||||
|
ttest.tone_buttons = test_btns
|
||||||
|
|
||||||
|
local function stop_all_tones()
|
||||||
|
for i = 1, #test_btns do test_btns[i].set_value(false) end
|
||||||
|
ttest.stop_tones()
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=tones,text="STOP",min_width=8,active_fg_bg=c_wht_gray,fg_bg=cpair(colors.black,colors.red),callback=stop_all_tones}
|
||||||
|
|
||||||
|
local alarms = Div{parent=audio,x=11,y=3,height=15,fg_bg=cpair(colors.lightGray,colors.black)}
|
||||||
|
|
||||||
|
TextBox{parent=alarms,text="Alarms (\x13)",height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=audio.get_fg_bg()}
|
||||||
|
|
||||||
|
local alarm_btns = {}
|
||||||
|
alarm_btns[1] = Checkbox{parent=alarms,label="BREACH",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_breach}
|
||||||
|
alarm_btns[2] = Checkbox{parent=alarms,label="RADIATION",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_rad}
|
||||||
|
alarm_btns[3] = Checkbox{parent=alarms,label="RCT LOST",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_lost}
|
||||||
|
alarm_btns[4] = Checkbox{parent=alarms,label="CRIT DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_crit}
|
||||||
|
alarm_btns[5] = Checkbox{parent=alarms,label="DAMAGE",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_dmg}
|
||||||
|
alarm_btns[6] = Checkbox{parent=alarms,label="OVER TEMP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_overtemp}
|
||||||
|
alarm_btns[7] = Checkbox{parent=alarms,label="HIGH TEMP",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_hightemp}
|
||||||
|
alarm_btns[8] = Checkbox{parent=alarms,label="WASTE LEAK",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_wasteleak}
|
||||||
|
alarm_btns[9] = Checkbox{parent=alarms,label="WASTE HIGH",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_highwaste}
|
||||||
|
alarm_btns[10] = Checkbox{parent=alarms,label="RPS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rps}
|
||||||
|
alarm_btns[11] = Checkbox{parent=alarms,label="RCS TRANS",min_width=15,box_fg_bg=c_yel_gray,callback=ttest.test_rcs}
|
||||||
|
alarm_btns[12] = Checkbox{parent=alarms,label="TURBINE TRP",min_width=15,box_fg_bg=c_red_gray,callback=ttest.test_turbinet}
|
||||||
|
|
||||||
|
ttest.alarm_buttons = alarm_btns
|
||||||
|
|
||||||
|
local function stop_all_alarms()
|
||||||
|
for i = 1, #alarm_btns do alarm_btns[i].set_value(false) end
|
||||||
|
ttest.stop_alarms()
|
||||||
|
end
|
||||||
|
|
||||||
|
PushButton{parent=alarms,x=3,y=15,text="STOP \x13",min_width=8,fg_bg=cpair(colors.black,colors.red),active_fg_bg=c_wht_gray,callback=stop_all_alarms}
|
||||||
|
|
||||||
|
local states = Div{parent=audio,x=2,y=14,height=5,width=8}
|
||||||
|
|
||||||
|
TextBox{parent=states,text="States",height=1,alignment=TEXT_ALIGN.CENTER}
|
||||||
|
local t_1 = IndicatorLight{parent=states,label="1",colors=c_blue_gray}
|
||||||
|
local t_2 = IndicatorLight{parent=states,label="2",colors=c_blue_gray}
|
||||||
|
local t_3 = IndicatorLight{parent=states,label="3",colors=c_blue_gray}
|
||||||
|
local t_4 = IndicatorLight{parent=states,label="4",colors=c_blue_gray}
|
||||||
|
local t_5 = IndicatorLight{parent=states,x=6,y=2,label="5",colors=c_blue_gray}
|
||||||
|
local t_6 = IndicatorLight{parent=states,x=6,label="6",colors=c_blue_gray}
|
||||||
|
local t_7 = IndicatorLight{parent=states,x=6,label="7",colors=c_blue_gray}
|
||||||
|
local t_8 = IndicatorLight{parent=states,x=6,label="8",colors=c_blue_gray}
|
||||||
|
|
||||||
|
ttest.tone_indicators = { t_1, t_2, t_3, t_4, t_5, t_6, t_7, t_8 }
|
||||||
|
|
||||||
|
--------------
|
||||||
|
-- App List --
|
||||||
|
--------------
|
||||||
|
|
||||||
|
App{parent=diag_home,x=3,y=4,text="\x0f",title="Alarm",callback=navigate_alarm,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||||
|
App{parent=diag_home,x=10,y=4,text="\x1e",title="LoopT",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
|
||||||
|
App{parent=diag_home,x=17,y=4,text="@",title="Comps",callback=function()end,app_fg_bg=cpair(colors.black,colors.orange)}
|
||||||
|
|
||||||
|
return main
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_view
|
@ -1,20 +1,21 @@
|
|||||||
-- local style = require("pocket.ui.style")
|
|
||||||
|
|
||||||
local core = require("graphics.core")
|
local core = require("graphics.core")
|
||||||
|
|
||||||
local Div = require("graphics.elements.div")
|
local Div = require("graphics.elements.div")
|
||||||
local TextBox = require("graphics.elements.textbox")
|
|
||||||
|
|
||||||
-- local cpair = core.cpair
|
local App = require("graphics.elements.controls.app")
|
||||||
|
|
||||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
local cpair = core.cpair
|
||||||
|
|
||||||
-- new home page view
|
-- new home page view
|
||||||
---@param root graphics_element parent
|
---@param root graphics_element parent
|
||||||
local function new_view(root)
|
local function new_view(root)
|
||||||
local main = Div{parent=root,x=1,y=1}
|
local main = Div{parent=root,x=1,y=1}
|
||||||
|
|
||||||
TextBox{parent=main,text="HOME",x=1,y=1,height=1,alignment=TEXT_ALIGN.CENTER}
|
App{parent=main,x=3,y=2,text="\x17",title="PRC",callback=function()end,app_fg_bg=cpair(colors.black,colors.purple)}
|
||||||
|
App{parent=main,x=10,y=2,text="\x15",title="CTL",callback=function()end,app_fg_bg=cpair(colors.black,colors.green)}
|
||||||
|
App{parent=main,x=17,y=2,text="\x08",title="DEV",callback=function()end,app_fg_bg=cpair(colors.black,colors.lightGray)}
|
||||||
|
App{parent=main,x=3,y=7,text="\x7f",title="Waste",callback=function()end,app_fg_bg=cpair(colors.black,colors.brown)}
|
||||||
|
App{parent=main,x=10,y=7,text="\xb6",title="Guide",callback=function()end,app_fg_bg=cpair(colors.black,colors.cyan)}
|
||||||
|
|
||||||
return main
|
return main
|
||||||
end
|
end
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
--
|
--
|
||||||
|
121
rtu/rtu.lua
121
rtu/rtu.lua
@ -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
|
||||||
|
|
||||||
@ -387,7 +432,49 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
|||||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||||
---@cast packet mgmt_frame
|
---@cast packet mgmt_frame
|
||||||
-- SCADA management packet
|
-- SCADA management packet
|
||||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
if rtu_state.linked then
|
||||||
|
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||||
|
-- keep alive request received, echo back
|
||||||
|
if packet.length == 1 and type(packet.data[1]) == "number" then
|
||||||
|
local timestamp = packet.data[1]
|
||||||
|
local trip_time = util.time() - timestamp
|
||||||
|
|
||||||
|
if trip_time > 750 then
|
||||||
|
log.warning("RTU KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- log.debug("RTU RTT = " .. trip_time .. "ms")
|
||||||
|
|
||||||
|
_send_keep_alive_ack(timestamp)
|
||||||
|
else
|
||||||
|
log.debug("SCADA_MGMT keep alive packet length/type mismatch")
|
||||||
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||||
|
-- close connection
|
||||||
|
conn_watchdog.cancel()
|
||||||
|
public.unlink(rtu_state)
|
||||||
|
println_ts("server connection closed by remote host")
|
||||||
|
log.warning("server connection closed by remote host")
|
||||||
|
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] == true) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- not supported
|
||||||
|
log.debug("received unsupported SCADA_MGMT message type " .. packet.type)
|
||||||
|
end
|
||||||
|
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||||
if packet.length == 1 then
|
if packet.length == 1 then
|
||||||
local est_ack = packet.data[1]
|
local est_ack = packet.data[1]
|
||||||
|
|
||||||
@ -421,36 +508,6 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
|
|||||||
else
|
else
|
||||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||||
end
|
end
|
||||||
elseif rtu_state.linked then
|
|
||||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
|
||||||
-- keep alive request received, echo back
|
|
||||||
if packet.length == 1 and type(packet.data[1]) == "number" then
|
|
||||||
local timestamp = packet.data[1]
|
|
||||||
local trip_time = util.time() - timestamp
|
|
||||||
|
|
||||||
if trip_time > 750 then
|
|
||||||
log.warning("RTU KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- log.debug("RTU RTT = " .. trip_time .. "ms")
|
|
||||||
|
|
||||||
_send_keep_alive_ack(timestamp)
|
|
||||||
else
|
|
||||||
log.debug("SCADA_MGMT keep alive packet length/type mismatch")
|
|
||||||
end
|
|
||||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
|
||||||
-- close connection
|
|
||||||
conn_watchdog.cancel()
|
|
||||||
public.unlink(rtu_state)
|
|
||||||
println_ts("server connection closed by remote host")
|
|
||||||
log.warning("server connection closed by remote host")
|
|
||||||
elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then
|
|
||||||
-- request for capabilities again
|
|
||||||
public.send_advertisement(units)
|
|
||||||
else
|
|
||||||
-- not supported
|
|
||||||
log.debug("received unsupported SCADA_MGMT message type " .. packet.type)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
||||||
end
|
end
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
@ -24,7 +25,7 @@ local threads = {}
|
|||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
local UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
|
||||||
|
|
||||||
local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
|
local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks)
|
||||||
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
|
||||||
|
|
||||||
-- main thread
|
-- main thread
|
||||||
@ -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
|
||||||
@ -66,6 +68,15 @@ function threads.thread__main(smem)
|
|||||||
-- blink heartbeat indicator
|
-- blink heartbeat indicator
|
||||||
databus.heartbeat()
|
databus.heartbeat()
|
||||||
|
|
||||||
|
-- update speaker states
|
||||||
|
for _, sounder in pairs(sounders) do
|
||||||
|
-- re-compute output if needed, then play audio if available
|
||||||
|
if sounder.stream.is_recompute_needed() then
|
||||||
|
sounder.stream.compute_buffer()
|
||||||
|
if sounder.stream.has_next_block() then sounder.play() else sounder.stop() end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- start next clock timer
|
-- start next clock timer
|
||||||
loop_clock.start()
|
loop_clock.start()
|
||||||
|
|
||||||
@ -110,6 +121,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 +170,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 +282,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 +338,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 +361,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
|
||||||
|
|
||||||
|
313
scada-common/audio.lua
Normal file
313
scada-common/audio.lua
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
--
|
||||||
|
-- Audio & Tone Control for Alarms
|
||||||
|
--
|
||||||
|
|
||||||
|
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
|
||||||
|
|
||||||
|
-- note: max samples = 0x20000 (128 * 1024 samples)
|
||||||
|
|
||||||
|
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
||||||
|
local _DRATE = 48000 -- 48kHz audio
|
||||||
|
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
||||||
|
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
||||||
|
|
||||||
|
---@class audio
|
||||||
|
local audio = {}
|
||||||
|
|
||||||
|
---@enum TONE
|
||||||
|
local TONE = {
|
||||||
|
T_340Hz_Int_2Hz = 1,
|
||||||
|
T_544Hz_440Hz_Alt = 2,
|
||||||
|
T_660Hz_Int_125ms = 3,
|
||||||
|
T_745Hz_Int_1Hz = 4,
|
||||||
|
T_800Hz_Int = 5,
|
||||||
|
T_800Hz_1000Hz_Alt = 6,
|
||||||
|
T_1000Hz_Int = 7,
|
||||||
|
T_1800Hz_Int_4Hz = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
audio.TONE = TONE
|
||||||
|
|
||||||
|
local tone_data = {
|
||||||
|
{ {}, {}, {}, {} }, -- 340Hz @ 2Hz Intermittent
|
||||||
|
{ {}, {}, {}, {} }, -- 544Hz 100mS / 440Hz 400mS Alternating
|
||||||
|
{ {}, {}, {}, {} }, -- 660Hz @ 125ms On 125ms Off
|
||||||
|
{ {}, {}, {}, {} }, -- 745Hz @ 1Hz Intermittent
|
||||||
|
{ {}, {}, {}, {} }, -- 800Hz @ 0.25s On 1.75s Off
|
||||||
|
{ {}, {}, {}, {} }, -- 800/1000Hz @ 0.25s Alternating
|
||||||
|
{ {}, {}, {}, {} }, -- 1KHz 1s on, 1s off Intermittent
|
||||||
|
{ {}, {}, {}, {} } -- 1.8KHz @ 4Hz Intermittent
|
||||||
|
}
|
||||||
|
|
||||||
|
-- calculate how many samples are in the given number of milliseconds
|
||||||
|
---@nodiscard
|
||||||
|
---@param ms integer milliseconds
|
||||||
|
---@return integer samples
|
||||||
|
local function ms_to_samples(ms) return math.floor(ms * 48) end
|
||||||
|
|
||||||
|
--#region Tone Generation (the Maths)
|
||||||
|
|
||||||
|
-- 340Hz @ 2Hz Intermittent
|
||||||
|
local function gen_tone_1()
|
||||||
|
local t, dt = 0, _2_PI * 340 / _DRATE
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[1][1][i] = val
|
||||||
|
tone_data[1][3][i] = val
|
||||||
|
tone_data[1][2][i] = 0
|
||||||
|
tone_data[1][4][i] = 0
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 544Hz 100mS / 440Hz 400mS Alternating
|
||||||
|
local function gen_tone_2()
|
||||||
|
local t1, dt1 = 0, _2_PI * 544 / _DRATE
|
||||||
|
local t2, dt2 = 0, _2_PI * 440 / _DRATE
|
||||||
|
local alternate_at = ms_to_samples(100)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local value
|
||||||
|
|
||||||
|
if i <= alternate_at then
|
||||||
|
value = math.floor(math.sin(t1) * _MAX_VAL)
|
||||||
|
t1 = (t1 + dt1) % _2_PI
|
||||||
|
else
|
||||||
|
value = math.floor(math.sin(t2) * _MAX_VAL)
|
||||||
|
t2 = (t2 + dt2) % _2_PI
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[2][1][i] = value
|
||||||
|
tone_data[2][2][i] = value
|
||||||
|
tone_data[2][3][i] = value
|
||||||
|
tone_data[2][4][i] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 660Hz @ 125ms On 125ms Off
|
||||||
|
local function gen_tone_3()
|
||||||
|
local elapsed_samples = 0
|
||||||
|
local alternate_after = ms_to_samples(125)
|
||||||
|
local alternate_at = alternate_after
|
||||||
|
local mode = true
|
||||||
|
|
||||||
|
local t, dt = 0, _2_PI * 660 / _DRATE
|
||||||
|
|
||||||
|
for set = 1, 4 do
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
if mode then
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[3][set][i] = val
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
else
|
||||||
|
t = 0
|
||||||
|
tone_data[3][set][i] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
if elapsed_samples == alternate_at then
|
||||||
|
mode = not mode
|
||||||
|
alternate_at = elapsed_samples + alternate_after
|
||||||
|
end
|
||||||
|
|
||||||
|
elapsed_samples = elapsed_samples + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 745Hz @ 1Hz Intermittent
|
||||||
|
local function gen_tone_4()
|
||||||
|
local t, dt = 0, _2_PI * 745 / _DRATE
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[4][1][i] = val
|
||||||
|
tone_data[4][3][i] = val
|
||||||
|
tone_data[4][2][i] = 0
|
||||||
|
tone_data[4][4][i] = 0
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 800Hz @ 0.25s On 1.75s Off
|
||||||
|
local function gen_tone_5()
|
||||||
|
local t, dt = 0, _2_PI * 800 / _DRATE
|
||||||
|
local stop_at = ms_to_samples(250)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
|
||||||
|
if i > stop_at then
|
||||||
|
tone_data[5][1][i] = val
|
||||||
|
else
|
||||||
|
tone_data[5][1][i] = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[5][2][i] = 0
|
||||||
|
tone_data[5][3][i] = 0
|
||||||
|
tone_data[5][4][i] = 0
|
||||||
|
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1000/800Hz @ 0.25s Alternating
|
||||||
|
local function gen_tone_6()
|
||||||
|
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
|
||||||
|
local t2, dt2 = 0, _2_PI * 800 / _DRATE
|
||||||
|
|
||||||
|
local alternate_at = ms_to_samples(250)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val
|
||||||
|
if i <= alternate_at then
|
||||||
|
val = math.floor(math.sin(t1) * _MAX_VAL)
|
||||||
|
t1 = (t1 + dt1) % _2_PI
|
||||||
|
else
|
||||||
|
val = math.floor(math.sin(t2) * _MAX_VAL)
|
||||||
|
t2 = (t2 + dt2) % _2_PI
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[6][1][i] = val
|
||||||
|
tone_data[6][2][i] = val
|
||||||
|
tone_data[6][3][i] = val
|
||||||
|
tone_data[6][4][i] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1KHz 1s on, 1s off Intermittent
|
||||||
|
local function gen_tone_7()
|
||||||
|
local t, dt = 0, _2_PI * 1000 / _DRATE
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
tone_data[7][1][i] = val
|
||||||
|
tone_data[7][2][i] = val
|
||||||
|
tone_data[7][3][i] = 0
|
||||||
|
tone_data[7][4][i] = 0
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1800Hz @ 4Hz Intermittent
|
||||||
|
local function gen_tone_8()
|
||||||
|
local t, dt = 0, _2_PI * 1800 / _DRATE
|
||||||
|
|
||||||
|
local off_at = ms_to_samples(250)
|
||||||
|
|
||||||
|
for i = 1, _05s_SAMPLES do
|
||||||
|
local val = 0
|
||||||
|
|
||||||
|
if i <= off_at then
|
||||||
|
val = math.floor(math.sin(t) * _MAX_VAL)
|
||||||
|
t = (t + dt) % _2_PI
|
||||||
|
end
|
||||||
|
|
||||||
|
tone_data[8][1][i] = val
|
||||||
|
tone_data[8][2][i] = val
|
||||||
|
tone_data[8][3][i] = val
|
||||||
|
tone_data[8][4][i] = val
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--#endregion
|
||||||
|
|
||||||
|
-- generate all 8 tone sequences
|
||||||
|
function audio.generate_tones()
|
||||||
|
gen_tone_1(); gen_tone_2(); gen_tone_3(); gen_tone_4(); gen_tone_5(); gen_tone_6(); gen_tone_7(); gen_tone_8()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- hard audio limiter
|
||||||
|
---@nodiscard
|
||||||
|
---@param output number output level
|
||||||
|
---@return number limited -128.0 to 127.0
|
||||||
|
local function limit(output)
|
||||||
|
return math.max(-128, math.min(127, output))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- clear output buffer
|
||||||
|
---@param buffer table quad buffer
|
||||||
|
local function clear(buffer)
|
||||||
|
for i = 1, 4 do
|
||||||
|
for s = 1, _05s_SAMPLES do buffer[i][s] = 0 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create a new audio tone stream controller
|
||||||
|
function audio.new_stream()
|
||||||
|
local self = {
|
||||||
|
any_active = false,
|
||||||
|
need_recompute = false,
|
||||||
|
next_block = 1,
|
||||||
|
-- split audio up into 0.5s samples, so specific components can be ended quicker
|
||||||
|
quad_buffer = { {}, {}, {}, {} },
|
||||||
|
-- all tone enable states
|
||||||
|
tone_active = { false, false, false, false, false, false, false, false }
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(self.quad_buffer)
|
||||||
|
|
||||||
|
---@class tone_stream
|
||||||
|
local public = {}
|
||||||
|
|
||||||
|
-- add a tone to the output buffer
|
||||||
|
---@param index TONE tone ID
|
||||||
|
---@param active boolean active state
|
||||||
|
function public.set_active(index, active)
|
||||||
|
if self.tone_active[index] ~= nil then
|
||||||
|
if self.tone_active[index] ~= active then self.need_recompute = true end
|
||||||
|
self.tone_active[index] = active
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if a tone is active
|
||||||
|
---@param index TONE tone index
|
||||||
|
function public.is_active(index)
|
||||||
|
if self.tone_active[index] then return self.tone_active[index] end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set all tones inactive, reset next block, and clear output buffer
|
||||||
|
function public.stop()
|
||||||
|
for i = 1, #self.tone_active do self.tone_active[i] = false end
|
||||||
|
self.next_block = 1
|
||||||
|
clear(self.quad_buffer)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if the output buffer needs to be recomputed due to changes
|
||||||
|
function public.is_recompute_needed() return self.need_recompute end
|
||||||
|
|
||||||
|
-- re-compute the output buffer
|
||||||
|
function public.compute_buffer()
|
||||||
|
clear(self.quad_buffer)
|
||||||
|
|
||||||
|
self.need_recompute = false
|
||||||
|
self.any_active = false
|
||||||
|
|
||||||
|
for id = 1, #tone_data do
|
||||||
|
if self.tone_active[id] then
|
||||||
|
self.any_active = true
|
||||||
|
for i = 1, 4 do
|
||||||
|
local buffer = self.quad_buffer[i]
|
||||||
|
local values = tone_data[id][i]
|
||||||
|
for s = 1, _05s_SAMPLES do self.quad_buffer[i][s] = limit(buffer[s] + values[s]) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check if the next audio block has data
|
||||||
|
function public.has_next_block() return #self.quad_buffer[self.next_block] > 0 end
|
||||||
|
|
||||||
|
-- get the next audio block
|
||||||
|
function public.get_next_block()
|
||||||
|
local block = self.quad_buffer[self.next_block]
|
||||||
|
self.next_block = self.next_block + 1
|
||||||
|
if self.next_block > 4 then self.next_block = 1 end
|
||||||
|
return block
|
||||||
|
end
|
||||||
|
|
||||||
|
return public
|
||||||
|
end
|
||||||
|
|
||||||
|
return audio
|
@ -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.1"
|
||||||
|
|
||||||
---@enum PROTOCOL
|
---@enum PROTOCOL
|
||||||
local PROTOCOL = {
|
local PROTOCOL = {
|
||||||
@ -46,7 +46,11 @@ 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
|
||||||
|
DIAG_TONE_GET = 6, -- diagnostic: get alarm tones
|
||||||
|
DIAG_TONE_SET = 7, -- diagnostic: set alarm tones
|
||||||
|
DIAG_ALARM_SET = 8 -- diagnostic: set alarm to simulate audio for
|
||||||
}
|
}
|
||||||
|
|
||||||
---@enum SCADA_CRDN_TYPE
|
---@enum SCADA_CRDN_TYPE
|
||||||
@ -146,6 +150,7 @@ function comms.scada_packet()
|
|||||||
local self = {
|
local self = {
|
||||||
modem_msg_in = nil, ---@type modem_message|nil
|
modem_msg_in = nil, ---@type modem_message|nil
|
||||||
valid = false,
|
valid = false,
|
||||||
|
authenticated = false,
|
||||||
raw = {},
|
raw = {},
|
||||||
src_addr = comms.BROADCAST,
|
src_addr = comms.BROADCAST,
|
||||||
dest_addr = comms.BROADCAST,
|
dest_addr = comms.BROADCAST,
|
||||||
@ -234,6 +239,9 @@ function comms.scada_packet()
|
|||||||
return self.valid
|
return self.valid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- report that this packet has been authenticated (was received with a valid HMAC)
|
||||||
|
function public.stamp_authenticated() self.authenticated = true end
|
||||||
|
|
||||||
-- public accessors --
|
-- public accessors --
|
||||||
|
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
@ -248,6 +256,8 @@ function comms.scada_packet()
|
|||||||
|
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
function public.is_valid() return self.valid end
|
function public.is_valid() return self.valid end
|
||||||
|
---@nodiscard
|
||||||
|
function public.is_authenticated() return self.authenticated end
|
||||||
|
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
function public.src_addr() return self.src_addr end
|
function public.src_addr() return self.src_addr end
|
||||||
@ -588,7 +598,11 @@ function comms.mgmt_packet()
|
|||||||
self.type == SCADA_MGMT_TYPE.CLOSE or
|
self.type == SCADA_MGMT_TYPE.CLOSE or
|
||||||
self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or
|
self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or
|
||||||
self.type == SCADA_MGMT_TYPE.RTU_ADVERT or
|
self.type == SCADA_MGMT_TYPE.RTU_ADVERT or
|
||||||
self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT
|
self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT or
|
||||||
|
self.type == SCADA_MGMT_TYPE.RTU_TONE_ALARM or
|
||||||
|
self.type == SCADA_MGMT_TYPE.DIAG_TONE_GET or
|
||||||
|
self.type == SCADA_MGMT_TYPE.DIAG_TONE_SET or
|
||||||
|
self.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET
|
||||||
end
|
end
|
||||||
|
|
||||||
-- make a SCADA management packet
|
-- make a SCADA management packet
|
||||||
|
@ -212,6 +212,7 @@ function network.nic(modem)
|
|||||||
if packet_hmac == computed_hmac then
|
if packet_hmac == computed_hmac then
|
||||||
-- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
|
-- log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
|
||||||
s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance)
|
s_packet.receive(side, sender, reply_to, textutils.unserialize(msg), distance)
|
||||||
|
s_packet.stamp_authenticated()
|
||||||
else
|
else
|
||||||
-- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
|
-- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
local audio = require("scada-common.audio")
|
||||||
local const = require("scada-common.constants")
|
local const = require("scada-common.constants")
|
||||||
local log = require("scada-common.log")
|
local log = require("scada-common.log")
|
||||||
local rsio = require("scada-common.rsio")
|
local rsio = require("scada-common.rsio")
|
||||||
@ -8,12 +9,16 @@ local unit = require("supervisor.unit")
|
|||||||
|
|
||||||
local rsctl = require("supervisor.session.rsctl")
|
local rsctl = require("supervisor.session.rsctl")
|
||||||
|
|
||||||
|
local TONE = audio.TONE
|
||||||
|
|
||||||
|
local ALARM = types.ALARM
|
||||||
|
local PRIO = types.ALARM_PRIORITY
|
||||||
|
local ALARM_STATE = types.ALARM_STATE
|
||||||
local PROCESS = types.PROCESS
|
local PROCESS = types.PROCESS
|
||||||
local PROCESS_NAMES = types.PROCESS_NAMES
|
local PROCESS_NAMES = types.PROCESS_NAMES
|
||||||
local PRIO = types.ALARM_PRIORITY
|
|
||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
local WASTE = types.WASTE_PRODUCT
|
|
||||||
local WASTE_MODE = types.WASTE_MODE
|
local WASTE_MODE = types.WASTE_MODE
|
||||||
|
local WASTE = types.WASTE_PRODUCT
|
||||||
|
|
||||||
local IO = rsio.IO
|
local IO = rsio.IO
|
||||||
|
|
||||||
@ -60,6 +65,7 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
units = {},
|
units = {},
|
||||||
status_text = { "START UP", "initializing..." },
|
status_text = { "START UP", "initializing..." },
|
||||||
all_sys_ok = false,
|
all_sys_ok = false,
|
||||||
|
allow_testing = false,
|
||||||
-- rtus
|
-- rtus
|
||||||
rtu_conn_count = 0,
|
rtu_conn_count = 0,
|
||||||
rtu_list = {},
|
rtu_list = {},
|
||||||
@ -109,6 +115,12 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
waste_product = WASTE.PLUTONIUM,
|
waste_product = WASTE.PLUTONIUM,
|
||||||
current_waste_product = WASTE.PLUTONIUM,
|
current_waste_product = WASTE.PLUTONIUM,
|
||||||
pu_fallback = false,
|
pu_fallback = false,
|
||||||
|
-- alarm tones
|
||||||
|
tone_states = {},
|
||||||
|
test_tone_set = false,
|
||||||
|
test_tone_reset = false,
|
||||||
|
test_tone_states = {},
|
||||||
|
test_alarm_states = {},
|
||||||
-- statistics
|
-- statistics
|
||||||
im_stat_init = false,
|
im_stat_init = false,
|
||||||
avg_charge = util.mov_avg(3, 0.0),
|
avg_charge = util.mov_avg(3, 0.0),
|
||||||
@ -128,6 +140,13 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
-- init redstone RTU I/O controller
|
-- init redstone RTU I/O controller
|
||||||
self.io_ctl = rsctl.new(self.redstone)
|
self.io_ctl = rsctl.new(self.redstone)
|
||||||
|
|
||||||
|
-- fill blank alarm/tone states
|
||||||
|
for _ = 1, 12 do table.insert(self.test_alarm_states, false) end
|
||||||
|
for _ = 1, 8 do
|
||||||
|
table.insert(self.tone_states, false)
|
||||||
|
table.insert(self.test_tone_states, false)
|
||||||
|
end
|
||||||
|
|
||||||
-- check if all auto-controlled units completed ramping
|
-- check if all auto-controlled units completed ramping
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
local function _all_units_ramped()
|
local function _all_units_ramped()
|
||||||
@ -262,15 +281,20 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
|
|
||||||
-- supervisor sessions reporting the list of active RTU sessions
|
-- supervisor sessions reporting the list of active RTU sessions
|
||||||
---@param rtu_sessions table session list of all connected RTUs
|
---@param rtu_sessions table session list of all connected RTUs
|
||||||
function public.report_rtus(rtu_sessions)
|
function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end
|
||||||
self.rtu_conn_count = #rtu_sessions
|
|
||||||
end
|
|
||||||
|
|
||||||
-- update (iterate) the facility management
|
-- update (iterate) the facility management
|
||||||
function public.update()
|
function public.update()
|
||||||
-- unlink RTU unit sessions if they are closed
|
-- unlink RTU unit sessions if they are closed
|
||||||
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
|
for _, v in pairs(self.rtu_list) do util.filter_table(v, function (u) return u.is_connected() end) end
|
||||||
|
|
||||||
|
-- check if test routines are allowed right now
|
||||||
|
self.allow_testing = true
|
||||||
|
for i = 1, #self.units do
|
||||||
|
local u = self.units[i] ---@type reactor_unit
|
||||||
|
self.allow_testing = self.allow_testing and u.is_safe_idle()
|
||||||
|
end
|
||||||
|
|
||||||
-- current state for process control
|
-- current state for process control
|
||||||
local charge_update = 0
|
local charge_update = 0
|
||||||
local rate_update = 0
|
local rate_update = 0
|
||||||
@ -750,6 +774,97 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then
|
if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then
|
||||||
self.current_waste_product = WASTE.PLUTONIUM
|
self.current_waste_product = WASTE.PLUTONIUM
|
||||||
else self.current_waste_product = self.waste_product end
|
else self.current_waste_product = self.waste_product end
|
||||||
|
|
||||||
|
------------------------
|
||||||
|
-- Update Alarm Tones --
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
local allow_test = self.allow_testing and self.test_tone_set
|
||||||
|
|
||||||
|
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||||
|
|
||||||
|
-- reset tone states before re-evaluting
|
||||||
|
for i = 1, #self.tone_states do self.tone_states[i] = false end
|
||||||
|
|
||||||
|
if allow_test then
|
||||||
|
alarms = self.test_alarm_states
|
||||||
|
else
|
||||||
|
-- check all alarms for all units
|
||||||
|
for i = 1, #self.units do
|
||||||
|
local u = self.units[i] ---@type reactor_unit
|
||||||
|
for id, alarm in pairs(u.get_alarms()) do
|
||||||
|
alarms[id] = alarms[id] or (alarm == ALARM_STATE.TRIPPED)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.test_tone_reset then
|
||||||
|
-- clear testing alarms if we aren't using them
|
||||||
|
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Evaluate Alarms --
|
||||||
|
|
||||||
|
-- containment breach is worst case CRITICAL alarm, this takes priority
|
||||||
|
if alarms[ALARM.ContainmentBreach] then
|
||||||
|
self.tone_states[TONE.T_1800Hz_Int_4Hz] = true
|
||||||
|
else
|
||||||
|
-- critical damage is highest priority CRITICAL level alarm
|
||||||
|
if alarms[ALARM.CriticalDamage] then
|
||||||
|
self.tone_states[TONE.T_660Hz_Int_125ms] = true
|
||||||
|
else
|
||||||
|
-- EMERGENCY level alarms + URGENT over temp
|
||||||
|
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
|
||||||
|
self.tone_states[TONE.T_544Hz_440Hz_Alt] = true
|
||||||
|
-- URGENT level turbine trip
|
||||||
|
elseif alarms[ALARM.TurbineTrip] then
|
||||||
|
self.tone_states[TONE.T_745Hz_Int_1Hz] = true
|
||||||
|
-- URGENT level reactor lost
|
||||||
|
elseif alarms[ALARM.ReactorLost] then
|
||||||
|
self.tone_states[TONE.T_340Hz_Int_2Hz] = true
|
||||||
|
-- TIMELY level alarms
|
||||||
|
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
||||||
|
self.tone_states[TONE.T_800Hz_Int] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- check RPS transient URGENT level alarm
|
||||||
|
if alarms[ALARM.RPSTransient] then
|
||||||
|
self.tone_states[TONE.T_1000Hz_Int] = true
|
||||||
|
-- disable really painful audio combination
|
||||||
|
self.tone_states[TONE.T_340Hz_Int_2Hz] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- radiation is a big concern, always play this CRITICAL level alarm if active
|
||||||
|
if alarms[ALARM.ContainmentRadiation] then
|
||||||
|
self.tone_states[TONE.T_800Hz_1000Hz_Alt] = true
|
||||||
|
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
|
||||||
|
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
|
||||||
|
if self.tone_states[TONE.T_1000Hz_Int] and alarms[ALARM.ReactorLost] then self.tone_states[TONE.T_340Hz_Int_2Hz] = true end
|
||||||
|
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
||||||
|
self.tone_states[TONE.T_745Hz_Int_1Hz] = false
|
||||||
|
self.tone_states[TONE.T_800Hz_Int] = false
|
||||||
|
self.tone_states[TONE.T_1000Hz_Int] = false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- add to tone states if testing is active
|
||||||
|
if allow_test then
|
||||||
|
for i = 1, #self.tone_states do
|
||||||
|
self.tone_states[i] = self.tone_states[i] or self.test_tone_states[i]
|
||||||
|
end
|
||||||
|
|
||||||
|
self.test_tone_reset = false
|
||||||
|
else
|
||||||
|
if not self.test_tone_reset then
|
||||||
|
-- clear testing tones if we aren't using them
|
||||||
|
for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- flag that tones were reset
|
||||||
|
self.test_tone_set = false
|
||||||
|
self.test_tone_reset = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- call the update function of all units in the facility<br>
|
-- call the update function of all units in the facility<br>
|
||||||
@ -891,8 +1006,52 @@ function facility.new(num_reactors, cooling_conf)
|
|||||||
return self.pu_fallback
|
return self.pu_fallback
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- DIAGNOSTIC TESTING --
|
||||||
|
|
||||||
|
-- attempt to set a test tone state
|
||||||
|
---@param id TONE|0 tone ID or 0 to disable all
|
||||||
|
---@param state boolean state
|
||||||
|
---@return boolean allow_testing, table test_tone_states
|
||||||
|
function public.diag_set_test_tone(id, state)
|
||||||
|
if self.allow_testing then
|
||||||
|
self.test_tone_set = true
|
||||||
|
self.test_tone_reset = false
|
||||||
|
|
||||||
|
if id == 0 then
|
||||||
|
for i = 1, #self.test_tone_states do self.test_tone_states[i] = false end
|
||||||
|
else
|
||||||
|
self.test_tone_states[id] = state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.allow_testing, self.test_tone_states
|
||||||
|
end
|
||||||
|
|
||||||
|
-- attempt to set a test alarm state
|
||||||
|
---@param id ALARM|0 alarm ID or 0 to disable all
|
||||||
|
---@param state boolean state
|
||||||
|
---@return boolean allow_testing, table test_alarm_states
|
||||||
|
function public.diag_set_test_alarm(id, state)
|
||||||
|
if self.allow_testing then
|
||||||
|
self.test_tone_set = true
|
||||||
|
self.test_tone_reset = false
|
||||||
|
|
||||||
|
if id == 0 then
|
||||||
|
for i = 1, #self.test_alarm_states do self.test_alarm_states[i] = false end
|
||||||
|
else
|
||||||
|
self.test_alarm_states[id] = state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.allow_testing, self.test_alarm_states
|
||||||
|
end
|
||||||
|
|
||||||
-- READ STATES/PROPERTIES --
|
-- READ STATES/PROPERTIES --
|
||||||
|
|
||||||
|
-- get current alarm tone on/off states
|
||||||
|
---@nodiscard
|
||||||
|
function public.get_alarm_tones() return self.tone_states end
|
||||||
|
|
||||||
-- get build properties of all facility devices
|
-- get build properties of all facility devices
|
||||||
---@nodiscard
|
---@nodiscard
|
||||||
---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil
|
---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil
|
||||||
|
@ -150,7 +150,8 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
|
|||||||
local function _send_fac_status()
|
local function _send_fac_status()
|
||||||
local status = {
|
local status = {
|
||||||
facility.get_control_status(),
|
facility.get_control_status(),
|
||||||
facility.get_rtu_statuses()
|
facility.get_rtu_statuses(),
|
||||||
|
facility.get_alarm_tones()
|
||||||
}
|
}
|
||||||
|
|
||||||
_send(SCADA_CRDN_TYPE.FAC_STATUS, status)
|
_send(SCADA_CRDN_TYPE.FAC_STATUS, status)
|
||||||
|
@ -33,8 +33,9 @@ local PERIODICS = {
|
|||||||
---@param in_queue mqueue in message queue
|
---@param in_queue mqueue in message queue
|
||||||
---@param out_queue mqueue out message queue
|
---@param out_queue mqueue out message queue
|
||||||
---@param timeout number communications timeout
|
---@param timeout number communications timeout
|
||||||
|
---@param facility facility facility data table
|
||||||
---@param fp_ok boolean if the front panel UI is running
|
---@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)
|
function pocket.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
|
-- 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 function println(message) if not fp_ok then util.println_ts(message) end end
|
||||||
|
|
||||||
@ -129,6 +130,55 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout, fp_ok)
|
|||||||
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||||
-- close the session
|
-- close the session
|
||||||
_close()
|
_close()
|
||||||
|
elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_GET then
|
||||||
|
-- get the state of alarm tones
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_GET, facility.get_alarm_tones())
|
||||||
|
elseif pkt.type == SCADA_MGMT_TYPE.DIAG_TONE_SET then
|
||||||
|
local valid = false
|
||||||
|
|
||||||
|
-- attempt to set a tone state
|
||||||
|
if pkt.scada_frame.is_authenticated() then
|
||||||
|
if pkt.length == 2 then
|
||||||
|
if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then
|
||||||
|
valid = true
|
||||||
|
|
||||||
|
-- try to set tone states, then send back if testing is allowed
|
||||||
|
local allow_testing, test_tone_states = facility.diag_set_test_tone(pkt.data[1], pkt.data[2])
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { allow_testing, test_tone_states })
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "SCADA diag tone set packet data type mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "SCADA diag tone set packet length mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "DIAG_TONE_SET is blocked without HMAC for security")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_TONE_SET, { false }) end
|
||||||
|
elseif pkt.type == SCADA_MGMT_TYPE.DIAG_ALARM_SET then
|
||||||
|
local valid = false
|
||||||
|
|
||||||
|
-- attempt to set an alarm state
|
||||||
|
if pkt.scada_frame.is_authenticated() then
|
||||||
|
if pkt.length == 2 then
|
||||||
|
if type(pkt.data[1]) == "number" and type(pkt.data[2]) == "boolean" then
|
||||||
|
valid = true
|
||||||
|
|
||||||
|
-- try to set alarm states, then send back if testing is allowed
|
||||||
|
local allow_testing, test_alarm_states = facility.diag_set_test_alarm(pkt.data[1], pkt.data[2])
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { allow_testing, test_alarm_states })
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "SCADA diag alarm set packet data type mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "SCADA diag alarm set packet length mismatch")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
log.debug(log_header .. "DIAG_ALARM_SET is blocked without HMAC for security")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not valid then _send_mgmt(SCADA_MGMT_TYPE.DIAG_ALARM_SET, { false }) end
|
||||||
else
|
else
|
||||||
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
|
||||||
end
|
end
|
||||||
|
@ -26,7 +26,8 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
|||||||
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
|
||||||
|
|
||||||
local PERIODICS = {
|
local PERIODICS = {
|
||||||
KEEP_ALIVE = 2000
|
KEEP_ALIVE = 2000,
|
||||||
|
ALARM_TONES = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
-- create a new RTU session
|
-- create a new RTU session
|
||||||
@ -58,7 +59,8 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
|||||||
-- periodic messages
|
-- periodic messages
|
||||||
periodics = {
|
periodics = {
|
||||||
last_update = 0,
|
last_update = 0,
|
||||||
keep_alive = 0
|
keep_alive = 0,
|
||||||
|
alarm_tones = 0
|
||||||
},
|
},
|
||||||
units = {}
|
units = {}
|
||||||
}
|
}
|
||||||
@ -389,6 +391,14 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
|
|||||||
periodics.keep_alive = 0
|
periodics.keep_alive = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- alarm tones
|
||||||
|
|
||||||
|
periodics.alarm_tones = periodics.alarm_tones + elapsed
|
||||||
|
if periodics.alarm_tones >= PERIODICS.ALARM_TONES then
|
||||||
|
_send_mgmt(SCADA_MGMT_TYPE.RTU_TONE_ALARM, { facility.get_alarm_tones() })
|
||||||
|
periodics.alarm_tones = 0
|
||||||
|
end
|
||||||
|
|
||||||
self.periodics.last_update = util.time()
|
self.periodics.last_update = util.time()
|
||||||
|
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
|
@ -104,8 +104,8 @@ local function _sv_handle_outq(session)
|
|||||||
|
|
||||||
-- max 100ms spent processing queue
|
-- max 100ms spent processing queue
|
||||||
if util.time() - handle_start > 100 then
|
if util.time() - handle_start > 100 then
|
||||||
log.warning("[SVS] supervisor out queue handler exceeded 100ms queue process limit")
|
log.debug("[SVS] supervisor out queue handler exceeded 100ms queue process limit")
|
||||||
log.warning(util.c("[SVS] offending session: ", session))
|
log.debug(util.c("[SVS] offending session: ", session))
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -430,7 +430,8 @@ function svsessions.establish_pdg_session(source_addr, version)
|
|||||||
|
|
||||||
local id = self.next_ids.pdg
|
local id = self.next_ids.pdg
|
||||||
|
|
||||||
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.fp_ok)
|
pdg_s.instance = pocket.new_session(id, source_addr, pdg_s.in_queue, pdg_s.out_queue, config.PKT_TIMEOUT, self.facility,
|
||||||
|
self.fp_ok)
|
||||||
table.insert(self.sessions.pdg, pdg_s)
|
table.insert(self.sessions.pdg, pdg_s)
|
||||||
|
|
||||||
local mt = {
|
local mt = {
|
||||||
|
@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
|
|||||||
|
|
||||||
local svsessions = require("supervisor.session.svsessions")
|
local svsessions = require("supervisor.session.svsessions")
|
||||||
|
|
||||||
local SUPERVISOR_VERSION = "v0.20.4"
|
local SUPERVISOR_VERSION = "v0.22.1"
|
||||||
|
|
||||||
local println = util.println
|
local println = util.println
|
||||||
local println_ts = util.println_ts
|
local println_ts = util.println_ts
|
||||||
|
@ -719,6 +719,23 @@ function unit.new(reactor_id, num_boilers, num_turbines)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- check if the reactor is connected, is stopped, the RPS is not tripped, and no alarms are active
|
||||||
|
---@nodiscard
|
||||||
|
function public.is_safe_idle()
|
||||||
|
-- can't be disconnected
|
||||||
|
if self.plc_i == nil then return false end
|
||||||
|
|
||||||
|
-- reactor must be stopped and RPS can't be tripped
|
||||||
|
if self.plc_i.get_status().status or self.plc_i.get_db().rps_tripped then return false end
|
||||||
|
|
||||||
|
-- alarms must be inactive and not tripping
|
||||||
|
for _, alarm in pairs(self.alarms) do
|
||||||
|
if not (alarm.state == AISTATE.INACTIVE or alarm.state == AISTATE.RING_BACK) then return false end
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
-- get build properties of machines
|
-- get build properties of machines
|
||||||
--
|
--
|
||||||
-- filter options
|
-- filter options
|
||||||
|
Loading…
Reference in New Issue
Block a user