Documentation
#! 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