#! lua local lanes = require "lanes".configure() local plot = require "plotspl" local struct = require "struct" local rc = dofile"scared.rc" rc.address = rc.address or "user@example.com" rc.mode = rc.mode or "SPL" -- today's date in (almost) ISO-8601 format local function isodate() return os.date"%Y-%m-%d %H:%M:%S" end -- today's date for use in a file name local function namedate() return os.date"%Y%m%d-%H%M%S" end -- make its argument be a single double-quoted string function flatten(s) if type(s)=="string" then return '"'..s..'"' end if type(s)~="table" then return nil end return '"'..table.concat(s, '" "')..'"' end -- Send an email to addr with subject and body. If body -- is nil then just repeat the subject. This assumes that -- a "mail" command is available with the usual options. local function email(addr, subj, body) body = body or subj local mail = ("mail -s '%q' %s"):format(subj,addr) local b = assert(io.popen(mail, "w")) b:write([[I heard a noise on ]], isodate(), "\r\n\r\n") b:write(body, "\r\n\r\n") b:write"(This an automated message.)\r\n" b:close() end -- Send a MIME packed email containing a file related -- to the event to a recipient with a subject local function mpack(address, subject, file) assert(address) assert(subject) assert(file) local cmd = ("mpack -a -s %q %q %s"):format(subject, file, address) print(cmd) os.execute(cmd) end -- Implement a buffer of samples. For simplicity, let the -- index increment without bound. Note that this will have -- a problem after 2^53 samples, which is about 30 million -- years at 10 samples per second or 300 thousand years at -- about 1000 samples per second. local recent = { depth=200, n=1, -- clear the buffer and set a new depth clear=function(self,newdepth) newdepth = newdepth or 200 local offset = self.n-self.depth-1 for i=1,self.depth do self[i+offset] = nil end self.n = 1 self.depth = newdepth end, -- add a sample to the buffer add=function(self,spl) local n = self.n self[n] = spl self[n-self.depth] = nil self.n = n + 1 end, -- return the entire buffer as a raw blob raw=function(self) return table.concat(self,'',self.n-self.depth,self.n-1) end, -- return a CSV formatted buffer assuming the samples -- are SPL levels at 10 Hz CSVtext=function(self, header) local t = header and {"s, dB"} or {} local offset = self.n-self.depth-1 for i=1,self.depth do if self[i+offset] then t[#t+1] = ("%0.1f, %s"):format(i/10,self[i+offset]) end end return table.concat(t,"\n") end } local function auheader(buf, rate) local fmt = ">c4LLLLLL" local hdr = struct.pack(fmt, ".snd", -- 0 magic number struct.size(fmt), -- 1 data offset #buf, -- 2 length or -1 1, -- 3 encoding uLaw rate, -- 4 8152 Hz sample rate 1, -- 5 mono 0) -- 6 empty metadata string return hdr end local function writeau(au, buf) local f = assert(io.open(au,"wb")) f:write(auheader(buf, 8152), buf) f:close() end function writemp3(name, udata) local soxgate = 'compand .1,.2 -inf,-55.1,-inf,-55,-55 0 -90 .1 ' local soxcompand = 'compand 0.3,1 6:-70,-60,-20 -5 -90 0.2 ' local soxcmd = 'sox -t au - -t wav -b 16 - ' .. soxgate .. soxcompand ..'| lame - '..name print(soxcmd) local fp = assert(io.popen('sh -c "'..soxcmd..'"', 'w')) fp:write(auheader(udata, 8000), udata) fp:close() end -- Quick and dirty way to force the UART to have the -- tty driver settings we need for clean raw access -- to the port. os.execute("stty -F /dev/ttyAMA0 115200" .." pass8 raw -iexten" .." -echo -echoe -echok -echoke -echoctl") -- Open the UART for both reading and writing, and send -- the SPLear command to go into raw SPL sample mode. The -- output must be unbuffered because the current SPLear -- firmware would revert to 0.5 Hz summary mode if it -- receives a newline. local port = assert(io.open('/dev/ttyAMA0','r+')) port:setvbuf"no" -- no output buffer at all local addr = flatten(rc.address) print("Notifying "..addr) -- command the requested mode if rc.mode == "record" then port:write"R" -- so this write is immediate local msgs = lanes.linda() -- temporary simulation of noises by 10 second timer lanes.timer(msgs, "tick", 10, 10) -- Function running in a separate thread that watches for loud -- noises signled via GPIO pin pinwatcher = lanes.gen("*", function() gpio = require "sysfsgpio" gpio.export(7) gpio.direction(7, "in") gpio.edge(7, "rising") print("Watching GPIO07 which is pin P1-26") while true do --local _, t = msgs:receive"tick" -- external simulation timer local r = gpio.wait(7) if r == 0 then -- poll timed out elseif r == 1 then msgs:send("eek", isodate()) else -- something unexpected happened end end end )() -- main loop reads data as usual, keeping an eye out for -- being scared, after which it saves and emails a recording. local triggered = false local eek while true do local sample = port:read(800) -- read 1/10 sec audio if sample then io.write("Rec "..(triggered and "TRG " or " "), recent.n, "\r") io.flush() recent:add(sample) -- Notice GPIO pin local s = pinwatcher.status if not (s == "waiting" or s == "running" or s == "pending") then print("Watcher status: "..s) break end local eekflag, eekval = msgs:receive(0.0, "eek") if eekflag then if not triggered then triggered = math.floor(recent.depth * 2. / 3.) eek = "EEK! "..eekval end end if triggered then triggered = triggered - 1 if triggered == 0 then triggered = false local data = recent:raw(false) local mp3file = "eek-"..namedate()..".mp3" writemp3(mp3file, data) print("email " .. addr, mp3file, isodate()) mpack(addr, eek, mp3file) end end spl0 = spl end end else -- mode == "SPL" port:write"S" -- so this write is immediate -- Loop reading the SPL samples and watching for loud -- noises, keeping a log of recent samples as we go. -- When a loud noise is heard, keep logging for a bit -- before sending email. Include the log in the email. local spl, spl0 local triggered = false local eek = "" while true do local line = port:read"*l" -- read a single line spl = tonumber(line) if spl then spl = 0.75 * spl io.write("SPL "..(triggered and "TRG " or " "), spl, "\r") recent:add(spl) -- Watch for the trigger condition: 12 dB louder if not triggered and spl0 and (spl - spl0 > 12.) then triggered = math.floor(recent.depth * 2. / 3.) eek = "EEK! "..spl.." dB" end if triggered then triggered = triggered - 1 if triggered == 0 then triggered = false local data = recent:CSVtext(false) local pngfile = "tmp-SPL.png" plot.plot(data, pngfile, recent.depth/30., -- 1/3 trigger pos, 1/10 for time scale "Startled at "..isodate()) local body = eek.."\n"..data print("email " .. addr, eek, isodate()) mpack(addr, eek, pngfile) end end spl0 = spl end end end