#!/usr/bin/python

# asterisk-phonepatch - Phonepatch for the Asterisk PBX

# Copyright (C) 2006 Arnau Sanchez
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

# Standard Python modules
import sys, os, optparse
import time, select, popen2
import audioop, signal, inspect
import tempfile, shutil, syslog

# External phonepatch modules
sys.path.append("/usr/lib/asterisk-phonepatch")
import templateparser, dtmf, radio
import radiocontrol, daemonize

__version__ = "$Revision: 1.2 $"
__author__ = "Arnau Sanchez <arnau@ehas.org>"
__depends__ = ['Asterisk', 'Sox', 'Festival', 'Python-2.4']
__copyright__ = """Copyright (C) 2006 Arnau Sanchez <arnau@ehas.org>.
This code is distributed under the terms of the GNU General Public License."""

###############################
###############################
class Phonepatch:
	"""A fully-configurable Radio-Phonepatch for the Asterisk PBX.
	
	The term "phonepatch" usually refers to the hardware device used to 
	connect a radio transceiver and a phoneline. This phonepatch takes 
	advantage of the <EAGI> Asterisk feature, and using <sox> as sound 
	converter and <festival> as text-to-speech syntetizer, provides a 
	powerful and fully configurable software-phonepatch. 
	
	It is only necessary to setup the hardware interface between computer and 
	radio, which involves audio (using the soundcard as D/A, A/D converter) 
	and PTT (Push-to-Talk). Please efer to Thomas Sailer's soundmodem project 
	for more information about that issue.

	This class has three possible modes, depending if it should run as an <AGI 
	incall>, <AGI outcall> or <Outcaller Daemon>, correspoding to incall(), 
	outcall() and daemon() methods. First, instance the <Phonepatch> class 
	and call one of these methods. Note that incall() and outcall() must be 
	run only as EAGI (Enhanced AGI) scripts (that's it, called from Asterisk),
	while the daemon() method should be run from command-line.	
	"""
		
	###################################
	def __init__(self, configuration, verbose=False):
		"""Configuration is a dictionary variable whose keys with sections:
		asterisk, soundcard, festival, telephony, dtmf, radio, incall and outcall
		"""
		self.configuration = configuration
		self.verbose = verbose
		self.modules_verbose = verbose
		
		# Global variables
		gconf = self.configuration["global"]
		self.pidfile = gconf["pidfile"]
		self.language = gconf["language"]
		self.sounds_dir = gconf["sounds"]
		self.outcalls_dir = gconf["spool"] 

		# Asterisk definitions
		self.asterisk_fifo = os.path.join("/tmp/php_fifo.raw")
		self.asterisk_samplerate = 8000
		self.asterisk_channels = 1
		
		# Audio buffer properties
		self.buffer_size = 1024
		self.sample_width = 2
		self.sample_max = 2.0**(self.sample_width*8) / 2.0
				
		# Parameters used to call sox later
		self.sox_pars = "-t raw -c%d -w -s" %self.asterisk_channels
		
		# Festival (audio-to-speech generator) options
		self.festival_gain = self.configuration["festival"]["audio_gain"]
		self.festival_isotolang = {"es": "spanish", "cy": "welsh"}
			
		# Configuration options
		self.festival_indicator = "@"
		self.samplerate = self.configuration["soundcard"]["samplerate"]
		
		# Initialize DTMF decoder
		self.dtmf_decoder = dtmf.Decoder(samplerate = self.samplerate, \
			channels = self.asterisk_channels, \
			sensibility = configuration["dtmf"]["sensibility"], \
			verbose = False)
		
		# state for verbose mode
		self.state = "phonepatch"
		self.background = False

		# Create signals dictionary: key = <signal_int> - value = <signal_name">
		self.signals = {}
		for var, value in inspect.getmembers(signal):
			if var.find("SIG") == 0 and var.find("SIG_") != 0: 
				self.signals[value] = var

	####################################
	def signal_handler(self, signum, frame):
		"""Signal handler for:
		
		SIGHUP: Newer versions of asterisk send it to kill the AGI (so catch to make a clean exit)
		SIGUSR1: Pause phonepatch daemon() function
		SIGUSR2: Continue phonepatch daemon() function
		SIGALRM: Continue phonepatch daemon() after reached the timeout
		SIGTERM/SIGKILL: Kill phonepatch daemon()
		"""
		signame = self.signals.get(signum, "unknown")
		self.debug("signal_handler - received %s" %signame)
		
		if signum == signal.SIGUSR1 and not self.sleep_signal:
			self.debug("signal_handler: phonepatch daemon paused")
			self.sleep_signal = True
		elif signum == signal.SIGUSR2 and self.stopped:
			self.debug("signal_handler: phonepatch daemon waked up")
			self.stopped = False
		elif signum == signal.SIGALRM and self.stopped:
			self.debug("signal_handler: phonepatch daemon timed out")
			self.stopped = False
		elif signum == signal.SIGHUP:
			if self.state == "daemon":
				self.debug("signal_handler: phonepatch daemon killed with SIGHUP")
				self.end_daemon()
				os._exit(0)
			self.debug("signal_handler: phonepatch stopped with SIGHUP")
		elif signum == signal.SIGTERM or signum == signal.SIGINT:
			self.debug("signal_handler: phonepatch daemon killed")
			self.end_daemon()
			os._exit(0)

	###############################
	def set_state(self, state):
		"""Set phonepatch state (incall/outcall/daemon)"""
		self.state = state
		if state: self.state_string = state
		else: self.state_string = ""
		self.debug("start of state: <%s>" %state)

	###############################
	def debug(self, log, exit = None, delete_pidfile = True):
		"""Print debug lines in verbose mode"""
		if not self.verbose: return
		if exit: log = "fatal error - " + log
		if self.state == "daemon" and self.background:
			syslog.syslog(log)
		else:
			log = "phonepatch[%s] - %s" %(self.state_string, log)
			sys.stderr.write(log + "\n")
			sys.stderr.flush()
		
		# Exit with code error if "exit" parameter given
		if exit != None: 
			if delete_pidfile: self.delete_pidfile()
			sys.exit(exit)
			
	###############################
	def command_output(self, command, input = None):
		"""Run a comand and capture standard output"""
		popen = popen2.Popen3(command)
		if input: popen.tochild.write(input)
		popen.tochild.close()
		buffer = popen.fromchild.read()
		popen.fromchild.close()
		popen.wait()
		return buffer

	###############################
	def agi_command(self, command):
		"""Send an AGI command to asterisk (written to stdout and flushed)"""
		self.debug("send AGI command: %s" %command)
		sc = command + "\n"
		sys.stdout.write(sc)
		sys.stdout.flush()
		
	###################
	def get_agi_keys(self, fd):
		"""Read the AGI keys given by asterisk (from stdin) -> dictionary"""
		self.debug ("reading AGI keys")
		
		# All AGI keys should begin with "agi_"
		begin_string = "agi_"
		
		# Init dictonary that will contain AGI keys
		agi_keys = {}
		while 1:
			line = fd.readline()
			if not line or not line.strip(): break
			line = line.strip()
			try: key, value = line.split(":")	
			except: continue
			if key.find(begin_string) != 0: continue
			key = key[len(begin_string):]
			agi_keys[key] = value.strip()
			#self.debug("%s = %s" %(key, agi_keys[key]))
		
		self.debug ("AGI keys: %d read" %len(agi_keys))
		self.agi_keys = agi_keys

	##################################
	def getpar(self, dictionary, *keys):
		"""Facility to bunch of keys from a dictionary and return them as list"""
		return [dictionary[x] for x in keys]

	###################################
	def open_radio(self):
		"""Open radio interface and configure PTT"""
		radioconf = self.configuration["radio"]
		soundcard = self.configuration["soundcard"]
		
		control = radioconf["radio_control"]
		self.radio_control = None
		if control != "off":
			param = control.split(":")
			if control.find("serial:") == 0:
				self.radio_control = radiocontrol.RadioControl("serial", param[1])
			elif control.find("parallel:") == 0:
				self.radio_control = radiocontrol.RadioControl("parallel",  param[1])
			elif control.find("command:") == 0:
				self.command_options = {"set_ptt_on": radioconf["ptt_on"], \
					"set_ptt_off": radioconf["ptt_off"],
					"get_carrier": radioconf["get_carrier"], 
					"get_carrier_response": radioconf["get_carrier_response"], }
				self.radio_control = radiocontrol.RadioControl("command",  param[1], self.command_options)
			else:
				self.debug("syntax error on radio_control: %s" %control, exit = 1)
	
		self.ptt, self.carrier_detection = radioconf["ptt"], radioconf["carrier_detection"]
		# Create radio instance (control soundcard and PTT)
		try:self.radio = radio.Radio(soundcard["device"], self.samplerate, self.asterisk_samplerate, \
			self.radio_control, self.ptt, self.carrier_detection, verbose=self.modules_verbose, \
			ptt_threshold_signal = radioconf["ptt_threshold_signal"], \
			ptt_tail_time = radioconf["ptt_tail_time"], \
			ptt_max_time = radioconf["ptt_max_time"], \
			ptt_wait_time = radioconf["ptt_wait_time"], 
			carrier_polling = radioconf["carrier_polling_time"], \
			fullduplex = radioconf["full_duplex"], \
			soundcard_retries = 5)
		except IOError, detail:
			self.debug("%s" %str(detail), 1)
			sys.exit(1)
		
		self.debug("soundcard opened: %s (%s sps)" %(soundcard["device"], self.samplerate))
		if self.radio_control:
			self.debug("radio control opened: %s" %control)
		
		# Phonepatch also uses audio_fd, so save it.
		self.audio_fd = self.radio.get_audiofd()
				
	###################################
	def open_interface(self, fifo_file):
		"""Opens phonepatch interface between Asterisk and radio:
		
		- Soundcard -> Phonepath FIFO -> Asterisk (GET DATA command)
		- Asterisk (file descriptor 3) > Soundcard
		"""
		# Way 1: soundcard -> php_fifo -> asterisk
		if os.path.exists(fifo_file):
			os.unlink(fifo_file)
		os.mkfifo(fifo_file)
		
		# GET DATA don't get extension file
		base_file = os.path.splitext(fifo_file)[0]
		command = 'GET DATA %s ""' %base_file
		
		try: self.agi_command(command)
		except: self.debug("error sending AGI command: %s" %command)
			
		# Open fifo (asterisk input) for writing
		self.asterisk_in = open(fifo_file, "w")

		# Way 2: asterisk (channel 3) -> soundcard
		self.asterisk_out = 3

	###################################
	def play_text(self, text):
		"""Sintetize text using Festival text-to-speech and return audio buffer"""
		
		# Festival only supports english, spanish and welsh. 
		# Get long name from ISO code
		audio_buffer = ""
		command = "festival"
		option = self.festival_isotolang.get(self.language, "")
		if option: command += " --language %s" %option

		# Write commands to festival stdin and read from stdout
		input = "(Parameter.set 'Audio_Method 'Audio_Command)\n"
		input += "(Parameter.set 'Audio_Required_Rate %d)\n" %self.asterisk_samplerate
		input += "(Parameter.set 'Audio_Command \"cat $FILE | sox -r $SR \
			-c1 -t raw -w -s - -v%d %s -\")\n" %(int(self.festival_gain), self.sox_pars)
		input += "(SayText \"%s\")\n" %text
		audio_buffer = self.command_output(command, input)
		self.debug("festival spawned: %s" %command)
		
		# Check that festival was succesfully run
		if not audio_buffer: 
			self.debug("festival error")
			return ""
		return audio_buffer
		
	###################################
	def play_file(self, audio_file):
		"""Load an audio file and returns audio buffer.
		
		If needed, resample to soundcard configured rate.
		Look for file in that order:
		1) @sounds_dir@/@language@
		2) @sounds_dir@
		"""
		audio_buffer = ""
		
		# Look for files
		for afile in [self.language, ""]:
			cfile = os.path.join(self.sounds_dir, afile, audio_file)
			if os.path.isfile(cfile):
				break
		else:
			self.debug("file not found: %s" %audio_file)
			return audio_buffer
		
		# Convert file to raw format with sox, so the soundcard can play it
		command = "sox %s -t raw -r%d %s -" %(cfile, self.asterisk_samplerate, self.sox_pars)
		self.debug("sox spawned: %s" %os.path.join(afile, audio_file))
		audio_buffer = self.command_output(command)
		if not audio_buffer: 
			self.debug("sox returned error")
			audio_buffer = ""
		return audio_buffer

	###################################
	def play(self, play_radio = False, play_asterisk = False, args = None, max_time = None, test_function = None, loop = None):
		"""Play either audio files or text (using festival) and returns bytes written.
		
		play_radio/play_asterisk: Play the sound to the radio and/or asterisk link.
		args: Comma separated string with files or text to play
		max_time: If defined, limit the maximum time to play audio
		test_function: If defined, this function is called every loop; if not succesful, giveup play
		"""
		# Some sanity checks
		if not play_radio and not play_asterisk or args == None: return
		if play_radio: self.debug("playing to soundcard: %s" %str(args))
		if play_asterisk: self.debug("playing to asterisk: %s" %str(args))
		try: last_playargs = self.last_playargs
		except: last_playargs = None
			
		# args: file | @texttospeech, separed by commas.
		if args == last_playargs: 
			# It's the same, so using cached audio buffer
			self.debug("using cached audio")
		else:
			# Load <args> (play_file() for audiofiles and play_text() for text-to-speech)
			self.raw_buffer = ""
			for option in args.split(","):
				if not option: continue
				try: option = option.strip().replace("%c", self.agi_keys["callerid"]).replace("%d", self.destination)
				except: pass
				if option[0] == self.festival_indicator:
					self.raw_buffer += self.play_text(option[len(self.festival_indicator):])
				else:
					self.raw_buffer += self.play_file(option)
			# If something went wrong, raw_buffer will have no data
			if not self.raw_buffer: 
				self.debug("audio buffer is empty, giving up audio play")
				return 0
			self.last_playargs = args
		
		# If <max_time> defined, calculate maximum amount of bytes to write
		buffer = self.raw_buffer
		written = 0
		
		if play_radio: self.radio.set_ptt(True)
			
		if max_time: 
			max_buffer = max_time * self.asterisk_samplerate * self.sample_width * self.asterisk_channels
			self.debug("playing time limited to %0.2f seconds" %max_time)
		else:
			t = float(len(self.raw_buffer)) / self.asterisk_samplerate
			self.debug("playing audio buffer (%0.2f seconds)" %t)

		while 1:
			if not buffer:
				if not loop: break
				buffer = self.raw_buffer
			if test_function and not test_function(): return
			if play_radio:
				try: self.radio.send_audio(buffer[:self.buffer_size], self.asterisk_samplerate)
				except: return
			if play_asterisk:
				try: self.asterisk_in.write(buffer[:self.buffer_size]); self.asterisk_in.flush()
				except: return
			buffer = buffer[self.buffer_size:]
			written += self.buffer_size
			if max_time and written >= max_buffer:
				break
		
		# Soundcards have internal buffers, make sure they are empty
		try: self.radio.flush_audio()
		except: pass
		
		# If playing to the radio, turn PTT off
		if play_radio: self.radio.set_ptt(False)
		return written
		
	#########################################
	def set_gain(self, buffer, audio_gain):
		"""Apply audio_gain to buffer"""
		if audio_gain == 1.0:
			return buffer
		return audioop.mul(buffer, self.sample_width, audio_gain)

	#########################################
	def audio_loop(self):
		"""Main loop for Asterisk<-> Radio interface
		
		configuration: dictionary containing incall or outcall configuration
		"""
		configuration = self.configuration["call"]
		if configuration["call_limit"]: time_limit = time.time() + configuration["call_limit"]
		else: time_limit = 0
		self.debug("start audio loop (device: %s)" %self.configuration["soundcard"]["device"])
		
		if configuration["hangup_button"]:
			self.debug("audio loop will hangup with DTMF button: %s" %configuration["hangup_button"])
		break_reason = None
		self.asterisk_timeout = 2.0
		
		asterisk_time = time.time() + self.asterisk_timeout
		while 1:
			try: retsel = select.select([self.asterisk_out, self.audio_fd], [], [])
			except: self.debug("select error"); break
			if not retsel: self.debug("select returned nothing"); break
			
			self.radio.update_carrier_state()
			now = time.time()
			if asterisk_time and now > asterisk_time:
				# Asterisk has been quiet for more than asterisk_timeout, disable PTT
				try: self.radio.flush_audio()
				except: pass
				self.debug("Turn PTT off due to asterisk inactivity")
				self.radio.set_ptt(False)				
				asterisk_time = 0
			
			if self.asterisk_out in retsel[0]:
				# Asterisk -> Radio (with VOX processing)
				asterisk_time = now + self.asterisk_timeout
				buffer = os.read(self.asterisk_out, self.buffer_size)
				if not buffer: self.debug("asterisk closed its read-descriptor"); break_reason = "asterisk"; break
				buffer = self.set_gain(buffer, self.configuration["radio"]["audio_gain"])
				self.radio.vox_process(buffer, self.asterisk_samplerate)
			
			if self.audio_fd in retsel[0]:
				# Radio -> Asterisk
				buffer = self.radio.read_audio(self.buffer_size, resample = True)
				if not buffer: self.debug("soundcard closed its descriptor"); break_reason = "radio"; break
				buffer = self.set_gain(buffer, self.configuration["telephony"]["audio_gain"])
				try: self.asterisk_in.write(buffer); self.asterisk_in.flush()
				except: self.debug("asterisk closed its writing descriptor"); break_reason = "asterisk"; break
				
				# If a <hangup_button> is configured, hangup line when received
				if configuration["hangup_button"]:
					keys = self.dtmf_decoder.decode_buffer(buffer)
					if keys: self.debug("DTMF keys received: %s" %("".join(keys)))
					if configuration["hangup_button"] in keys:
						self.debug("Hangup DTMF button received")
						self.play(True, True, configuration["end_audio"])
						break_reason = "user"
						break
			
			# If time_limit defined, close interface at that time
			if time_limit and time.time() >= time_limit:
				self.debug("call time-limit reached: %0.2f seconds" %configuration["call_limit"])
				self.play(True, True, configuration["end_audio"])
				break_reason = "timeout"
				break
		
		if break_reason == "asterisk":
			self.play(True, False, configuration["end_audio"])
		
		self.debug("end audio loop")

	###################################
	def close_interface(self):
		"""Close asterisk interface (FIFO)"""
		self.asterisk_in.close()
		os.unlink(self.asterisk_fifo)

	###################################
	def detect_dtmf(self, dtmf_key, timeout = None):
		"""Detect dtmf_key in audio from radio link and return it if found"""
		if not self.dtmf_decoder: return 
		if timeout: timeout_time = time.time() + timeout
		self.sleep_signal = False
		while 1:
			if self.sleep_signal and self.state == "daemon":
				self.sleep_daemon(self.configuration["call"]["call_limit"])
			self.radio.update_carrier_state()
			buffer = self.radio.read_audio(self.buffer_size)
			if not buffer: break
			keys = self.dtmf_decoder.decode_buffer(buffer)
			for key in keys: 
				self.debug("DTMF button received: %s" %key)
			if dtmf_key in keys: 
				return dtmf_key
			if timeout and time.time() >= timeout_time:
				break

	###################################
	def delete_pidfile(self):
		"""Delete pidfile after a daemon process has finished"""
		if not self.pidfile: return
		self.debug("deleting pidfile: %s" %self.pidfile)
		try: os.unlink(self.pidfile)
		except: pass

	###################################
	def create_pidfile(self):
		"""Create pidfile when a daemon process starts"""
		self.debug("creating pidfile: %s" %self.pidfile)
		try: fd = open(self.pidfile, "w")
		except: self.debug("pidfile could not be opened for writing", exit = 1, delete_pidfile = False)
		fd.write(str(os.getpid()) + "\n")
		fd.close()

	###################################
	def read_pidfile(self):
		"""Read pifile and return pid -> Integer"""
		fd = open(self.pidfile)
		pid = int(fd.readline().strip())
		fd.close()
		return pid

	###################################
	def signal_daemon(self, sig):
		"""Send sig signal to phonepatch daemon (read PID from pidfile)"""
		try: pid = self.read_pidfile()
		except:self.debug("cannot read pidfile, daemon not running" ); return
		try: os.kill(int(pid), sig)
		except OSError, detail: self.debug("kill operation error: %s" %detail); return
		signame = self.signals.get(sig, "unknown")
		self.debug("%s sent to process %s" %(signame, pid))
		
	###################################
	def process_incall(self):
		"""Waits for DTMF <answer_button> (with a timeout) and open the interface if received"""
		incall = self.configuration["incall"]
		timeout_time = time.time() +incall["report_call_timeout"]
		while 1:
			if self.play(True, True, incall["report_call_audio"]) == None: break
			answer_button = incall["answer_button"]
			self.debug("wait %d seconds for DTMF button %s" %(incall["report_call_audio_wait"], answer_button))
			#return 1 # temporal
			try: dtmf = self.detect_dtmf(answer_button, incall["report_call_audio_wait"])
			except IOError: break
			if dtmf:
				self.debug("DTMF answer button received")
				return 1
			elif time.time() > timeout_time:
				self.debug("incall timeout reached: %d seconds" %incall["report_call_timeout"])
				self.play(True, True, incall["report_call_timeout_audio"])
				break
		return 0

	###################################
	def continue_outcall(self):
		"""Callback function to test if an outcall is still active"""
		if os.path.exists(self.outcallfile) and not self.sleep_signal:
			return True
		return False

	###################################
	def make_call(self, outcall_configuration, number, reopen = True):
		"""Use outgoing calls Asterisk facility to call <number>"""
		channel = outcall_configuration["channel"] + "/" + str(number)
		
		# Create a temporal file to write outgoing call options
		tempfd, callpath = tempfile.mkstemp()
		oc = outcall_configuration
		options = [("Channel", channel), ("MaxRetries", oc["call_max_retries"]), \
			("RetryTime", oc["call_retry_time"]), ("Context", oc["context"]), \
			("Extension", oc["extension"]), ("WaitTime", oc["call_timeout"]), \
			("Priority", oc["call_priority"])]
		for key, value in options:
			buffer = "%s: %s" %(key, value)
			os.write(tempfd, buffer + "\n")
			self.debug("outcall - %s" %buffer)
		os.close(tempfd)
		
		# Now make the outcall and wait for asterisk response
		self.debug("making an outcall")
		shutil.move(callpath, self.outcalls_dir)
	
		# Save outcall spool file name on class, as callback continue_outcall() uses it
		self.outcallfile = os.path.join(self.outcalls_dir, os.path.basename(callpath))
		
		# Loop until spool file is processed or a <sleep_signal> received
		while 1:
			if self.play(True, False, oc["ring_audio"], max_time = oc["ring_audio_time"], \
				test_function = self.continue_outcall) == None: break
			etime = time.time() + oc["ring_audio_wait"]
			while time.time() < etime and self.continue_outcall():
				time.sleep(0.1)
			if not self.continue_outcall():
				break
		
		# If not a sleep_signal, Asterisk could not connect to peer
		if not self.sleep_signal:
			self.debug("Asterisk was unable to connect")
			self.play(True, False, oc["ring_timeout_audio"])
			return
		
		# Ok, we received the <sleep_signal>, time to sleep 
		self.debug("<sleep_signal> received, a Phonepatch AGI has been launched")
	
		self.sleep_daemon(self.configuration["call"]["call_limit"], reopen)
		
	###################################
	def set_signals(self, signals):
		"""Bind a list of signal to default <signal_handler>"""
		for sig in signals:
			signal.signal(sig, self.signal_handler)

	#####################################
	def sleep_daemon(self, sleep_time, reopen = True):
		self.debug("sleeping phonepatch until it receives a continue signal")
		self.radio.close()
		self.sleep_signal = False
		
		# Safe time gives some time extra to EAGI to finish
		safe_time = 5.0
		
		# If there is an <call_limit>, the EAGI should end in that time, but
		# for security set an alarm (with an extra <safe_time>) and wake up
		if sleep_time: 
			alarm_time = int(sleep_time + safe_time)
			signal.alarm(alarm_time)
			self.debug("sleep timeout set to %d secs" %sleep_time)
		
		# Wait for <continue_signal>, which will be sent by the outcall EAGI phonepatch
		self.stopped = True
		while self.stopped:
			time.sleep(0.1)
		
		signal.alarm(0)
		
		if reopen:
			self.debug("reopening phonepatch interface")
			self.open_radio()
		else:
			self.debug("reopening phonepatch function is disabled")

	#######################################
	def check_active(self):
		try: pid = self.read_pidfile()
		except: return
		
		# Check /proc info to check if it is really a xhotkeys process
		statfile = "/proc/%d/stat" % pid 
		try: fd = open(statfile)
		except IOError: self.debug("cannot read process status (%s)" %statfile); return
		name = fd.read().split()[1]
		if name.find("phonepatch") < 0 and name.find("asterisk-phone") < 0 :
			self.debug("pidfile found but not a phonepatch daemon, so deleting it")
			try: os.unlink(self.pidfile)
			except: self.debug("error deleting pidfile" %self.pidfile, ERROR)
			return
		return pid

	###################################
	def init_daemon(self):
		if self.background: 
			syslog.openlog("phonepatch", syslog.LOG_PID, syslog.LOG_DAEMON)
			self.modules_verbose = False
		# Check if outcall -> extension is configured (compulsory). If not, exit.		
		extension = self.configuration["outcall"]["extension"]
		if extension == None:
			self.debug("parameter <extension> (outcall section) must be configured", 1)
			
		pid = self.check_active()
		if pid: self.debug("phonepatch daemon is already runnning with pid %d" %pid, 1, delete_pidfile = False)

		# Init flag variables (pause and continue) and set signals
		self.sleep_signal = self.stopped = False
		self.set_signals([signal.SIGHUP, signal.SIGUSR1, signal.SIGUSR2, \
			signal.SIGALRM, signal.SIGTERM, signal.SIGINT])
	
	###################################
	def process_noisy_number(self, number, noisy_button):
		"""All repetitions between a noisy_button are 
		removed (and noisy_button itself)"""
		if not noisy_button or type(noisy_button) != str or len(noisy_button) != 1: 
			return number
		output = ""
		memory = None
		for n in number:
			if n == noisy_button and memory != None:
				output += memory
				memory = None
			elif n != noisy_button and memory != None and n != memory: 
				output += memory
				memory = n
			elif n != noisy_button and memory == None:
				memory = n
		if n != noisy_button:
			output = output + n
		return output

	###################################
	def loop_daemon(self):
		# Get outcall configuration
		outcall = self.configuration["outcall"]
		
		# Wait for <asktone> button, record number and make a call when received <call_button>
		while 1:
			self.debug("waiting for <askfortone_button>")
			key = self.detect_dtmf(outcall["askfortone_button"])
			if not key: break
			
			# AskForTone DTMF button received, now record the destination number
			self.play(True, False, outcall["tone_audio"], max_time = outcall["tone_audio_time"], loop = True)
			self.debug("waiting for number and <call_button>")
			timeout_time = time.time() + outcall["tone_timeout"]
			dtmf_keys = []
		
			while 1:
				if self.sleep_signal:
					self.sleep_daemon(self.configuration["call"]["call_limit"])
					break

				now = time.time()
				if now >= timeout_time:
					self.debug("wait for number timed out")
					self.play(True, False, outcall["tone_timeout_audio"])
					break
				buffer = self.radio.read_audio(self.buffer_size)
				if not buffer: break
				keys = self.dtmf_decoder.decode_buffer(buffer)
				dtmf_keys += keys
				for key in keys: self.debug("DTMF button received: %s (current number: %s)" %(key, "".join(dtmf_keys)))
				if outcall["clear_button"] in dtmf_keys:
					dtmf_keys = dtmf_keys[dtmf_keys.index(outcall["clear_button"])+1:]
					self.debug("<clear_button> received, reinit dial process")
					continue
				if outcall["call_button"] in dtmf_keys: 
					dtmf_keys = dtmf_keys[:dtmf_keys.index(outcall["call_button"])]
					if not dtmf_keys:
						self.debug("<call_button> received")
						self.debug("call number is empty: just restarting timeout")
						timeout_time = time.time() + outcall["tone_timeout"]
					else:
						# We have a number (in a list) to call to, convert to string
						number = "".join(dtmf_keys)						
						noisy_button = self.configuration["dtmf"]["noisy_mode_button"]
						number = self.process_noisy_number(number, noisy_button)
						self.debug("<call_button> received, making a call to %s" %number)
						self.make_call(outcall, number)
						dtmf_keys = []
						break

	###################################
	def end_daemon(self):
		try: self.radio.close()
		except: pass
		self.delete_pidfile()
		self.debug("<daemon> process ended")

	###################################
	### MAIN FUNCTIONS: incall(), outcall(), daemon()
	###################################
			
	###################################
	def daemon(self, background = False, testcall = None):
		"""Phonepatch acting as daemon.
		
		Listen from radio interface to see if radio-user wants to make a call.
		When an incall or outcall start, this process will be stopped by a signal
		"""
		self.background = background
		if self.background:	
			daemonize.daemonize()
		self.set_state("daemon")
		self.init_daemon()
		self.open_radio()
		self.create_pidfile()
		
		if testcall != None:
			self.make_call(self.configuration["outcall"], testcall, reopen = False)
			self.debug("test outcall ended")
			self.delete_pidfile()
			sys.exit(0)

		self.loop_daemon()	
		
	###################################
	def outcall(self):
		"""Phonepatch acting as outgoing caller"""
		self.set_state("outcall")
		self.set_signals([signal.SIGHUP])
		
		# Send a stop_signal (SIGUSR1) to the active phonepatch
		self.signal_daemon(signal.SIGUSR1)
		self.get_agi_keys(sys.stdin)
		self.open_radio()
		self.open_interface(self.asterisk_fifo)
		self.audio_loop()
		self.close_interface()
		self.radio.close()

		# The outcall has finished, tell the phonepatch daemon to continue
		self.signal_daemon(signal.SIGUSR2)
		self.debug("<outcall> process ended")

	###################################
	def incall(self, destination):	
		"""Phonepatch acting as call-receiver"""
		self.set_state("incall")
		self.set_signals([signal.SIGHUP])
		
		# It's likely that there is phonepatch daemon active, send a <stop_signal> to that process
		self.signal_daemon(signal.SIGUSR1)
		self.destination = destination
		self.get_agi_keys(sys.stdin)
		self.open_radio()
		self.open_interface(self.asterisk_fifo)

		if self.process_incall():
			self.audio_loop()

		self.close_interface()
		self.radio.close()
		
		# The incall has finished, signal the phonepatch daemon to continue
		self.signal_daemon(signal.SIGUSR2)
		self.debug("<incall> process ended")

###################################
def main():
	usage = """
phonepatch [options]

By default the phonepatch acts as daemon, opens radio interface 
and spawn outgoing calls when required (radio DTMF controlled)"""
	
	default_template = "/usr/share/asterisk-phonepatch/phonepatch.template"
	default_configuration = "/etc/asterisk-phonepatch/phonepatch.conf"
	
	optpar = optparse.OptionParser(usage)
	optpar.add_option('-q', '--quiet', dest='verbose', default = True, action='store_false', help = 'Be quiet (disable verbose mode)')
	optpar.add_option('-o', '--asterisk-outcall', dest='outcall', default = False, action = 'store_true', help = 'Start a phonepatch outcall communication (EAGI mode only)')
	optpar.add_option('-i', '--asterisk-incall',  dest='incall', type = "string", default = "", metavar = 'DESTINATION', help = 'Start a phonepatch incall communication (EAGI mode only)')
	optpar.add_option('-f', '--configuration-file',  dest='configuration_file', type = "string", default = default_configuration, help = 'Use configuration file')
	optpar.add_option('-b', '--background',  dest='background',  default = False, action = 'store_true', help = 'Run in background')
	optpar.add_option('-c', '--test-outcall',  dest='test_outcall', metavar = 'NUMBER', type = "string", help = 'Make an outcall test')
	
	options, args = optpar.parse_args()
	
	config = templateparser.Parser(verbose = True)

	config.read_template(default_template)
	try: configuration = config.read_configuration(options.configuration_file)
	except IOError, e: print "Error reading configuration file: %s" %options.configuration_file; sys.exit(1)

	# Run daemon (default), incall or outcall mode
	php = Phonepatch(configuration, verbose=options.verbose)
	if options.incall:
		destination = options.incall
		php.incall(destination)
	elif options.outcall:
		php.outcall()
	else:
		php.daemon(options.background, options.test_outcall)
		
	sys.exit(0)

##############################
## MAIN
#################

if __name__ == "__main__":
	main()
