alarm-system/Scripts/SecuritySystem.lua

754 lines
19 KiB
Lua

local DataStoreService = game:GetService("DataStoreService")
local dataStore = DataStoreService:GetDataStore('securityCodes-' .. script.Parent.dataStoreKey.Value or "XP-99")
function setup(parentScript)
-- dont touch
version = "v1.2.0"
-- codes object, will be swapped for data store later
codes = dataStore:GetAsync("codes")
if not codes then
codes = {"1234"}
dataStore:SetAsync("codes", codes)
end
-- Make a settings object mapped to the values in parentScript.Parent.Settings
settings = {}
for i, e in pairs(parentScript.Parent.Settings:GetChildren()) do
settings[e.Name] = e.Value
end
power = false
chime = false
armState = 0
delay = 0
instant = false
faults = {}
fault = false
alarms = {}
fires = {}
zones = {}
keypads = {}
--[[
armState
0 is disarmed
1 is armed stay
2 is armed away
3 is alarm
4 is fire alarm
5 is armed instant (stay no entry delay)
6 is armed maximum (away no entry delay)
delay
0 is inactive
1 is exit delay
When active, use slow beep, show ARMED ***AWAY***\nYou may exit now or ARMED ***STAY***\nYou may exit now depending on state. During last 10 seconds of delay, switch to fast beep, at each second check if the system has been armed, if it has drop out of the loop
2 is entry delay. When active, use slow beep, show "DISARM SYSTEM\nOr alarm will occur", during last 10 seconds of delay, switch to fast beep. At each second check if the system has been disarmed, if it has drop out of the loop
]]
ux = require(parentScript.Parent.ScriptModules:WaitForChild("UX"))
-- Get keypads
for i, e in pairs(parentScript.Parent.Keypads:GetChildren()) do
if e:IsA("Model") then table.insert(keypads, e) end
end
-- Get zone
for i, e in ipairs(parentScript.Parent.Zones:GetChildren()) do
-- add a task that constantly checks e.tripped.Value, and set zones[i] to the value of e.tripped.Value
zones[e.Name] = e
end
-- Funcs
function rightAlignString(str)
local strLen = string.len(str)
local padding = 16 - strLen
local paddingStr = ""
for i = 1, padding do paddingStr = paddingStr .. " " end
return paddingStr .. str
end
function playAud(sound) if not sound.Playing then sound:Play() end end
function stopAud(sound) if sound.Playing then sound:Stop() end end
local curTone = 0
local lastTone = 0 -- only really used to go back from button pulse to previous tone
local beeping = false
function changeTone(tone) curTone = tone end
function beep(keypad)
if not power then return end
if not beeping then
task.spawn(function()
beeping = true
lastTone = curTone
playAud(keypad.Display.hi)
wait(0.08)
stopAud(keypad.Display.hi)
wait(0.08)
beeping = false
end)
end
end
function ding(count)
-- Play a ding sound count times, each lasting .3 seconds with a .1 second pause between each
task.spawn(function()
for i = 1, count do
if not power then return end
for i, keypad in ipairs(keypads) do
playAud(keypad.Display.ding)
end
wait(0.2)
-- if its not the last ding, stop the sound
if i ~= count then
for i, keypad in ipairs(keypads) do
stopAud(keypad.Display.ding)
end
end
end
end)
end
--[[
0 no tone
1 constant
2 pulse (for number input)
3 slow pulse (entry/exit delay main)
4 fast pulse (end of exit/entry delay and trouble)
]]
function audioLoop()
while true do
wait()
if not power then curTone = 0 end
if curTone == 0 then -- Stop tone
for i, keypad in ipairs(keypads) do
stopAud(keypad.Display.hi)
stopAud(keypad.Display.lo)
end
elseif curTone == 1 then -- Constant tone
for i, keypad in ipairs(keypads) do
playAud(keypad.Display.hi)
stopAud(keypad.Display.lo)
end
elseif curTone == 2 then -- Slow pulse tone
for i, keypad in ipairs(keypads) do
stopAud(keypad.Display.hi)
stopAud(keypad.Display.lo)
end
wait(0.5)
for i, keypad in ipairs(keypads) do
playAud(keypad.Display.hi)
end
wait(0.5)
elseif curTone == 3 then -- Fast pulse tone
for i, keypad in ipairs(keypads) do
stopAud(keypad.Display.hi)
stopAud(keypad.Display.lo)
end
wait(0.065)
for i, keypad in ipairs(keypads) do
playAud(keypad.Display.hi)
end
wait(0.065)
elseif curTone == 4 then -- HiLo tone alarm
for i, keypad in ipairs(keypads) do
playAud(keypad.Display.hi)
stopAud(keypad.Display.lo)
end
wait(0.5)
for i, keypad in ipairs(keypads) do
stopAud(keypad.Display.hi)
playAud(keypad.Display.lo)
end
wait(0.5)
elseif curTone == 5 then -- Fire alarm tone
-- Make this do temporal 3
for i = 1, 3 do
for i, keypad in ipairs(keypads) do
playAud(keypad.Display.hi)
stopAud(keypad.Display.lo)
end
wait(.5)
for i, keypad in ipairs(keypads) do
stopAud(keypad.Display.hi)
stopAud(keypad.Display.lo)
end
wait(.5)
end
wait(1)
end
end
end
function displayLoop()
while true do
wait()
if not power then
for i, keypad in ipairs(keypads) do
ux.setDisplays(keypads, "", "")
end
else
if armState == 0 then -- We are disarmed, check if there are any faults
if #faults > 0 then
-- for however many faults there are, display them one at a time based on faultPage (gets updated every 2 seconds, or when * is pressed)
--ux.setDisplays(keypads, "FAULT " .. table.concat(faults, ", ", faultPage, faultPage + 1))
ux.setLEDs(keypads, "ready", false)
fault = true
ux.setDisplays(keypads, "FAULT " .. faults[1])
else
ux.setLEDs(keypads, "ready", true)
ux.setLEDs(keypads, "armed", false)
ux.setDisplays(keypads, "****DISARMED****", " READY TO ARM ")
end
elseif armState == 1 then -- We are armed stay
ux.setLEDs(keypads, "armed", true)
ux.setLEDs(keypads, "ready", false)
if delay == 1 then
ux.setDisplays(keypads, "ARMED ***STAY***", "You may exit now")
elseif delay == 2 then
ux.setDisplays(keypads, "DISARM SYSTEM", "Or alarm occurs.")
else
ux.setDisplays(keypads, "ARMED ***STAY***")
end
elseif armState == 2 then -- We are armed away
ux.setLEDs(keypads, "armed", true)
ux.setLEDs(keypads, "ready", false)
if delay == 1 then
ux.setDisplays(keypads, "ARMED ***AWAY***", "You may exit now")
elseif delay == 2 then
ux.setDisplays(keypads, "DISARM SYSTEM", "Or alarm occurs.")
else
ux.setDisplays(keypads, "ARMED ***AWAY***", "***ALL SECURE***")
end
elseif armState == 3 then -- We are in alarm
ux.setLEDs(keypads, "ready", false)
if curTone ~= 4 then changeTone(4) end
-- show the zone number of the alarmed zone
ux.setDisplays(keypads, "ALARM " .. tostring(alarms[1] or ""))
elseif armState == 4 then -- We are in fire alarm
ux.setLEDs(keypads, "ready", false)
-- show the zone number of the alarmed zone
ux.setDisplays(keypads, "FIRE " .. tostring(fires[1] or ""))
elseif armState == 5 then -- Armed instant
ux.setLEDs(keypads, "armed", true)
ux.setLEDs(keypads, "ready", false)
if delay == 1 then
ux.setDisplays(keypads, "ARMED *INSTANT*", "You may exit now")
else
ux.setDisplays(keypads, "ARMED *INSTANT*")
end
elseif armState == 6 then -- Armed max
ux.setLEDs(keypads, "armed", true)
ux.setLEDs(keypads, "ready", false)
if delay == 1 then
ux.setDisplays(keypads, "ARMED *MAXIMUM*", "You may exit now")
else
ux.setDisplays(keypads, "ARMED *MAXIMUM*", "***ALL SECURE***")
end
end
end
end
end
local input = {}
local keyClear = nil
local authenticated = false
local inputCount = 0
local inputMode = 0
local authedUser = nil
lockout = false
authAttempts = 0
function handleInput(key)
if not power then return end
if lockout then
task.spawn(function()
curTone = 1
wait(2)
curTone = 0
end)
return
end -- eventually do long beep 2 seconds here
if key == "*" then
deauth()
return
end
keyClear = math.random(1, 10000)
task.spawn(function() inputTimeout(keyClear) end)
if not authenticated then
-- Should work like a honeywell vista, for every number entered 4 or more in buffer, check if the last 4 numbers are a valid code. For every 4 numbers entered, if its not a valid code add one to the attempts
table.insert(input, key)
if #input >= 4 then
local code = table.concat(input, "", #input - 3, #input)
if table.find(codes, code) then
authenticated = true
inputMode = 1
authAttempts = 0
keyClear = nil
input = {}
authedUser = code
else
authAttempts = authAttempts + 1
if authAttempts >= 3 then
task.spawn(lockoutFunc)
end
keyClear = nil
input = {}
end
end
elseif authenticated and inputMode == 1 then
wait(math.random(.5, 1.5))
if key == "1" then
disarm()
deauth()
elseif key == "2" then
if armState ~= 0 then return end
armAway()
deauth()
elseif key == "3" then
if armState ~= 0 then return end
armStay()
deauth()
elseif key == "4" then -- Arm Max
if armState ~= 0 then return end
armMax()
deauth()
elseif key == "5" then -- Test (TODO)
elseif key == "6" then -- Bypass (TODO)
elseif key == "7" then -- Instant
if armState ~= 0 then return end
armInstant()
deauth()
elseif key == "8" then
-- If the code used was code 1, and the system is disarmed, Set inputMode to 2 (code change)
-- See what index the code is at, if its index 1, its the right code, set mode to 2. do NOT hardcode the code here
if codes[1] == authedUser then
inputMode = 2
else
ding(1)
deauth()
end
elseif key == "9" then
toggleChime()
deauth()
end
elseif authenticated and inputMode == 2 then
-- Keep adding inputs together until we have 2+4+4 in the buffer, if both codes match, and arent being used, set the new code to the code at index of the first 2 digits
-- input data is: 2 digits: user id/index, 4 digits: new code, 4 digits: new code verify
table.insert(input, key)
-- check if digit 3 is a hash and 4 a 0, if so delete the user
if #input >= 4 and input[3] == "#" and input[4] == "0" then
local uid = table.concat(input, "", 1, 2)
if tonumber(uid) == 1 then
ding(4)
return
else
codes[tonumber(uid)] = nil
dataStore:SetAsync("codes", codes)
ding(1)
end
deauth()
else
if #input >= 10 then
local uid = table.concat(input, "", 1, 2)
local newCode = table.concat(input, "", 3, 6)
local newCodeVerify = table.concat(input, "", 7, 10)
if newCode == newCodeVerify then
if not table.find(codes, newCode) then
codes[tonumber(uid)] = newCode
dataStore:SetAsync("codes", codes)
ding(1)
else
ding(4)
end
else
ding(4)
end
deauth()
end
end
end
end
function deauth()
keyClear = nil
authenticated = false
input = {}
inputMode = 0
end
function toggleChime()
if chime then
ding(2)
else
ding(1)
end
chime = not chime
deauth()
end
function disarm()
deauth()
task.spawn(function()
-- Reset any smoke zones
for i,e in pairs(zones) do
if e:FindFirstChild("Reset") then
e.Reset.Value = true
wait(5)
e.Reset.Value = false
end
end
end)
wait(.1)
armState = 0
changeTone(0)
alarms = {}
fires = {}
faults = {}
ding(1)
end
function armStay()
if #faults > 0 then
ding(1)
return
end
armState = 1
delay = 1
task.spawn(function()
ding(3)
for i = 1, settings.exitDelay do
if armState ~= 1 then
delay = 0
return
end
wait(1)
if i >= settings.exitDelay then delay = 0 end
end
end)
end
function armAway()
if #faults > 0 then
ding(1)
return
end
armState = 2
delay = 1
task.spawn(function()
changeTone(2)
for i = 1, settings.exitDelay do
if armState ~= 2 then
changeTone(0)
delay = 0
return
end
wait(1)
if i >= settings.exitDelay - 10 then changeTone(3) end
if i >= settings.exitDelay then
changeTone(0)
delay = 0
end
end
end)
end
function armMax()
if #faults > 0 then
ding(1)
return
end
armState = 6
delay = 1
task.spawn(function()
changeTone(2)
for i = 1, settings.exitDelay do
if armState ~= 6 then
changeTone(0)
delay = 0
return
end
wait(1)
if i >= settings.exitDelay - 10 then changeTone(3) end
if i >= settings.exitDelay then
changeTone(0)
delay = 0
end
end
end)
end
function armInstant()
if #faults > 0 then
ding(1)
return
end
armState = 5
delay = 1
task.spawn(function()
ding(3)
for i = 1, settings.exitDelay do
if armState ~= 5 then
delay = 0
return
end
wait(1)
if i >= settings.exitDelay then delay = 0 end
end
end)
end
function inputTimeout(myNum)
wait(10)
if keyClear ~= myNum then return end
authenticated = false
input = {}
end
function lockoutFunc()
lockout = true
changeTone(1)
wait(2)
changeTone(0)
wait(60)
lockout = false
end
-- Button Click Handler (each keypad has Buttons folder with parts that have a clickdetector nested within, add an event handler for each that adds the button to the input buffer)
function buttonClickHandler(button)
button.MouseClick:Connect(function()
if not power then return end
task.spawn(function() beep(button.Parent.Parent.Parent) end)
wait(0.05)
handleInput(button.Parent.Name)
end)
end
for i, keypad in ipairs(keypads) do
for j, button in ipairs(keypad.Buttons:GetChildren()) do
buttonClickHandler(button.ClickDetector)
end
end
-- Arming Functions
function boot()
task.spawn(audioLoop)
power = true
wait()
ux.displayPower(keypads, true)
ux.setLEDs(keypads, "*", true)
ux.setDisplays(keypads, "################", "################")
changeTone(1)
wait(1)
changeTone(0)
ux.setLEDs(keypads, "*", false)
ux.setLEDs(keypads, "power", true)
ux.setDisplays(keypads, "Busy - Standby", rightAlignString(version))
wait(5)
task.spawn(displayLoop)
task.spawn(handleZones)
task.spawn(sirenLoop)
task.spawn(stateLoop)
end
function stateLoop()
while true do
script.Parent.armState.Value = armState
script.Parent.delay.Value = delay
script.Parent.curTone.Value = curTone
wait(0)
end
end
function handleZones()
while true do
wait()
-- Always: If a zone is tripped, add it to the faults table if its not already there
-- If the zone is not tripped remove it from the faults table if its there
-- If the zone is tripped and the system is armed, check if its an instant zone, if it is, set the alarm state to 3, otherwise trigger entry delay
-- If the system is in alarm, check if the zone is tripped, if it is, add it to the alarms table if its not already there
for i, zone in pairs(zones) do
-- if the zone is tripped add it to the faults table if its not already there
if zone.tripped.Value then
-- if the zone is already in faults, continue
if not table.find(faults, zone.Name) then
if not table.find(faults, zone.Name) then
table.insert(faults, zone.Name)
end
if zone.isFire.Value then
armState = 4
changeTone(5)
table.insert(fires, zone.Name)
end
if zone.alwaysOn.Value then -- This zone is a 24/7 audible zone, go straight to alarm
armState = 3
changeTone(4)
table.insert(alarms, zone.Name)
end
if armState == 0 then
if zone:FindFirstChild("chime") then
if chime and zone.chime.Value then
ding(3)
end
end
elseif armState == 1 and delay == 0 then
-- if the zone is interior, ignore
if zone.interior.Value then
-- ignore
else
-- Check if the zone is instant or not
-- If it is, set the alarm state to 3
-- If it is not, trigger entry delay
if zone.instant.Value then
-- Add to alarms
table.insert(alarms, zone.Name)
armState = 3
changeTone(4)
else
delay = 2
task.spawn(function()
table.insert(alarms, zone.Name)
changeTone(2)
for i = 1, settings.entryDelay do
if armState ~= 1 then
changeTone(0)
delay = 0
return
end
wait(1)
if i >= settings.entryDelay - 10 then
changeTone(3)
end
if i >= settings.entryDelay then
-- at this point the system has not been disarmed, go to alarm
changeTone(4)
delay = 0
armState = 3
end
end
end)
end
end
elseif armState == 2 and delay == 0 then
-- Check if the zone is instant or not
-- If it is, set the alarm state to 3
-- If it is not, trigger entry delay
if zone.instant.Value then
-- Add to alarms
table.insert(alarms, zone.Name)
armState = 3
changeTone(4)
else
table.insert(alarms, zone.Name)
delay = 2
task.spawn(function()
changeTone(2)
for i = 1, settings.entryDelay do
if armState ~= 2 then
changeTone(0)
delay = 0
return
end
wait(1)
if i >= settings.entryDelay - 10 then
changeTone(3)
end
if i >= settings.entryDelay then
-- at this point the system has not been disarmed, go to alarm
changeTone(4)
delay = 0
armState = 3
end
end
end)
end
elseif armState == 3 then
-- if the zone is already in alarms, continue
if not table.find(alarms, zone.Name) then
table.insert(alarms, zone.Name)
changeTone(4)
end
elseif armState == 5 and delay == 0 then -- Armed instant,
if not zone.interior.Value then
table.insert(alarms, zone.Name)
armState = 3
changeTone(4)
end
elseif armState == 6 and delay == 0 then
table.insert(alarms, zone.Name)
armState = 3
changeTone(4)
end
end
else
if table.find(faults, zone.Name) then
table.remove(faults, table.find(faults, zone.Name))
end
end
end
end
end
function sirenLoop() -- For external sirens
while true do
wait()
if armState == 3 then
-- Turn on siren
for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
if not e.Speaker.siren.playing then
e.Speaker.siren:Play()
end
end
elseif armState == 4 then -- Fire alarm
-- Handle dedicated fire sirens
for i,e in ipairs(parentScript.Parent.FireSirens:GetChildren()) do
if not e.Speaker.siren.playing then
e.Speaker.siren:Play()
end
end
-- Make this do temporal 3
for i = 1, 3 do
for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
if not e.Speaker.siren.playing then
e.Speaker.siren:Play()
end
end
wait(.5)
for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
if e.Speaker.siren.playing then
e.Speaker.siren:Stop()
end
end
wait(.5)
end
wait(1)
else
-- Turn off siren
for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
if e.Speaker.siren.playing then
e.Speaker.siren:Stop()
end
end
for i,e in ipairs(parentScript.Parent.FireSirens:GetChildren()) do
if e.Speaker.siren.playing then
e.Speaker.siren:Stop()
end
end
end
end
end
boot()
end
setup(script)