diff --git a/ccmsi.lua b/ccmsi.lua
index 36a74c5..cc986ba 100644
--- a/ccmsi.lua
+++ b/ccmsi.lua
@@ -20,7 +20,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end
-local CCMSI_VERSION = "v1.7d"
+local CCMSI_VERSION = "v1.8"
local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
@@ -63,16 +63,15 @@ end
local function pkg_message(message, package) white();print(message .. " ");blue();println(package);white() end
-- indicate actions to be taken based on package differences for installs/updates
-local function show_pkg_change(name, v_local, v_remote)
- if v_local ~= nil then
- if v_local ~= v_remote then
- print("[" .. name .. "] updating ");blue();print(v_local);white();print(" \xbb ");blue();println(v_remote);white()
+local function show_pkg_change(name, v)
+ if v.v_local ~= nil then
+ if v.v_local ~= v.v_remote then
+ print("[" .. name .. "] updating ");blue();print(v.v_local);white();print(" \xbb ");blue();println(v.v_remote);white()
elseif mode == "install" then
- pkg_message("[" .. name .. "] reinstalling", v_local)
+ pkg_message("[" .. name .. "] reinstalling", v.v_local)
end
- else
- pkg_message("[" .. name .. "] new install of", v_remote)
- end
+ else pkg_message("[" .. name .. "] new install of", v.v_remote) end
+ return v.v_local ~= v.v_remote
end
-- read the local manifest file
@@ -284,6 +283,7 @@ elseif mode == "install" or mode == "update" then
app = { v_local = nil, v_remote = nil, changed = false },
boot = { v_local = nil, v_remote = nil, changed = false },
comms = { v_local = nil, v_remote = nil, changed = false },
+ common = { v_local = nil, v_remote = nil, changed = false },
graphics = { v_local = nil, v_remote = nil, changed = false },
lockbox = { v_local = nil, v_remote = nil, changed = false }
}
@@ -299,6 +299,7 @@ elseif mode == "install" or mode == "update" then
ver.boot.v_local = local_manifest.versions.bootloader
ver.app.v_local = local_manifest.versions[app]
ver.comms.v_local = local_manifest.versions.comms
+ ver.common.v_local = local_manifest.versions.common
ver.graphics.v_local = local_manifest.versions.graphics
ver.lockbox.v_local = local_manifest.versions.lockbox
@@ -316,6 +317,7 @@ elseif mode == "install" or mode == "update" then
ver.boot.v_remote = manifest.versions.bootloader
ver.app.v_remote = manifest.versions[app]
ver.comms.v_remote = manifest.versions.comms
+ ver.common.v_remote = manifest.versions.common
ver.graphics.v_remote = manifest.versions.graphics
ver.lockbox.v_remote = manifest.versions.lockbox
@@ -327,28 +329,15 @@ elseif mode == "install" or mode == "update" then
end
white()
- -- display bootloader version change information
- show_pkg_change("bootldr", ver.boot.v_local, ver.boot.v_remote)
- ver.boot.changed = ver.boot.v_local ~= ver.boot.v_remote
-
- -- display app version change information
- show_pkg_change(app, ver.app.v_local, ver.app.v_remote)
- ver.app.changed = ver.app.v_local ~= ver.app.v_remote
-
- -- display comms version change information
- show_pkg_change("comms", ver.comms.v_local, ver.comms.v_remote)
- ver.comms.changed = ver.comms.v_local ~= ver.comms.v_remote
+ ver.boot.changed = show_pkg_change("bootldr", ver.boot)
+ ver.common.changed = show_pkg_change("common", ver.common)
+ ver.comms.changed = show_pkg_change("comms", ver.comms)
if ver.comms.changed and ver.comms.v_local ~= nil then
print("[comms] ");yellow();println("other devices on the network will require an update");white()
end
-
- -- display graphics version change information
- show_pkg_change("graphics", ver.graphics.v_local, ver.graphics.v_remote)
- ver.graphics.changed = ver.graphics.v_local ~= ver.graphics.v_remote
-
- -- display lockbox version change information
- show_pkg_change("lockbox", ver.lockbox.v_local, ver.lockbox.v_remote)
- ver.lockbox.changed = ver.lockbox.v_local ~= ver.lockbox.v_remote
+ ver.app.changed = show_pkg_change(app, ver.app)
+ ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
+ ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
-- ask for confirmation
if not ask_y_n("Continue", false) then return end
@@ -392,16 +381,13 @@ elseif mode == "install" or mode == "update" then
if dependency == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed
elseif dependency == "lockbox" then return not ver.lockbox.changed
- elseif dependency == "common" then return not (ver.app.changed or ver.comms.changed)
+ elseif dependency == "common" then return not (ver.common.changed or ver.comms.changed)
elseif dependency == app then return not ver.app.changed
else return true end
end
if not single_file_mode then
- if fs.exists(install_dir) then
- fs.delete(install_dir)
- fs.makeDir(install_dir)
- end
+ if fs.exists(install_dir) then fs.delete(install_dir);fs.makeDir(install_dir) end
-- download all dependencies
for _, dependency in pairs(dependencies) do
diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua
index 577b8e6..4149500 100644
--- a/coordinator/iocontrol.lua
+++ b/coordinator/iocontrol.lua
@@ -84,6 +84,8 @@ function iocontrol.init(conf, comms)
scram_ack = __generic_ack,
ack_alarms_ack = __generic_ack,
+ alarm_tones = { false, false, false, false, false, false, false, false },
+
ps = psil.create(),
induction_ps_tbl = {},
@@ -780,6 +782,16 @@ function iocontrol.update_facility_status(status)
end
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
return valid
@@ -1199,9 +1211,6 @@ function iocontrol.update_unit_statuses(statuses)
io.facility.ps.publish("po_pl_rate", po_pl_rate)
io.facility.ps.publish("po_am_rate", po_am_rate)
io.facility.ps.publish("spent_waste_rate", spent_rate)
-
- -- update alarm sounder
- sounder.eval(io.units)
end
return valid
diff --git a/coordinator/sounder.lua b/coordinator/sounder.lua
index 86fb9b4..e3b2051 100644
--- a/coordinator/sounder.lua
+++ b/coordinator/sounder.lua
@@ -2,269 +2,25 @@
-- Alarm Sounder
--
+local audio = require("scada-common.audio")
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
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 = {
speaker = nil,
volume = 0.5,
- playing = false,
- num_active = 0,
- next_block = 1,
- -- split audio up into 0.5s samples so specific components can be ended quicker
- quad_buffer = { {}, {}, {}, {} }
+ stream = audio.new_stream()
}
--- 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
---@return boolean success successfully added buffer to audio output
local function play()
if not alarm_ctl.playing then
alarm_ctl.playing = true
- alarm_ctl.next_block = 1
-
return sounder.continue()
- else
- return true
- end
+ else return true end
end
-- initialize the annunciator alarm system
@@ -273,23 +29,10 @@ end
function sounder.init(speaker, volume)
alarm_ctl.speaker = speaker
alarm_ctl.speaker.stop()
-
alarm_ctl.volume = volume
- alarm_ctl.playing = false
- alarm_ctl.num_active = 0
- alarm_ctl.next_block = 1
+ alarm_ctl.stream.stop()
- zero()
-
- -- 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()
+ audio.generate_tones()
end
-- reconnect the speaker peripheral
@@ -297,173 +40,40 @@ end
function sounder.reconnect(speaker)
alarm_ctl.speaker = speaker
alarm_ctl.playing = false
- alarm_ctl.next_block = 1
- alarm_ctl.num_active = 0
- for id = 1, #TONES do TONES[id].active = false end
+ alarm_ctl.stream.stop()
end
--- check alarm state to enable/disable alarms
----@param units table|nil unit list or nil to use test mode
-function sounder.eval(units)
- local changed = false
- local any_active = false
- 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 }
+-- set alarm tones
+---@param states table alarm tone commands from supervisor
+function sounder.set(states)
+ -- set tone states
+ for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
- if units ~= nil then
- -- check all alarms for all units
- for i = 1, #units do
- 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
+ -- re-compute output if needed, then play audio if available
+ if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
+ if alarm_ctl.stream.has_next_block() then play() else sounder.stop() end
end
-- stop all audio and clear output buffer
function sounder.stop()
alarm_ctl.playing = false
alarm_ctl.speaker.stop()
- alarm_ctl.next_block = 1
- alarm_ctl.num_active = 0
- for id = 1, #TONES do TONES[id].active = false end
- zero()
+ alarm_ctl.stream.stop()
end
-- continue audio on buffer empty
---@return boolean success successfully added buffer to audio output
function sounder.continue()
+ local success = false
+
if alarm_ctl.playing then
- if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then
- local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume)
-
- alarm_ctl.next_block = alarm_ctl.next_block + 1
- if alarm_ctl.next_block > 4 then alarm_ctl.next_block = 1 end
-
- if not success then
- log.debug("SOUNDER: error playing audio")
- end
-
- return success
- else
- return false
- 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
+ if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
+ 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
end
end
- log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
+ return success
end
---#endregion
-
return sounder
diff --git a/graphics/core.lua b/graphics/core.lua
index 5abc28d..4e56714 100644
--- a/graphics/core.lua
+++ b/graphics/core.lua
@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
-core.version = "1.0.3"
+core.version = "1.1.1"
core.flasher = flasher
core.events = events
diff --git a/graphics/element.lua b/graphics/element.lua
index 145ee0c..5bfb7cd 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -20,6 +20,7 @@ local element = {}
---@alias graphics_args graphics_args_generic
---|waiting_args
+---|app_button_args
---|checkbox_args
---|hazard_button_args
---|multi_button_args
@@ -515,19 +516,21 @@ function element.new(args, child_offset_x, child_offset_y)
-- 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
function public.handle_mouse(event)
- local x_ini, y_ini = event.initial.x, event.initial.y
+ if protected.window.isVisible() then
+ 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)
- if ini_in then
- local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y)
+ if ini_in then
+ local event_T = core.events.mouse_transposed(event, self.position.x, self.position.y)
- -- handle the mouse event then pass to children
- protected.handle_mouse(event_T)
- for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
+ -- handle the mouse event then pass to children
+ protected.handle_mouse(event_T)
+ for _, child in pairs(protected.children) do child.handle_mouse(event_T) end
+ end
end
end
diff --git a/graphics/elements/controls/app.lua b/graphics/elements/controls/app.lua
new file mode 100644
index 0000000..226946c
--- /dev/null
+++ b/graphics/elements/controls/app.lua
@@ -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
diff --git a/imgen.py b/imgen.py
index 046fc36..8fc7989 100644
--- a/imgen.py
+++ b/imgen.py
@@ -48,6 +48,7 @@ def make_manifest(size):
"versions" : {
"installer" : get_version("./ccmsi.lua"),
"bootloader" : get_version("./startup.lua"),
+ "common" : get_version("./scada-common/util.lua", True),
"comms" : get_version("./scada-common/comms.lua", True),
"graphics" : get_version("./graphics/core.lua", True),
"lockbox" : get_version("./lockbox/init.lua", True),
diff --git a/install_manifest.json b/install_manifest.json
index 7b7cc96..51fe9f3 100644
--- a/install_manifest.json
+++ b/install_manifest.json
@@ -1 +1 @@
-{"versions": {"installer": "v1.7d", "bootloader": "0.2", "comms": "2.1.2", "graphics": "1.0.1", "lockbox": "1.0", "reactor-plc": "v1.5.5", "rtu": "v1.5.5", "supervisor": "v0.20.3", "coordinator": "v0.21.1", "pocket": "alpha-v0.5.2"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/tcd.lua", "scada-common/crash.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua", "scada-common/network.lua"], "graphics": ["graphics/element.lua", "graphics/events.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/listbox.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/multipane.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/controls/tabbar.lua", "graphics/elements/controls/checkbox.lua", "graphics/elements/controls/sidebar.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/digest/md5.lua", "lockbox/mac/hmac.lua"], "reactor-plc": ["reactor-plc/renderer.lua", "reactor-plc/threads.lua", "reactor-plc/databus.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua", "reactor-plc/panel/front_panel.lua", "reactor-plc/panel/style.lua"], "rtu": ["rtu/renderer.lua", "rtu/threads.lua", "rtu/rtu.lua", "rtu/databus.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/panel/front_panel.lua", "rtu/panel/style.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/dynamicv_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/renderer.lua", "supervisor/databus.lua", "supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/panel/pgi.lua", "supervisor/panel/front_panel.lua", "supervisor/panel/style.lua", "supervisor/panel/components/rtu_entry.lua", "supervisor/panel/components/pdg_entry.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/pocket.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/dynamicv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/pgi.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/layout/front_panel.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/pkt_entry.lua", "coordinator/ui/components/process_ctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/turbine.lua", "coordinator/session/pocket.lua", "coordinator/session/apisessions.lua"], "pocket": ["pocket/pocket.lua", "pocket/renderer.lua", "pocket/config.lua", "pocket/coreio.lua", "pocket/startup.lua", "pocket/ui/main.lua", "pocket/ui/style.lua", "pocket/ui/components/conn_waiting.lua", "pocket/ui/pages/turbine_page.lua", "pocket/ui/pages/reactor_page.lua", "pocket/ui/pages/home_page.lua", "pocket/ui/pages/unit_page.lua", "pocket/ui/pages/boiler_page.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics", "lockbox"], "rtu": ["system", "common", "graphics", "lockbox"], "supervisor": ["system", "common", "graphics", "lockbox"], "coordinator": ["system", "common", "graphics", "lockbox"], "pocket": ["system", "common", "graphics", "lockbox"]}, "sizes": {"manifest": 5789, "system": 1991, "common": 98474, "graphics": 147714, "lockbox": 34900, "reactor-plc": 95833, "rtu": 105774, "supervisor": 335453, "coordinator": 232175, "pocket": 37639}}
\ No newline at end of file
+{"versions": {"installer": "v1.8", "bootloader": "0.2", "common": "1.0.1", "comms": "2.2.1", "graphics": "1.1.0", "lockbox": "1.0", "reactor-plc": "v1.5.5", "rtu": "v1.6.0", "supervisor": "v0.22.1", "coordinator": "v0.22.0", "pocket": "v0.6.0-alpha"}, "files": {"system": ["initenv.lua", "startup.lua"], "common": ["scada-common/ppm.lua", "scada-common/comms.lua", "scada-common/psil.lua", "scada-common/rsio.lua", "scada-common/constants.lua", "scada-common/mqueue.lua", "scada-common/tcd.lua", "scada-common/crash.lua", "scada-common/audio.lua", "scada-common/log.lua", "scada-common/types.lua", "scada-common/util.lua", "scada-common/network.lua"], "graphics": ["graphics/element.lua", "graphics/events.lua", "graphics/flasher.lua", "graphics/core.lua", "graphics/elements/listbox.lua", "graphics/elements/textbox.lua", "graphics/elements/displaybox.lua", "graphics/elements/pipenet.lua", "graphics/elements/rectangle.lua", "graphics/elements/div.lua", "graphics/elements/multipane.lua", "graphics/elements/tiling.lua", "graphics/elements/colormap.lua", "graphics/elements/indicators/alight.lua", "graphics/elements/indicators/icon.lua", "graphics/elements/indicators/power.lua", "graphics/elements/indicators/rad.lua", "graphics/elements/indicators/state.lua", "graphics/elements/indicators/light.lua", "graphics/elements/indicators/vbar.lua", "graphics/elements/indicators/led.lua", "graphics/elements/indicators/coremap.lua", "graphics/elements/indicators/data.lua", "graphics/elements/indicators/ledpair.lua", "graphics/elements/indicators/hbar.lua", "graphics/elements/indicators/trilight.lua", "graphics/elements/indicators/ledrgb.lua", "graphics/elements/controls/switch_button.lua", "graphics/elements/controls/spinbox_numeric.lua", "graphics/elements/controls/hazard_button.lua", "graphics/elements/controls/push_button.lua", "graphics/elements/controls/app.lua", "graphics/elements/controls/radio_button.lua", "graphics/elements/controls/multi_button.lua", "graphics/elements/controls/tabbar.lua", "graphics/elements/controls/checkbox.lua", "graphics/elements/controls/sidebar.lua", "graphics/elements/animations/waiting.lua"], "lockbox": ["lockbox/init.lua", "lockbox/LICENSE", "lockbox/kdf/pbkdf2.lua", "lockbox/util/bit.lua", "lockbox/util/array.lua", "lockbox/util/stream.lua", "lockbox/util/queue.lua", "lockbox/digest/sha2_224.lua", "lockbox/digest/sha1.lua", "lockbox/digest/sha2_256.lua", "lockbox/digest/md5.lua", "lockbox/mac/hmac.lua"], "reactor-plc": ["reactor-plc/renderer.lua", "reactor-plc/threads.lua", "reactor-plc/databus.lua", "reactor-plc/plc.lua", "reactor-plc/config.lua", "reactor-plc/startup.lua", "reactor-plc/panel/front_panel.lua", "reactor-plc/panel/style.lua"], "rtu": ["rtu/renderer.lua", "rtu/threads.lua", "rtu/rtu.lua", "rtu/databus.lua", "rtu/modbus.lua", "rtu/config.lua", "rtu/startup.lua", "rtu/panel/front_panel.lua", "rtu/panel/style.lua", "rtu/dev/sps_rtu.lua", "rtu/dev/envd_rtu.lua", "rtu/dev/boilerv_rtu.lua", "rtu/dev/redstone_rtu.lua", "rtu/dev/sna_rtu.lua", "rtu/dev/imatrix_rtu.lua", "rtu/dev/dynamicv_rtu.lua", "rtu/dev/turbinev_rtu.lua"], "supervisor": ["supervisor/renderer.lua", "supervisor/databus.lua", "supervisor/supervisor.lua", "supervisor/unit.lua", "supervisor/config.lua", "supervisor/startup.lua", "supervisor/unitlogic.lua", "supervisor/facility.lua", "supervisor/panel/pgi.lua", "supervisor/panel/front_panel.lua", "supervisor/panel/style.lua", "supervisor/panel/components/rtu_entry.lua", "supervisor/panel/components/pdg_entry.lua", "supervisor/session/coordinator.lua", "supervisor/session/svqtypes.lua", "supervisor/session/pocket.lua", "supervisor/session/svsessions.lua", "supervisor/session/rtu.lua", "supervisor/session/plc.lua", "supervisor/session/rsctl.lua", "supervisor/session/rtu/boilerv.lua", "supervisor/session/rtu/dynamicv.lua", "supervisor/session/rtu/txnctrl.lua", "supervisor/session/rtu/unit_session.lua", "supervisor/session/rtu/turbinev.lua", "supervisor/session/rtu/envd.lua", "supervisor/session/rtu/imatrix.lua", "supervisor/session/rtu/sps.lua", "supervisor/session/rtu/qtypes.lua", "supervisor/session/rtu/sna.lua", "supervisor/session/rtu/redstone.lua"], "coordinator": ["coordinator/coordinator.lua", "coordinator/renderer.lua", "coordinator/iocontrol.lua", "coordinator/sounder.lua", "coordinator/config.lua", "coordinator/startup.lua", "coordinator/process.lua", "coordinator/ui/dialog.lua", "coordinator/ui/pgi.lua", "coordinator/ui/style.lua", "coordinator/ui/layout/main_view.lua", "coordinator/ui/layout/unit_view.lua", "coordinator/ui/layout/front_panel.lua", "coordinator/ui/components/reactor.lua", "coordinator/ui/components/pkt_entry.lua", "coordinator/ui/components/process_ctl.lua", "coordinator/ui/components/unit_overview.lua", "coordinator/ui/components/boiler.lua", "coordinator/ui/components/unit_detail.lua", "coordinator/ui/components/imatrix.lua", "coordinator/ui/components/turbine.lua", "coordinator/session/pocket.lua", "coordinator/session/apisessions.lua"], "pocket": ["pocket/pocket.lua", "pocket/renderer.lua", "pocket/iocontrol.lua", "pocket/config.lua", "pocket/startup.lua", "pocket/ui/main.lua", "pocket/ui/style.lua", "pocket/ui/components/conn_waiting.lua", "pocket/ui/pages/turbine_page.lua", "pocket/ui/pages/reactor_page.lua", "pocket/ui/pages/diag_page.lua", "pocket/ui/pages/home_page.lua", "pocket/ui/pages/unit_page.lua", "pocket/ui/pages/boiler_page.lua"]}, "depends": {"reactor-plc": ["system", "common", "graphics", "lockbox"], "rtu": ["system", "common", "graphics", "lockbox"], "supervisor": ["system", "common", "graphics", "lockbox"], "coordinator": ["system", "common", "graphics", "lockbox"], "pocket": ["system", "common", "graphics", "lockbox"]}, "sizes": {"manifest": 5908, "system": 1991, "common": 108345, "graphics": 152364, "lockbox": 34900, "reactor-plc": 95833, "rtu": 111126, "supervisor": 345892, "coordinator": 219352, "pocket": 53069}}
\ No newline at end of file
diff --git a/pocket/coreio.lua b/pocket/coreio.lua
deleted file mode 100644
index 6f43dfd..0000000
--- a/pocket/coreio.lua
+++ /dev/null
@@ -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
diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua
new file mode 100644
index 0000000..30645ae
--- /dev/null
+++ b/pocket/iocontrol.lua
@@ -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
diff --git a/pocket/pocket.lua b/pocket/pocket.lua
index b0432cf..65b286a 100644
--- a/pocket/pocket.lua
+++ b/pocket/pocket.lua
@@ -1,16 +1,15 @@
-local comms = require("scada-common.comms")
-local log = require("scada-common.log")
-local util = require("scada-common.util")
+local comms = require("scada-common.comms")
+local log = require("scada-common.log")
+local util = require("scada-common.util")
-local coreio = require("pocket.coreio")
+local iocontrol = require("pocket.iocontrol")
local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
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 = {}
@@ -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
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
local function _send_sv_establish()
_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
function public.link_update()
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
_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
end
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
_send_api_establish()
@@ -166,10 +151,29 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
end
else
-- linked, all good!
- coreio.report_link_state(LINK_STATE.LINKED)
+ iocontrol.report_link_state(LINK_STATE.LINKED)
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
---@param side string
---@param sender integer
@@ -205,6 +209,8 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
-- handle a packet
---@param packet mgmt_frame|capi_frame|nil
function public.handle_packet(packet)
+ local diag = iocontrol.get_db().diag
+
if packet ~= nil then
local l_chan = packet.scada_frame.local_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
api_watchdog.feed()
- if protocol == PROTOCOL.COORD_API then
- ---@cast packet capi_frame
- elseif protocol == PROTOCOL.SCADA_MGMT then
+ if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
- if 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
- 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 self.api.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
@@ -298,6 +266,42 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
else
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from coordinator")
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
log.debug("discarding coordinator non-link SCADA_MGMT packet before linked")
end
@@ -325,43 +329,7 @@ function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range
-- handle packet
if protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
- if 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
- 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 self.sv.linked then
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
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.addr = comms.BROADCAST
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
log.debug("received unknown SCADA_MGMT packet type " .. packet.type .. " from supervisor")
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
log.debug("discarding supervisor non-link SCADA_MGMT packet before linked")
end
diff --git a/pocket/startup.lua b/pocket/startup.lua
index f8403c1..14ece9e 100644
--- a/pocket/startup.lua
+++ b/pocket/startup.lua
@@ -4,21 +4,21 @@
require("/initenv").init_env()
-local crash = require("scada-common.crash")
-local log = require("scada-common.log")
-local network = require("scada-common.network")
-local ppm = require("scada-common.ppm")
-local tcd = require("scada-common.tcd")
-local util = require("scada-common.util")
+local crash = require("scada-common.crash")
+local log = require("scada-common.log")
+local network = require("scada-common.network")
+local ppm = require("scada-common.ppm")
+local tcd = require("scada-common.tcd")
+local util = require("scada-common.util")
-local core = require("graphics.core")
+local core = require("graphics.core")
-local config = require("pocket.config")
-local coreio = require("pocket.coreio")
-local pocket = require("pocket.pocket")
-local renderer = require("pocket.renderer")
+local config = require("pocket.config")
+local iocontrol = require("pocket.iocontrol")
+local pocket = require("pocket.pocket")
+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_ts = util.println_ts
@@ -73,7 +73,7 @@ local function main()
network.init_mac(config.AUTH_KEY)
end
- coreio.report_link_state(coreio.LINK_STATE.UNLINKED)
+ iocontrol.report_link_state(iocontrol.LINK_STATE.UNLINKED)
-- get the communications modem
local modem = ppm.get_wireless_modem()
@@ -104,6 +104,9 @@ local function main()
local MAIN_CLOCK = 0.5
local loop_clock = util.new_clock(MAIN_CLOCK)
+ -- init I/O control
+ iocontrol.init_core(pocket_comms)
+
----------------------------------------
-- start the UI
----------------------------------------
@@ -128,6 +131,9 @@ local function main()
conn_wd.api.feed()
log.debug("startup> conn watchdog started")
+ local io_db = iocontrol.get_db()
+ local nav = io_db.nav
+
-- main event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -140,6 +146,13 @@ local function main()
-- relink if necessary
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()
elseif conn_wd.sv.is_timer(param1) then
-- supervisor watchdog timeout
diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua
index 11331e4..212b5bc 100644
--- a/pocket/ui/main.lua
+++ b/pocket/ui/main.lua
@@ -2,17 +2,18 @@
-- Pocket GUI Root
--
-local coreio = require("pocket.coreio")
+local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
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 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 unit_page = require("pocket.ui.pages.unit_page")
local core = require("graphics.core")
@@ -22,6 +23,9 @@ local TextBox = require("graphics.elements.textbox")
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 cpair = core.cpair
@@ -29,6 +33,9 @@ local cpair = core.cpair
-- create new main view
---@param main graphics_element main displaybox
local function init(main)
+ local nav = iocontrol.get_db().nav
+ local ps = iocontrol.get_db().ps
+
-- window header message
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}
- root_pane.register(coreio.core_ps(), "link_state", function (state)
- if state == coreio.LINK_STATE.UNLINKED or state == coreio.LINK_STATE.API_LINK_ONLY then
+ root_pane.register(ps, "link_state", function (state)
+ if state == LINK_STATE.UNLINKED or state == LINK_STATE.API_LINK_ONLY then
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)
else
root_pane.set_value(3)
@@ -81,19 +88,36 @@ local function init(main)
{
char = "T",
color = cpair(colors.black,colors.white)
+ },
+ {
+ char = "D",
+ color = cpair(colors.black,colors.orange)
}
}
- local pane_1 = home_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 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 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
return init
diff --git a/pocket/ui/pages/diag_page.lua b/pocket/ui/pages/diag_page.lua
new file mode 100644
index 0000000..38d9368
--- /dev/null
+++ b/pocket/ui/pages/diag_page.lua
@@ -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
diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua
index a31cae8..d192796 100644
--- a/pocket/ui/pages/home_page.lua
+++ b/pocket/ui/pages/home_page.lua
@@ -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 App = require("graphics.elements.controls.app")
--- local cpair = core.cpair
-
-local TEXT_ALIGN = core.TEXT_ALIGN
+local cpair = core.cpair
-- new home page view
---@param root graphics_element parent
local function new_view(root)
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
end
diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua
index 0c8022f..66435d9 100644
--- a/reactor-plc/startup.lua
+++ b/reactor-plc/startup.lua
@@ -19,7 +19,7 @@ local plc = require("reactor-plc.plc")
local renderer = require("reactor-plc.renderer")
local threads = require("reactor-plc.threads")
-local R_PLC_VERSION = "v1.5.5"
+local R_PLC_VERSION = "v1.5.6"
local println = util.println
local println_ts = util.println_ts
diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua
index 10634ef..12ae75a 100644
--- a/reactor-plc/threads.lua
+++ b/reactor-plc/threads.lua
@@ -165,7 +165,7 @@ function threads.thread__main(smem, init)
local type, device = ppm.handle_unmount(param1)
if type ~= nil and device ~= nil then
- if type == "fissionReactorLogicAdapter" then
+ if device == plc_dev.reactor then
println_ts("reactor disconnected!")
log.error("reactor logic adapter disconnected")
@@ -205,7 +205,7 @@ function threads.thread__main(smem, init)
local type, device = ppm.mount(param1)
if type ~= nil and device ~= nil then
- if type == "fissionReactorLogicAdapter" then
+ if plc_state.no_reactor and (type == "fissionReactorLogicAdapter") then
-- reconnected reactor
plc_dev.reactor = device
plc_state.no_reactor = false
diff --git a/rtu/config.lua b/rtu/config.lua
index a0d3e68..98ca5df 100644
--- a/rtu/config.lua
+++ b/rtu/config.lua
@@ -15,6 +15,10 @@ config.COMMS_TIMEOUT = 5
-- all devices on the same network must use the same key
-- config.AUTH_KEY = "SCADAfacility123"
+-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
+-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
+config.SOUNDER_VOLUME = 1.0
+
-- log path
config.LOG_PATH = "/log.txt"
-- log mode
diff --git a/rtu/databus.lua b/rtu/databus.lua
index 3014367..4fe183a 100644
--- a/rtu/databus.lua
+++ b/rtu/databus.lua
@@ -37,6 +37,12 @@ function databus.tx_hw_modem(has_modem)
databus.ps.publish("has_modem", has_modem)
end
+-- transmit the number of speakers connected
+---@param count integer
+function databus.tx_hw_spkr_count(count)
+ databus.ps.publish("speaker_count", count)
+end
+
-- transmit unit hardware type across the bus
---@param uid integer unit ID
---@param type RTU_UNIT_TYPE
diff --git a/rtu/panel/front_panel.lua b/rtu/panel/front_panel.lua
index dc0dd44..ad6f7a0 100644
--- a/rtu/panel/front_panel.lua
+++ b/rtu/panel/front_panel.lua
@@ -2,36 +2,27 @@
-- RTU Front Panel GUI
--
-local types = require("scada-common.types")
-local util = require("scada-common.util")
+local types = require("scada-common.types")
+local util = require("scada-common.util")
-local databus = require("rtu.databus")
+local databus = require("rtu.databus")
-local style = require("rtu.panel.style")
+local style = require("rtu.panel.style")
-local core = require("graphics.core")
+local core = require("graphics.core")
-local Div = require("graphics.elements.div")
-local TextBox = require("graphics.elements.textbox")
+local Div = require("graphics.elements.div")
+local TextBox = require("graphics.elements.textbox")
-local LED = require("graphics.elements.indicators.led")
-local RGBLED = require("graphics.elements.indicators.ledrgb")
+local DataIndicator = require("graphics.elements.indicators.data")
+local LED = require("graphics.elements.indicators.led")
+local RGBLED = require("graphics.elements.indicators.ledrgb")
local TEXT_ALIGN = core.TEXT_ALIGN
local cpair = core.cpair
-local UNIT_TYPE_LABELS = {
- "UNKNOWN",
- "REDSTONE",
- "BOILER",
- "TURBINE",
- "DYNAMIC TANK",
- "IND MATRIX",
- "SPS",
- "SNA",
- "ENV DETECTOR"
-}
+local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC TANK", "IND MATRIX", "SPS", "SNA", "ENV DETECTOR" }
-- create new front panel view
@@ -72,6 +63,10 @@ local function init(panel, units)
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=cpair(colors.lightGray,colors.ivory)}
+ TextBox{parent=system,x=1,y=14,text="SPEAKERS",height=1,width=8,fg_bg=style.label}
+ local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=cpair(colors.gray,colors.white)}
+ speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
+
--
-- about label
--
diff --git a/rtu/rtu.lua b/rtu/rtu.lua
index 3832986..4c92afc 100644
--- a/rtu/rtu.lua
+++ b/rtu/rtu.lua
@@ -1,9 +1,11 @@
+local audio = require("scada-common.audio")
local comms = require("scada-common.comms")
local ppm = require("scada-common.ppm")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
+local config = require("rtu.config")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
@@ -155,6 +157,48 @@ function rtu.init_unit()
return protected
end
+-- create an alarm speaker sounder
+---@param speaker table device peripheral
+function rtu.init_sounder(speaker)
+ ---@class rtu_speaker_sounder
+ local spkr_ctl = {
+ speaker = speaker,
+ name = ppm.get_iface(speaker),
+ playing = false,
+ stream = audio.new_stream(),
+ play = function () end,
+ stop = function () end,
+ continue = function () end
+ }
+
+ -- continue audio stream if playing
+ function spkr_ctl.continue()
+ if spkr_ctl.playing then
+ if spkr_ctl.speaker ~= nil and spkr_ctl.stream.has_next_block() then
+ local success = spkr_ctl.speaker.playAudio(spkr_ctl.stream.get_next_block(), config.SOUNDER_VOLUME)
+ if not success then log.error(util.c("rtu_sounder(", spkr_ctl.name, "): error playing audio")) end
+ end
+ end
+ end
+
+ -- start audio stream playback
+ function spkr_ctl.play()
+ if not spkr_ctl.playing then
+ spkr_ctl.playing = true
+ return spkr_ctl.continue()
+ end
+ end
+
+ -- stop audio stream playback
+ function spkr_ctl.stop()
+ spkr_ctl.playing = false
+ spkr_ctl.speaker.stop()
+ spkr_ctl.stream.stop()
+ end
+
+ return spkr_ctl
+end
+
-- RTU Communications
---@nodiscard
---@param version string RTU version
@@ -312,7 +356,8 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
---@param packet modbus_frame|mgmt_frame
---@param units table RTU units
---@param rtu_state rtu_state
- function public.handle_packet(packet, units, rtu_state)
+ ---@param sounders table speaker alarm sounders
+ function public.handle_packet(packet, units, rtu_state, sounders)
-- print a log message to the terminal as long as the UI isn't running
local function println_ts(message) if not rtu_state.fp_ok then util.println_ts(message) end end
@@ -387,7 +432,49 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- 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
local est_ack = packet.data[1]
@@ -421,36 +508,6 @@ function rtu.comms(version, nic, rtu_channel, svr_channel, range, conn_watchdog)
else
log.debug("SCADA_MGMT establish packet length mismatch")
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
log.debug("discarding non-link SCADA_MGMT packet before linked")
end
diff --git a/rtu/startup.lua b/rtu/startup.lua
index dd1c27f..acec142 100644
--- a/rtu/startup.lua
+++ b/rtu/startup.lua
@@ -4,6 +4,7 @@
require("/initenv").init_env()
+local audio = require("scada-common.audio")
local comms = require("scada-common.comms")
local crash = require("scada-common.crash")
local log = require("scada-common.log")
@@ -30,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
-local RTU_VERSION = "v1.5.5"
+local RTU_VERSION = "v1.6.0"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE
@@ -96,6 +97,9 @@ local function main()
return
end
+ -- generate alarm tones
+ audio.generate_tones()
+
---@class rtu_shared_memory
local __shared_memory = {
-- RTU system state flags
@@ -106,6 +110,11 @@ local function main()
shutdown = false
},
+ -- RTU gateway devices (not RTU units)
+ rtu_dev = {
+ sounders = {}
+ },
+
-- system objects
rtu_sys = {
nic = network.nic(modem),
@@ -481,6 +490,18 @@ local function main()
log.info("startup> running in headless mode without front panel")
end
+ -- find and setup all speakers
+ local speakers = ppm.get_all_devices("speaker")
+ for _, s in pairs(speakers) do
+ local sounder = rtu.init_sounder(s)
+
+ table.insert(__shared_memory.rtu_dev.sounders, sounder)
+
+ log.debug(util.c("startup> added speaker, attached as ", sounder.name))
+ end
+
+ databus.tx_hw_spkr_count(#__shared_memory.rtu_dev.sounders)
+
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
log.debug("startup> conn watchdog started")
diff --git a/rtu/threads.lua b/rtu/threads.lua
index 904f37b..ee8e270 100644
--- a/rtu/threads.lua
+++ b/rtu/threads.lua
@@ -8,6 +8,7 @@ local util = require("scada-common.util")
local databus = require("rtu.databus")
local modbus = require("rtu.modbus")
local renderer = require("rtu.renderer")
+local rtu = require("rtu.rtu")
local boilerv_rtu = require("rtu.dev.boilerv_rtu")
local dynamicv_rtu = require("rtu.dev.dynamicv_rtu")
@@ -24,7 +25,7 @@ local threads = {}
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
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)
-- main thread
@@ -47,6 +48,7 @@ function threads.thread__main(smem)
-- load in from shared memory
local rtu_state = smem.rtu_state
+ local sounders = smem.rtu_dev.sounders
local nic = smem.rtu_sys.nic
local rtu_comms = smem.rtu_sys.rtu_comms
local conn_watchdog = smem.rtu_sys.conn_watchdog
@@ -66,6 +68,15 @@ function threads.thread__main(smem)
-- blink heartbeat indicator
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
loop_clock.start()
@@ -110,6 +121,18 @@ function threads.thread__main(smem)
else
log.warning("non-comms modem disconnected")
end
+ elseif type == "speaker" then
+ for i = 1, #sounders do
+ if sounders[i].speaker == device then
+ table.remove(sounders, i)
+
+ log.warning(util.c("speaker ", param1, " disconnected"))
+ println_ts("speaker disconnected")
+
+ databus.tx_hw_spkr_count(#sounders)
+ break
+ end
+ end
else
for i = 1, #units do
-- find disconnected device
@@ -147,6 +170,13 @@ function threads.thread__main(smem)
else
log.info("wired modem reconnected")
end
+ elseif type == "speaker" then
+ table.insert(sounders, rtu.init_sounder(device))
+
+ println_ts("speaker connected")
+ log.info(util.c("connected speaker ", param1))
+
+ databus.tx_hw_spkr_count(#sounders)
else
-- relink lost peripheral to correct unit entry
for i = 1, #units do
@@ -252,6 +282,15 @@ function threads.thread__main(smem)
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" then
-- handle a mouse event
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
+ elseif event == "speaker_audio_empty" then
+ -- handle empty speaker audio buffer
+ for i = 1, #sounders do
+ local sounder = sounders[i] ---@type rtu_speaker_sounder
+ if sounder.name == param1 then
+ sounder.continue()
+ break
+ end
+ end
end
-- check for termination request
@@ -299,6 +338,7 @@ function threads.thread__comms(smem)
-- load in from shared memory
local rtu_state = smem.rtu_state
+ local sounders = smem.rtu_dev.sounders
local rtu_comms = smem.rtu_sys.rtu_comms
local units = smem.rtu_sys.units
@@ -321,8 +361,8 @@ function threads.thread__comms(smem)
-- received data
elseif msg.qtype == mqueue.TYPE.PACKET then
-- received a packet
- -- handle the packet (rtu_state passed to allow setting link flag)
- rtu_comms.handle_packet(msg.message, units, rtu_state)
+ -- handle the packet (rtu_state passed to allow setting link flag, sounders passed to manage alarm audio)
+ rtu_comms.handle_packet(msg.message, units, rtu_state, sounders)
end
end
diff --git a/scada-common/audio.lua b/scada-common/audio.lua
new file mode 100644
index 0000000..32dfb57
--- /dev/null
+++ b/scada-common/audio.lua
@@ -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
diff --git a/scada-common/comms.lua b/scada-common/comms.lua
index 6a8324d..af049d7 100644
--- a/scada-common/comms.lua
+++ b/scada-common/comms.lua
@@ -14,50 +14,54 @@ local max_distance = nil ---@type number|nil maximum acceptable t
---@class comms
local comms = {}
-comms.version = "2.1.2"
+comms.version = "2.2.1"
---@enum PROTOCOL
local PROTOCOL = {
- MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
- RPLC = 1, -- reactor PLC protocol
- SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
- SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers
- COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
+ MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
+ RPLC = 1, -- reactor PLC protocol
+ SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
+ SCADA_CRDN = 3, -- data/control packets for coordinators to/from supervisory controllers
+ COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
---@enum RPLC_TYPE
local RPLC_TYPE = {
- STATUS = 0, -- reactor/system status
- MEK_STRUCT = 1, -- mekanism build structure
- MEK_BURN_RATE = 2, -- set burn rate
- RPS_ENABLE = 3, -- enable reactor
- RPS_SCRAM = 4, -- SCRAM reactor (manual request)
- RPS_ASCRAM = 5, -- SCRAM reactor (automatic request)
- RPS_STATUS = 6, -- RPS status
- RPS_ALARM = 7, -- RPS alarm broadcast
- RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately)
- RPS_AUTO_RESET = 9, -- clear RPS trip if it is just a timeout or auto scram
- AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited
+ STATUS = 0, -- reactor/system status
+ MEK_STRUCT = 1, -- mekanism build structure
+ MEK_BURN_RATE = 2, -- set burn rate
+ RPS_ENABLE = 3, -- enable reactor
+ RPS_SCRAM = 4, -- SCRAM reactor (manual request)
+ RPS_ASCRAM = 5, -- SCRAM reactor (automatic request)
+ RPS_STATUS = 6, -- RPS status
+ RPS_ALARM = 7, -- RPS alarm broadcast
+ RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately)
+ RPS_AUTO_RESET = 9, -- clear RPS trip if it is just a timeout or auto scram
+ AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited
}
---@enum SCADA_MGMT_TYPE
local SCADA_MGMT_TYPE = {
- ESTABLISH = 0, -- establish new connection
- KEEP_ALIVE = 1, -- keep alive packet w/ RTT
- CLOSE = 2, -- close a connection
- RTU_ADVERT = 3, -- RTU capability advertisement
- RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
+ ESTABLISH = 0, -- establish new connection
+ KEEP_ALIVE = 1, -- keep alive packet w/ RTT
+ CLOSE = 2, -- close a connection
+ RTU_ADVERT = 3, -- RTU capability advertisement
+ RTU_DEV_REMOUNT = 4, -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
+ RTU_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
local SCADA_CRDN_TYPE = {
- INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
- FAC_BUILDS = 1, -- facility RTU builds
- FAC_STATUS = 2, -- state of facility and facility devices
- FAC_CMD = 3, -- faility command
- UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs)
- UNIT_STATUSES = 5, -- state of each of the reactor units
- UNIT_CMD = 6 -- command a reactor unit
+ INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
+ FAC_BUILDS = 1, -- facility RTU builds
+ FAC_STATUS = 2, -- state of facility and facility devices
+ FAC_CMD = 3, -- faility command
+ UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs)
+ UNIT_STATUSES = 5, -- state of each of the reactor units
+ UNIT_CMD = 6 -- command a reactor unit
}
---@enum CAPI_TYPE
@@ -66,50 +70,50 @@ local CAPI_TYPE = {
---@enum ESTABLISH_ACK
local ESTABLISH_ACK = {
- ALLOW = 0, -- link approved
- DENY = 1, -- link denied
- COLLISION = 2, -- link denied due to existing active link
- BAD_VERSION = 3 -- link denied due to comms version mismatch
+ ALLOW = 0, -- link approved
+ DENY = 1, -- link denied
+ COLLISION = 2, -- link denied due to existing active link
+ BAD_VERSION = 3 -- link denied due to comms version mismatch
}
---@enum DEVICE_TYPE
local DEVICE_TYPE = {
- PLC = 0, -- PLC device type for establish
- RTU = 1, -- RTU device type for establish
- SV = 2, -- supervisor device type for establish
- CRDN = 3, -- coordinator device type for establish
- PKT = 4 -- pocket device type for establish
+ PLC = 0, -- PLC device type for establish
+ RTU = 1, -- RTU device type for establish
+ SV = 2, -- supervisor device type for establish
+ CRDN = 3, -- coordinator device type for establish
+ PKT = 4 -- pocket device type for establish
}
---@enum PLC_AUTO_ACK
local PLC_AUTO_ACK = {
- FAIL = 0, -- failed to set burn rate/burn rate invalid
- DIRECT_SET_OK = 1, -- successfully set burn rate
- RAMP_SET_OK = 2, -- successfully started burn rate ramping
- ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate
+ FAIL = 0, -- failed to set burn rate/burn rate invalid
+ DIRECT_SET_OK = 1, -- successfully set burn rate
+ RAMP_SET_OK = 2, -- successfully started burn rate ramping
+ ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate
}
---@enum FAC_COMMAND
local FAC_COMMAND = {
- SCRAM_ALL = 0, -- SCRAM all reactors
- STOP = 1, -- stop automatic process control
- START = 2, -- start automatic process control
- ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
- SET_WASTE_MODE = 4, -- set automatic waste processing mode
- SET_PU_FB = 5 -- set plutonium fallback mode
+ SCRAM_ALL = 0, -- SCRAM all reactors
+ STOP = 1, -- stop automatic process control
+ START = 2, -- start automatic process control
+ ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
+ SET_WASTE_MODE = 4, -- set automatic waste processing mode
+ SET_PU_FB = 5 -- set plutonium fallback mode
}
---@enum UNIT_COMMAND
local UNIT_COMMAND = {
- SCRAM = 0, -- SCRAM the reactor
- START = 1, -- start the reactor
- RESET_RPS = 2, -- reset the RPS
- SET_BURN = 3, -- set the burn rate
- SET_WASTE = 4, -- set the waste processing mode
- ACK_ALL_ALARMS = 5, -- ack all active alarms
- ACK_ALARM = 6, -- ack a particular alarm
- RESET_ALARM = 7, -- reset a particular alarm
- SET_GROUP = 8 -- assign this unit to a group
+ SCRAM = 0, -- SCRAM the reactor
+ START = 1, -- start the reactor
+ RESET_RPS = 2, -- reset the RPS
+ SET_BURN = 3, -- set the burn rate
+ SET_WASTE = 4, -- set the waste processing mode
+ ACK_ALL_ALARMS = 5, -- ack all active alarms
+ ACK_ALARM = 6, -- ack a particular alarm
+ RESET_ALARM = 7, -- reset a particular alarm
+ SET_GROUP = 8 -- assign this unit to a group
}
comms.PROTOCOL = PROTOCOL
@@ -146,6 +150,7 @@ function comms.scada_packet()
local self = {
modem_msg_in = nil, ---@type modem_message|nil
valid = false,
+ authenticated = false,
raw = {},
src_addr = comms.BROADCAST,
dest_addr = comms.BROADCAST,
@@ -234,6 +239,9 @@ function comms.scada_packet()
return self.valid
end
+ -- report that this packet has been authenticated (was received with a valid HMAC)
+ function public.stamp_authenticated() self.authenticated = true end
+
-- public accessors --
---@nodiscard
@@ -248,6 +256,8 @@ function comms.scada_packet()
---@nodiscard
function public.is_valid() return self.valid end
+ ---@nodiscard
+ function public.is_authenticated() return self.authenticated end
---@nodiscard
function public.src_addr() return self.src_addr end
@@ -313,7 +323,7 @@ function comms.authd_packet()
self.valid = false
self.raw = self.modem_msg_in.msg
- if (type(max_distance) == "number") and (distance > max_distance) then
+ if (type(max_distance) == "number") and (type(distance) == "number") and (distance > max_distance) then
-- outside of maximum allowable transmission distance
-- log.debug("comms.authd_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range")
else
@@ -588,7 +598,11 @@ function comms.mgmt_packet()
self.type == SCADA_MGMT_TYPE.CLOSE or
self.type == SCADA_MGMT_TYPE.REMOTE_LINKED 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
-- make a SCADA management packet
diff --git a/scada-common/constants.lua b/scada-common/constants.lua
index 7c661ee..eaf6dd3 100644
--- a/scada-common/constants.lua
+++ b/scada-common/constants.lua
@@ -2,10 +2,12 @@
-- System and Safety Constants
--
+---@class scada_constants
local constants = {}
--#region Reactor Protection System (on the PLC) Limits
+---@class _rps_constants
local rps = {}
rps.MAX_DAMAGE_PERCENT = 90 -- damage >= 90%
@@ -21,6 +23,7 @@ constants.RPS_LIMITS = rps
--#region Annunciator Limits
+---@class _annunciator_constants
local annunc = {}
annunc.RCSFlowLow_H2O = -3.2 -- flow < -3.2 mB/s
@@ -44,6 +47,7 @@ constants.ANNUNCIATOR_LIMITS = annunc
--#region Supervisor Alarm Limits
+---@class _alarm_constants
local alarms = {}
-- unit alarms
diff --git a/scada-common/crash.lua b/scada-common/crash.lua
index 45a3874..1942a8d 100644
--- a/scada-common/crash.lua
+++ b/scada-common/crash.lua
@@ -6,8 +6,10 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log")
local util = require("scada-common.util")
-local core = require("graphics.core")
+local has_graphics, core = pcall(require, "graphics.core")
+local has_lockbox, lockbox = pcall(require, "lockbox")
+---@class crash_handler
local crash = {}
local app = "unknown"
@@ -34,7 +36,8 @@ function crash.handler(error)
log.info(util.c("APPLICATION: ", app))
log.info(util.c("FIRMWARE VERSION: ", ver))
log.info(util.c("COMMS VERSION: ", comms.version))
- log.info(util.c("GRAPHICS VERSION: ", core.version))
+ if has_graphics then log.info(util.c("GRAPHICS VERSION: ", core.version)) end
+ if has_lockbox then log.info(util.c("LOCKBOX VERSION: ", lockbox.version)) end
log.info("----------------------------------")
log.info(debug.traceback("--- begin debug trace ---", 1))
log.info("--- end debug trace ---")
diff --git a/scada-common/log.lua b/scada-common/log.lua
index 51b5373..2c266bb 100644
--- a/scada-common/log.lua
+++ b/scada-common/log.lua
@@ -4,14 +4,11 @@
local util = require("scada-common.util")
----@class log
+---@class logger
local log = {}
---@alias MODE integer
-local MODE = {
- APPEND = 0,
- NEW = 1
-}
+local MODE = { APPEND = 0, NEW = 1 }
log.MODE = MODE
diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua
index b48e4ad..fc60a1e 100644
--- a/scada-common/mqueue.lua
+++ b/scada-common/mqueue.lua
@@ -4,6 +4,14 @@
local mqueue = {}
+---@class queue_item
+---@field qtype MQ_TYPE
+---@field message any
+
+---@class queue_data
+---@field key any
+---@field val any
+
---@enum MQ_TYPE
local TYPE = {
COMMAND = 0,
@@ -13,22 +21,14 @@ local TYPE = {
mqueue.TYPE = TYPE
+local insert = table.insert
+local remove = table.remove
+
-- create a new message queue
---@nodiscard
function mqueue.new()
local queue = {}
- local insert = table.insert
- local remove = table.remove
-
- ---@class queue_item
- ---@field qtype MQ_TYPE
- ---@field message any
-
- ---@class queue_data
- ---@field key any
- ---@field val any
-
---@class mqueue
local public = {}
@@ -48,28 +48,20 @@ function mqueue.new()
-- push a new item onto the queue
---@param qtype MQ_TYPE
---@param message any
- local function _push(qtype, message)
- insert(queue, { qtype = qtype, message = message })
- end
+ local function _push(qtype, message) insert(queue, { qtype = qtype, message = message }) end
-- push a command onto the queue
---@param message any
- function public.push_command(message)
- _push(TYPE.COMMAND, message)
- end
+ function public.push_command(message) _push(TYPE.COMMAND, message) end
-- push data onto the queue
---@param key any
---@param value any
- function public.push_data(key, value)
- _push(TYPE.DATA, { key = key, val = value })
- end
+ function public.push_data(key, value) _push(TYPE.DATA, { key = key, val = value }) end
-- push a packet onto the queue
---@param packet packet|frame
- function public.push_packet(packet)
- _push(TYPE.PACKET, packet)
- end
+ function public.push_packet(packet) _push(TYPE.PACKET, packet) end
-- get an item off the queue
---@nodiscard
@@ -77,9 +69,7 @@ function mqueue.new()
function public.pop()
if #queue > 0 then
return remove(queue, 1)
- else
- return nil
- end
+ else return nil end
end
return public
diff --git a/scada-common/network.lua b/scada-common/network.lua
index 491faef..d9fa83f 100644
--- a/scada-common/network.lua
+++ b/scada-common/network.lua
@@ -2,19 +2,21 @@
-- Network Communications
--
+local comms = require("scada-common.comms")
+local log = require("scada-common.log")
+local util = require("scada-common.util")
+
local md5 = require("lockbox.digest.md5")
local sha256 = require("lockbox.digest.sha2_256")
local pbkdf2 = require("lockbox.kdf.pbkdf2")
local hmac = require("lockbox.mac.hmac")
local stream = require("lockbox.util.stream")
local array = require("lockbox.util.array")
-local comms = require("scada-common.comms")
-
-local log = require("scada-common.log")
-local util = require("scada-common.util")
+---@class scada_net_interface
local network = {}
+-- cryptography engine
local c_eng = {
key = nil,
hmac = nil
@@ -212,6 +214,7 @@ function network.nic(modem)
if packet_hmac == computed_hmac then
-- 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.stamp_authenticated()
else
-- log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
end
diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua
index 90f0b17..6af4e2f 100644
--- a/scada-common/ppm.lua
+++ b/scada-common/ppm.lua
@@ -8,15 +8,13 @@ local util = require("scada-common.util")
---@class ppm
local ppm = {}
-local ACCESS_FAULT = nil ---@type nil
-
-local UNDEFINED_FIELD = "undefined field"
-
+local ACCESS_FAULT = nil ---@type nil
+local UNDEFINED_FIELD = "undefined field"
local VIRTUAL_DEVICE_TYPE = "ppm_vdev"
-ppm.ACCESS_FAULT = ACCESS_FAULT
-ppm.UNDEFINED_FIELD = UNDEFINED_FIELD
-ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
+ppm.ACCESS_FAULT = ACCESS_FAULT
+ppm.UNDEFINED_FIELD = UNDEFINED_FIELD
+ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
----------------------------
-- PRIVATE DATA/FUNCTIONS --
@@ -34,9 +32,9 @@ local ppm_sys = {
mute = false
}
--- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program
--- also provides peripheral-specific fault checks (auto-clear fault defaults to true)
--- assumes iface is a valid peripheral
+-- Wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program.
+-- Additionally provides peripheral-specific fault checks (auto-clear fault defaults to true).
+-- Note: assumes iface is a valid peripheral.
---@param iface string CC peripheral interface
local function peri_init(iface)
local self = {
@@ -307,16 +305,12 @@ end
-- list all available peripherals
---@nodiscard
---@return table names
-function ppm.list_avail()
- return peripheral.getNames()
-end
+function ppm.list_avail() return peripheral.getNames() end
-- list mounted peripherals
---@nodiscard
---@return table mounts
-function ppm.list_mounts()
- return ppm_sys.mounts
-end
+function ppm.list_mounts() return ppm_sys.mounts end
-- get a mounted peripheral side/interface by device table
---@nodiscard
@@ -390,9 +384,7 @@ end
-- get the fission reactor (if multiple, returns the first)
---@nodiscard
---@return table|nil reactor function table
-function ppm.get_fission_reactor()
- return ppm.get_device("fissionReactorLogicAdapter")
-end
+function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end
-- get the wireless modem (if multiple, returns the first)
-- if this is in a CraftOS emulated environment, wired modems will be used instead
@@ -419,9 +411,7 @@ function ppm.get_monitor_list()
local list = {}
for iface, device in pairs(ppm_sys.mounts) do
- if device.type == "monitor" then
- list[iface] = device
- end
+ if device.type == "monitor" then list[iface] = device end
end
return list
diff --git a/scada-common/psil.lua b/scada-common/psil.lua
index 13dcaa5..09686bf 100644
--- a/scada-common/psil.lua
+++ b/scada-common/psil.lua
@@ -9,14 +9,12 @@ local psil = {}
-- instantiate a new PSI layer
---@nodiscard
function psil.create()
- local self = {
- ic = {}
- }
+ local ic = {}
-- allocate a new interconnect field
---@key string data key
local function alloc(key)
- self.ic[key] = { subscribers = {}, value = nil }
+ ic[key] = { subscribers = {}, value = nil }
end
---@class psil
@@ -28,22 +26,22 @@ function psil.create()
---@param func function function to call on change
function public.subscribe(key, func)
-- allocate new key if not found or notify if value is found
- if self.ic[key] == nil then
+ if ic[key] == nil then
alloc(key)
- elseif self.ic[key].value ~= nil then
- func(self.ic[key].value)
+ elseif ic[key].value ~= nil then
+ func(ic[key].value)
end
-- subscribe to key
- table.insert(self.ic[key].subscribers, { notify = func })
+ table.insert(ic[key].subscribers, { notify = func })
end
-- unsubscribe a function from a given key
---@param key string data key
---@param func function function to unsubscribe
function public.unsubscribe(key, func)
- if self.ic[key] ~= nil then
- util.filter_table(self.ic[key].subscribers, function (s) return s.notify ~= func end)
+ if ic[key] ~= nil then
+ util.filter_table(ic[key].subscribers, function (s) return s.notify ~= func end)
end
end
@@ -51,32 +49,32 @@ function psil.create()
---@param key string data key
---@param value any data value
function public.publish(key, value)
- if self.ic[key] == nil then alloc(key) end
+ if ic[key] == nil then alloc(key) end
- if self.ic[key].value ~= value then
- for i = 1, #self.ic[key].subscribers do
- self.ic[key].subscribers[i].notify(value)
+ if ic[key].value ~= value then
+ for i = 1, #ic[key].subscribers do
+ ic[key].subscribers[i].notify(value)
end
end
- self.ic[key].value = value
+ ic[key].value = value
end
-- publish a toggled boolean value to a given key, passing it to all subscribers if it has changed
-- this is intended to be used to toggle boolean indicators such as heartbeats without extra state variables
---@param key string data key
function public.toggle(key)
- if self.ic[key] == nil then alloc(key) end
+ if ic[key] == nil then alloc(key) end
- self.ic[key].value = self.ic[key].value == false
+ ic[key].value = ic[key].value == false
- for i = 1, #self.ic[key].subscribers do
- self.ic[key].subscribers[i].notify(self.ic[key].value)
+ for i = 1, #ic[key].subscribers do
+ ic[key].subscribers[i].notify(ic[key].value)
end
end
-- clear the contents of the interconnect
- function public.purge() self.ic = nil end
+ function public.purge() ic = {} end
return public
end
diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua
index 29acfd2..13a6d6a 100644
--- a/scada-common/rsio.lua
+++ b/scada-common/rsio.lua
@@ -123,7 +123,7 @@ function rsio.to_string(port)
if util.is_int(port) and port > 0 and port <= #names then
return names[port]
else
- return ""
+ return "UNKNOWN"
end
end
diff --git a/scada-common/tcd.lua b/scada-common/tcd.lua
index f5ff0e5..a3c920f 100644
--- a/scada-common/tcd.lua
+++ b/scada-common/tcd.lua
@@ -68,7 +68,7 @@ function tcd.handle(event)
end
end
--- identify any overdo callbacks
+-- identify any overdue callbacks
-- prints to log debug output
function tcd.diagnostics()
for timer, entry in pairs(registry) do
diff --git a/scada-common/util.lua b/scada-common/util.lua
index 99f62a6..bf44884 100644
--- a/scada-common/util.lua
+++ b/scada-common/util.lua
@@ -7,6 +7,9 @@ local cc_strings = require("cc.strings")
---@class util
local util = {}
+-- scada-common version
+util.version = "1.0.1"
+
-- ENVIRONMENT CONSTANTS --
util.TICK_TIME_S = 0.05
@@ -80,9 +83,9 @@ end
---@return string
function util.strrep(str, n)
local repeated = ""
- for _ = 1, n do
- repeated = repeated .. str
- end
+
+ for _ = 1, n do repeated = repeated .. str end
+
return repeated
end
@@ -123,7 +126,9 @@ function util.strwrap(str, limit) return cc_strings.wrap(str, limit) end
---@diagnostic disable-next-line: unused-vararg
function util.concat(...)
local str = ""
+
for _, v in ipairs(arg) do str = str .. util.strval(v) end
+
return str
end
@@ -322,7 +327,8 @@ end
--- EVENT_CONSUMER: this function consumes events
function util.nop() util.psleep(0.05) end
--- attempt to maintain a minimum loop timing (duration of execution)
+-- attempt to maintain a minimum loop timing (duration of execution)
+-- note: will not yield for time periods less than 50ms
---@nodiscard
---@param target_timing integer minimum amount of milliseconds to wait for
---@param last_update integer millisecond time of last update
@@ -399,7 +405,7 @@ local function GFE(fe) return fe / 1000000000.0 end
local function TFE(fe) return fe / 1000000000000.0 end
local function PFE(fe) return fe / 1000000000000000.0 end
local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass
-local function ZFE(fe) return fe / 1000000000000000000000.0 end -- please stop
+local function ZFE(fe) return fe / 1000000000000000000000.0 end -- how & why did you do this?
-- format a power value into XXX.XX UNIT format (FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE)
---@nodiscard
diff --git a/supervisor/facility.lua b/supervisor/facility.lua
index 641f0c5..8c91157 100644
--- a/supervisor/facility.lua
+++ b/supervisor/facility.lua
@@ -1,3 +1,4 @@
+local audio = require("scada-common.audio")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
@@ -10,13 +11,17 @@ local qtypes = require("supervisor.session.rtu.qtypes")
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 CONTAINER_MODE = types.CONTAINER_MODE
local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES
-local PRIO = types.ALARM_PRIORITY
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
-local CONTAINER_MODE = types.CONTAINER_MODE
-local WASTE = types.WASTE_PRODUCT
local WASTE_MODE = types.WASTE_MODE
+local WASTE = types.WASTE_PRODUCT
local IO = rsio.IO
@@ -65,6 +70,7 @@ function facility.new(num_reactors, cooling_conf)
units = {},
status_text = { "START UP", "initializing..." },
all_sys_ok = false,
+ allow_testing = false,
-- rtus
rtu_conn_count = 0,
rtu_list = {},
@@ -114,6 +120,12 @@ function facility.new(num_reactors, cooling_conf)
waste_product = WASTE.PLUTONIUM,
current_waste_product = WASTE.PLUTONIUM,
pu_fallback = false,
+ -- alarm tones
+ tone_states = {},
+ test_tone_set = false,
+ test_tone_reset = false,
+ test_tone_states = {},
+ test_alarm_states = {},
-- statistics
im_stat_init = false,
avg_charge = util.mov_avg(3, 0.0),
@@ -133,6 +145,13 @@ function facility.new(num_reactors, cooling_conf)
-- init redstone RTU I/O controller
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
---@nodiscard
local function _all_units_ramped()
@@ -267,15 +286,20 @@ function facility.new(num_reactors, cooling_conf)
-- supervisor sessions reporting the list of active RTU sessions
---@param rtu_sessions table session list of all connected RTUs
- function public.report_rtus(rtu_sessions)
- self.rtu_conn_count = #rtu_sessions
- end
+ function public.report_rtus(rtu_sessions) self.rtu_conn_count = #rtu_sessions end
-- update (iterate) the facility management
function public.update()
-- 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
+ -- 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
local charge_update = 0
local rate_update = 0
@@ -778,6 +802,97 @@ function facility.new(num_reactors, cooling_conf)
end
end
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
-- call the update function of all units in the facility
@@ -919,8 +1034,52 @@ function facility.new(num_reactors, cooling_conf)
return self.pu_fallback
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 --
+ -- 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
---@nodiscard
---@param type RTU_UNIT_TYPE? type or nil to include only a particular unit type, or to include all if nil
diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua
index 015b599..aa64e17 100644
--- a/supervisor/session/coordinator.lua
+++ b/supervisor/session/coordinator.lua
@@ -154,7 +154,8 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
local function _send_fac_status()
local status = {
facility.get_control_status(),
- facility.get_rtu_statuses()
+ facility.get_rtu_statuses(),
+ facility.get_alarm_tones()
}
_send(SCADA_CRDN_TYPE.FAC_STATUS, status)
diff --git a/supervisor/session/pocket.lua b/supervisor/session/pocket.lua
index 9de55ab..30ca7eb 100644
--- a/supervisor/session/pocket.lua
+++ b/supervisor/session/pocket.lua
@@ -33,8 +33,9 @@ local PERIODICS = {
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
+---@param facility facility facility data table
---@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
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
-- close the session
_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
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua
index c33f11a..1e42f49 100644
--- a/supervisor/session/rtu.lua
+++ b/supervisor/session/rtu.lua
@@ -26,7 +26,8 @@ local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local PERIODICS = {
- KEEP_ALIVE = 2000
+ KEEP_ALIVE = 2000,
+ ALARM_TONES = 500
}
-- create a new RTU session
@@ -58,7 +59,8 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- periodic messages
periodics = {
last_update = 0,
- keep_alive = 0
+ keep_alive = 0,
+ alarm_tones = 0
},
units = {}
}
@@ -389,6 +391,14 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
periodics.keep_alive = 0
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()
--------------------------------------------
diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua
index e890401..05b543b 100644
--- a/supervisor/session/svsessions.lua
+++ b/supervisor/session/svsessions.lua
@@ -104,8 +104,8 @@ local function _sv_handle_outq(session)
-- max 100ms spent processing queue
if util.time() - handle_start > 100 then
- log.warning("[SVS] supervisor out queue handler exceeded 100ms queue process limit")
- log.warning(util.c("[SVS] offending session: ", session))
+ log.debug("[SVS] supervisor out queue handler exceeded 100ms queue process limit")
+ log.debug(util.c("[SVS] offending session: ", session))
break
end
end
@@ -430,7 +430,8 @@ function svsessions.establish_pdg_session(source_addr, version)
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)
local mt = {
diff --git a/supervisor/unit.lua b/supervisor/unit.lua
index 1d3dd60..262de6b 100644
--- a/supervisor/unit.lua
+++ b/supervisor/unit.lua
@@ -733,6 +733,23 @@ function unit.new(reactor_id, num_boilers, num_turbines)
return false
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
+
-- check if emergency coolant activation has been tripped
---@nodiscard
function public.is_emer_cool_tripped() return self.emcool_opened end