#!/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, pwd, grp
import time, select, popen2, re
import audioop, signal, inspect
import tempfile, shutil, syslog, errno

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

__version__ = "$Revision: 1.5 $"
__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."""

# CTCSS Private Lines Codes (used by Motorola)
PL_CODES = {"XZ": 67.0, "WZ": 69.3, "XA": 71.9, "WA": 74.4, "XB": 77.0, "SP": 79.7, 
	"YZ": 82.5, "YA": 85.4, "YB": 88.5, "ZZ": 91.5, "ZA": 94.8, "ZB": 97.4, "1Z": 100.0,
	"1A": 103.5, "1B" : 107.2, "2Z": 110.9, "2A": 114.8, "2B": 118.8, "3Z": 123.0,
	"3A": 127.3, "3B": 131.8, "4Z": 136.5, "4A": 141.3, "4B": 146.2, "5Z": 151.4,
	"5A": 156.7, "5B": 162.2, "6Z": 167.9, "6A": 173.8, "6B": 179.9, "7Z": 186.2,
	"7A": 192.8, "M1": 203.5, "8Z": 206.5, "M2" : 210.7, "M3": 218.1, "M4": 225.7,
	"9Z": 229.2, "M5": 233.6, "M6": 241.8, "M7": 250.3, "0Z": 254.1}

OUTCALL_IDNAME = "_phpoutcall_"
GLOBAL_SECTION = "general"
PIDFILE_DIR = "/var/run/asterisk"
DEFAULT_SECTION = None

###############################
###############################
class Container:
	def __init__(self, **args):
		for arg in args:
			setattr(self, arg, args[arg])	
			
###############################
###############################
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
		self.set_state("phonepatch")
			
		self.phonepatch_extension = None
		self.phonepatch_php = None

		# 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 = 320
		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_isotolang = {"es": "spanish", "cy": "welsh"}
			
		# Configuration options
		self.festival_indicator = "@"
		
		# 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

		self.background = False
		self.genctcss = None
		self.interface = None

	####################################
	def signal_handler(self, signum, frame):
		"""Signal handler for:
		
		SIGHUP: Newer versions of asterisk send it to kill the AGI
		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 init_php(self, phonepatch):	
		# Initialize DTMF decoder
		if not phonepatch: self.debug("Phonepatch name not given", 1)
		if phonepatch not in self.configuration: self.debug("Phonepatch name not found in configuration: %s" %phonepatch, 1)
		self.phonepatch_php = phonepatch
		#self.samplerate = self.getconf("soundcard_samplerate")
		self.samplerate = self.asterisk_samplerate
		self.dtmf_decoder = dtmf.Decoder(samplerate = self.samplerate, \
			channels = self.asterisk_channels, \
			sensibility = self.getconf("dtmf_sensibility"), \
			verbose = False)
		
		self.pidfile = os.path.join(PIDFILE_DIR, self.phonepatch_php + ".pid")
		self.language = self.getconf("language")
		self.sounds_dir = self.getconf("sounds_dir")
		self.outcalls_dir = self.getconf("spool_dir") 
		self.festival_gain = self.getconf("festival_audio_gain")

	####################################
	def getconf(self, parameter, phpext=None):
		if not phpext:
			phpext = self.phonepatch_extension
		if phpext and parameter in self.configuration[phpext]:
			return self.configuration[phpext][parameter]
		php = self.phonepatch_php
		if php and parameter in self.configuration[php]:
			return self.configuration[php][parameter]
			
		if self.configuration.has_key(GLOBAL_SECTION) and parameter in self.configuration[GLOBAL_SECTION]:
			return self.configuration[GLOBAL_SECTION][parameter]
		if parameter in self.configuration[DEFAULT_SECTION]:
			value = self.configuration[DEFAULT_SECTION][parameter]
			self.debug("getconf: parameter not defined, returning default: %s=%s" %(parameter, value))
			return value
		self.debug("getconf: unknown parameter: %s" %parameter, 1)

	###############################
	def set_state(self, state):
		"""Set phonepatch state (incall/outcall/daemon)"""
		self.state = state
		if state: self.state_string = state
		else: self.state_string = ""

	###############################
	def debug(self, log, exit = None, delete_pidfile = True):
		"""Output 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)
			self.secure_os(sys.stderr.write, log + "\n")
			self.secure_os(sys.stderr.flush)
		
		# Exit with code error if "exit" parameter given
		if exit != None: 
			if delete_pidfile: self.delete_pidfile()
			sys.exit(exit)

	###############################
	def secure_os(self, method, *args):
		while 1:
			try: buffer = method(*args)
			except IOError, e:
				if e.errno == errno.EINTR: continue
				else: raise
			else: break
		return buffer

	###############################
	def command_output(self, command, input = None):
		"""Run a comand and capture standard output"""
		popen = popen2.Popen3(command)
		if input: self.secure_os(popen.tochild.write, input)
		popen.tochild.close()
		buffer = self.secure_os(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("agi_command: send AGI command: %s" %command)
		sc = command + "\n"
		self.secure_os(sys.stdout.write, sc)
		self.secure_os(sys.stdout.flush)
		
	###################
	def get_agi_keys(self, fd):
		"""Read the AGI keys given by asterisk (from stdin) -> dictionary"""
		self.debug ("get_agi_keys: 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("get_agi_keys: %s = %s" %(key, agi_keys[key]))
		
		self.agi_keys = agi_keys

	###################################
	def open_radio(self):
		"""Open radio interface and configure PTT"""
		
		self.ptt = self.carrier = self.radio_control = None
		control = self.getconf("radio_control")
		if control != "off" and (self.getconf("ptt") or self.getconf("carrier_detection") in ("on", "audio")):
			dtype, lines, device = re.findall("(serial|parallel|command)(.*):(.*)$", control)[0]
			try: dtype, lines, device = re.findall("(serial|parallel|command)(.*):(.*)$", control)[0]
			except: self.debug("open_radio: syntax error on radio_control: %s" %control, exit = 1)
			if lines and lines[0] == "[" and lines[-1] == "]": lines = [x.strip() for x in lines[1:-1].split(",")]
			else: lines = None
			if control.find("serial") == 0:
				self.radio_control = radiocontrol.RadioControl("serial", device, lines)
			elif control.find("parallel") == 0:
				self.radio_control = radiocontrol.RadioControl("parallel", device, lines)
			elif control.find("command") == 0:
				self.command_options = {"set_ptt_on": self.getconf("command_ptt_on"), \
					"set_ptt_off": self.getconf("command_ptt_off"),
					"get_carrier": self.getconf("command_get_carrier"), 
					"get_carrier_response": self.getconf("command_get_carrier_response"), }
				self.radio_control = radiocontrol.RadioControl("command", device, command_options=self.command_options)

			else:
				self.debug("open_radio: syntax error on radio_control: %s" %control, exit = 1)
			if self.getconf("ptt"):
				self.ptt = Container(set=self.radio_control.set_ptt, get=self.radio_control.get_ptt, \
					threshold = self.getconf("ptt_threshold_signal"), \
					tailtime = self.getconf("ptt_tail_time"), \
					maxtime = self.getconf("ptt_max_time"), \
					waittime = self.getconf("ptt_wait_time"))
			if self.getconf("carrier_detection") in ("on", "audio"):
				self.carrier = Container(type=self.getconf("carrier_detection"), \
					get=self.radio_control.get_carrier, \
					pollingtime=self.getconf("carrier_polling_time"),\
					threshold = self.getconf("carrier_threshold_signal"), \
					tailtime = self.getconf("carrier_tail_time"), \
					maxtime = self.getconf("carrier_max_time"), \
					waittime = self.getconf("carrier_wait_time"))

		# Create radio instance (control soundcard and PTT)
		try:self.radio = radio.Radio(self.getconf("soundcard_device"), self.asterisk_samplerate, \
			self.ptt, self.carrier, verbose=self.modules_verbose, fullduplex = self.getconf("full_duplex"), \
			soundcard_retries = 5, latency = self.getconf("soundcard_latency"), enable_ctcss=True)
		except IOError, detail:
			self.debug("open_radio: %s" %str(detail), 1)
			sys.exit(1)
			
		self.debug("open_radio: soundcard opened: %s (%s sps)" %(self.getconf("soundcard_device"), self.samplerate))
		if self.radio_control:
			self.debug("open_radio: 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("open_interface: 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
		
		self.interface = True
		
	###################################
	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("play_text: festival spawned: %s" %command)
		
		# Check that festival was succesfully run
		if not audio_buffer: 
			self.debug("play_text: festival error")
			return ""
		return audio_buffer
		
	###################################
	def play_file(self, audio_file):
		"""Load an audio file and returns audio buffer.
		
		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("play_file: 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("play_file: sox spawned: %s" %os.path.join(afile, audio_file))
		audio_buffer = self.command_output(command)
		if not audio_buffer: 
			self.debug("play_file: 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("play: playing to soundcard: %s" %str(args))
		if play_asterisk: self.debug("play: 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("play: 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"])
				except: pass
				try: option = option.strip().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("play: 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 max_time: 
			max_buffer = max_time * self.asterisk_samplerate * self.sample_width * self.asterisk_channels
			self.debug("play: playing time limited to %0.2f seconds" %max_time)
		else:
			t = float(len(self.raw_buffer)) / self.asterisk_samplerate
			self.debug("play: playing audio buffer (%0.2f seconds)" %t)
		
		txdelay = self.getconf("ptt_txdelay")
		if txdelay and play_radio: 
			txtime = time.time() + self.getconf("ptt_txdelay")
		pttflag = False
		while 1:
			if not buffer:
				if not loop: break
				buffer = self.raw_buffer
			if test_function and not test_function(): 
				return
			if txdelay and time.time() < txtime:
				continue
			if play_radio and self.ptt and not pttflag: 
				self.radio.set_ptt(True)
				pttflag = True
			if play_radio:
				try: self.radio.send_audio(buffer[:self.buffer_size], self.genctcss)
				except: self.debug("play: radio send_audio error"); return
			if play_asterisk:
				if not self.flush_asterisk(False) and not play_radio: break
				try: self.secure_os(self.asterisk_in.write, buffer[:self.buffer_size]); self.secure_os(self.asterisk_in.flush)
				except: self.debug("play: asterisk write error"); return
			else: self.flush_asterisk()

			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 and self.ptt and pttflag: 
			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 get_ctcss_freq(self, ctcss_id):
		if not ctcss_id or ctcss_id == "off": return
		if ctcss_id in PL_CODES: ctcss_freq = PL_CODES[ctcss_id]
		else: ctcss_freq = ctcss_id
		try: ctcss_freq = float(ctcss_freq)
		except: self.debug("get_ctcss_freq: invalid CTCSS frequency: %s" %(str(ctcss_id))); return
		return ctcss_freq

	###################################
	def flush_asterisk(self, write=True):
		if not self.interface: return True
		try: 
			retsel = select.select([self.asterisk_out], [], [], 0.2)[0]
			if self.asterisk_out not in retsel: return False
			buffer = self.secure_os(os.read, self.asterisk_out, self.buffer_size)
			if not buffer: return False
			if write:
				buffer = "\x00" * len(buffer)
				self.secure_os(self.asterisk_in.write, buffer)
				self.secure_os(self.asterisk_in.flush)
			return True
		except: return False

	###########################
	def empty_asterisk(self, t):
		timeout = time.time() + t
		while time.time() < timeout:
			retsel = select.select([self.audio_fd, self.asterisk_out], [], [])
			if self.asterisk_out in retsel[0]:
				buffer = self.secure_os(os.read, self.asterisk_out, self.buffer_size)

	#########################################
	def audio_loop(self):
		"""Main loop for Asterisk<-> Radio interface"""
		
		if self.getconf("call_limit"): 
			time_limit = time.time() + self.getconf("call_limit")
		else: time_limit = None
		self.debug("audio_loop: start audio loop (device: %s)" %self.getconf("soundcard_device"))		
		if self.getconf("hangup_button"):
			self.debug("audio_loop: hangup button: %s" %self.getconf("hangup_button"))
		break_reason = None
		asterisk_timeout = 2.0
		asterisk_time = time.time() + asterisk_timeout
		try: self.empty_asterisk(0.1)
		except: pass

		while 1:
			try: retsel = select.select([self.asterisk_out, self.audio_fd], [], [])
			except: self.debug("audio_loop: select error"); break
			if not retsel: self.debug("audio_loop: select returned nothing"); break
			now = time.time()
			if asterisk_time and now > asterisk_time:
				self.debug("audio_loop: asterisk inactivity")
				break_reason = "asterisk"
				break
			
			if self.asterisk_out in retsel[0]:
				# Asterisk -> Radio (with VOX processing)
				asterisk_time = now + asterisk_timeout
				buffer = self.secure_os(os.read, self.asterisk_out, self.buffer_size)
				if not buffer: self.debug("audio_loop: asterisk closed its read-descriptor"); break_reason = "asterisk"; break
				buffer = self.set_gain(buffer, self.getconf("radio_audio_gain"))
				self.radio.vox_toradio(buffer, self.genctcss)
				
			if self.audio_fd in retsel[0]:
				# Radio -> Asterisk
				buffer = self.radio.read_audio(self.buffer_size)
				if not buffer: self.debug("audio_loop: soundcard closed its descriptor"); break_reason = "radio"; break
				buffer = self.set_gain(buffer, self.getconf("telephony_audio_gain"))
				try: self.radio.vox_topeer(self.asterisk_in, buffer)
				except: self.debug("audio_loop: asterisk closed its writing descriptor"); break_reason = "asterisk"; break
								
				# If hangup_button is configured, hangup line when received
				if self.getconf("hangup_button"):
					keys = self.dtmf_decoder.decode_buffer(buffer)
					if keys: self.debug("audio_loop: DTMF keys received: %s" %("".join(keys)))
					if self.getconf("hangup_button") in keys:
						self.debug("audio_loop: hangup DTMF button received")
						break
			
			# If time_limit defined, close interface at that time
			if time_limit and time.time() >= time_limit:
				self.debug("audio_loop: call time-limit reached: %0.2f seconds" %self.getconf("call_limit"))
				break
		
		end_audio = self.getconf("end_audio")
		if break_reason == "asterisk":
			self.play(True, False, end_audio)
		elif break_reason == "timeout":
			self.play(True, True, end_audio)
		elif break_reason == "user":
			self.play(True, True, end_audio)

		self.debug("audio_loop: 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, need_ctcss=False):
		"""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.getconf("call_limit"))
			if not self.flush_asterisk(): raise IOError, "Asterisk closed descriptor"
			buffer = self.radio.read_audio(self.buffer_size)
			if not buffer: self.debug("detect_dmtf: no buffer from radio"); break
			if need_ctcss: self.radio.decode_ctcss(buffer)
			keys = self.dtmf_decoder.decode_buffer(buffer)
			for key in keys: 
				self.debug("detect_dtmf: DTMF button received: %s" %key)
			if dtmf_key in keys:				
				if need_ctcss and not self.check_ctcss():
					self.debug("detect_dtmf: CTCSS tone needed but not detected")
					continue
				return dtmf_key
			if timeout and time.time() >= timeout_time:
				self.debug("detect_dmtf: timeout reached")
				return

	###################################
	def detect_ctcss(self, ctcss, timeout=None):
		"""Wait for CTCSS tone and return if found"""
		if timeout: timeout_time = time.time() + timeout
		while 1:
			if not self.flush_asterisk(): raise IOError, "Asterisk closed descriptor"
			buffer = self.radio.read_audio(self.buffer_size)
			if not buffer: break
			self.radio.decode_ctcss(buffer)
			tone = self.radio.get_ctcss_tone()
			if tone: self.debug("detect_ctcss: CTCSS tone %0.1f detected" %tone)
			if tone == ctcss: return True
			if timeout and time.time() >= timeout_time: break
		return False

	###################################
	def delete_pidfile(self):
		"""Delete pidfile after a daemon process has finished"""
		try: pidfile = self.pidfile
		except: return
		if not pidfile: return
		try: os.unlink(self.pidfile)
		except OSError, e: 
			if e.errno != errno.ENOENT: raise
		else: self.debug("delete_pidfile: %s" %self.pidfile)

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

	###################################
	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("signal_daemon: cannot read pidfile, daemon not running" ); return
		try: os.kill(int(pid), sig)
		except OSError, detail: self.debug("signal_daemon: kill operation error: %s" %detail); return
		signame = self.signals.get(sig, "unknown")
		self.debug("signal_daemon: %s sent to process %s" %(signame, pid))
		
	###################################
	def check_ctcss(self, extension=None):
		tone = self.radio.get_ctcss_tone()
		if not tone: self.debug("check_ctcss: CTCSS not detected"); return
		if extension == None: extensions = [s for s in self.configuration if s and s.isdigit()]
		else: extensions = [extension]
		for section in extensions:
			freq = self.get_ctcss_freq(self.getconf("ctcss", section))
			if freq and freq == tone:
				self.debug("check_ctcss: CTCSS tone %0.1f found in phonepatch extension %s" %(tone, section))
				return section
		if extension == None:
			self.debug("check_ctcss: CTCSS tone %0.1f not found in any phonepatch extension" %tone)
		else: self.debug("check_ctcss: CTCSS tone %0.1f not found in phonepatch extension %d" %(tone, extension))

	###################################
	def process_incall(self):
		"""Waits for DTMF <answer_button> (with a timeout) and open the interface if received"""
		incall = self.getconf("incall")
		timeout_time = time.time() +self.getconf("incall_report_timeout")
		answer_button = self.getconf("incall_answer_button")
		incall_ctcss = self.getconf("incall_ctcss")
		if not incall_ctcss and self.getconf("incall_answer_mode") == "ctcss":
			self.debug("process_incall: incall_answer_mode=ctcss but incall_ctcss not enabled")
			return 
		if incall_ctcss and not self.genctcss:
			self.debug("process_incall: incall_ctcss enabled but destination has not CTCSS defined")
			return 
		while 1:
			# TODO: fullduplex
			if self.play(True, True, self.getconf("incall_report_audio")) == None: 
				self.debug("process_incall: play() ended abnormally")
				return
			if self.getconf("incall_answer_mode") == "open": 
				self.debug("process_incall: answer mode set to open, opening channel")
				return "answered"
			elif self.getconf("incall_answer_mode") == "ctcss":
				freq = self.genctcss[0]
				self.debug("process_incall: waiting for CTCSS tone %0.1f" %freq)
				try: ctcss_found = self.detect_ctcss(freq, self.getconf("incall_report_audio_wait"))
				except: self.debug("process_incall: detect_ctcss ended"); return
				if ctcss_found:
					self.debug("process_incall: extension CTCSS tone %0.1f detected" %freq)
					return "answered"
			else:
				if not answer_button: return "answered"
				self.debug("process_incall: waiting for DTMF button: %s" %answer_button)
				try: dtmf = self.detect_dtmf(answer_button, timeout=self.getconf("incall_report_audio_wait"), need_ctcss=incall_ctcss)
				except IOError: self.debug("process_incall: detect_dtmf ended"); return
				if dtmf:
					self.debug("process_incall: DTMF answer button received")
					return "answered"
			if time.time() > timeout_time:
				self.debug("process_incall: timeout reached: %d seconds" %self.getconf("incall_report_timeout"))
				self.play(True, True, self.getconf("incall_report_timeout_audio"))
				return

	###################################
	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, number, reopen=True):
		"""Use outgoing calls Asterisk facility to call <number>"""
		number = str(number)		
		extension = self.getconf("outcall_extension").replace("X", "")
		if extension == None: extension = ""
		else: extension = str(extension)
		outcall_ctcss = self.getconf("outcall_ctcss")
		
		if not outcall_ctcss and self.getconf("outcall_extension_mode") == "ctcss":
			self.debug("process_incall: outcall_extension_mode=ctcss but outcall_ctcss not enabled")
			return 
		if len(number) <= self.phonepatch_extension_length:
			self.debug("make_call: destination number (%s) length is insufficient" %number)
			return
		if self.getconf("outcall_extension_mode") == "dtmf":
			phonepatch_extension = number[:self.phonepatch_extension_length]
			asterisk_extension = extension + phonepatch_extension
			number = number[self.phonepatch_extension_length:]
		else: asterisk_extension = phonepatch_extension = extension
		
		if self.getconf("outcall_extension_mode") == "ctcss":
			phonepatch_extension = self.phonepatch_extension
			if not phonepatch_extension: 
				self.debug("make_call: phonepatch extension empty"); return
			asterisk_extension += phonepatch_extension
		if phonepatch_extension not in self.configuration:
			self.debug("make_call: phonepatch extension not defined: %s" %phonepatch_extension)
			return
		self.phonepatch_extension = phonepatch_extension
		phpname = self.getconf("phonepatch")
		if phpname != self.phonepatch_php:
			self.debug("make_call: phonepatch extension %s cannot use phonepatch %s (configured for %s)" %(phonepatch_extension, self.phonepatch_php, phpname))
			return
		if not self.getconf("outcall"):
			self.debug("make_call: phonepatch extension %s not allowed to make outcalls" %phonepatch_extension)
			return
		self.debug("make_call: asterisk: %s - phonepatch: %s - destination: %s" %(asterisk_extension, phonepatch_extension, number))
		channel = self.getconf("outcall_channel") + "/" + number
		try: callerid = ("CallerID", "%s <%s>" %(self.getconf("name"), asterisk_extension))
		except: 
			self.debug("make_call: name parameter not defined for phonepatch extension %s" %phonepatch_extension)
			return
		options = [("Channel", channel), ("MaxRetries", "0"), \
			("RetryTime", "60"), ("Context", self.getconf("outcall_context")), \
			("Extension", asterisk_extension), ("WaitTime", self.getconf("outcall_timeout")), \
			("Priority", self.getconf("outcall_priority")), ("Account", OUTCALL_IDNAME), callerid]
		# Create a temporal file to write outgoing call options
		tempfd, callpath = tempfile.mkstemp()

		for key, value in options:
			buffer = "%s: %s" %(key, value)
			os.write(tempfd, buffer + "\n")
			self.debug("make_call: outcall - %s" %buffer)
		os.close(tempfd)
		
		# Spool file must be owned by Asterisk
		uid, gid = self.get_asterisk_id()
		os.chown(callpath, uid, gid)
		
		# Now make the outcall and wait for asterisk response
		self.debug("make_call: start")
		callspool = os.path.join(self.outcalls_dir, os.path.basename(callpath))
		os.rename(callpath, callspool)
		
		# Save outcall spool file name on class object, 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
		# TODO: fullduplex
		while 1:
			if self.play(True, False, self.getconf("ring_audio"), max_time = self.getconf("ring_audio_time"), \
				test_function = self.continue_outcall) == None: break
			etime = time.time() + self.getconf("ring_audio_wait")
			
			# If hangup_button is configured, abort call if received
			while time.time() < etime and self.continue_outcall():
				"""
				Auto-dial call cannot be stopped
				if self.getconf("hangup_button"):
					buffer = self.radio.read_audio(self.buffer_size)
					if not buffer: self.debug("make_call: soundcard closed its descriptor"); break_reason = "radio"; break
					keys = self.dtmf_decoder.decode_buffer(buffer)
					if self.getconf("hangup_button") in keys:
						self.debug("make_call: hangup DTMF button received: aborting call")
						try: os.unlink(self.outcallfile)
						except: self.debug("make_call: error deleting callfile")
						return True
				else: time.sleep(0.1)
				"""
				time.sleep(0.1)
			else: continue
			break
			
		# If the sleep_signal was not received, Asterisk was unable to connect to peer
		if not self.sleep_signal:
			self.debug("make_call: Asterisk was unable to connect")
			return
		
		# Ok, we received the <sleep_signal>, time to sleep 
		self.debug("make_call: sleep_signal received, an AGI-Phonepatch has been launched")
		self.sleep_daemon(self.getconf("call_limit"), reopen)
		return True
		
	###################################
	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("sleep_daemon: sleeping phonepatch until it receives a continue signal")
		self.radio.close()
		if self.radio_control: self.radio_control.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_daemon: 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("sleep_daemon: reopening phonepatch interface")
			self.open_radio()

	#######################################
	def check_daemon(self):
		try: pid = self.read_pidfile()
		except: return
		# Check /proc info to check if it is really a phonepatch daemon running
		statfile = "/proc/%d/stat" % pid 
		try: fd = open(statfile)
		except IOError: self.debug("check_daemon: 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("check_daemon: pidfile found but not a phonepatch daemon, so deleting it")
			try: os.unlink(self.pidfile)
			except: self.debug("check_daemon: error deleting pidfile" %self.pidfile, ERROR)
			return
		return pid

	###################################
	def get_asterisk_id(self):
		return pwd.getpwnam("asterisk")[2:4]

	###################################
	def init_daemon(self):
		if self.background: 
			syslog.openlog("phonepatch", syslog.LOG_PID, syslog.LOG_DAEMON)
			self.modules_verbose = False
		
		extension = self.getconf("outcall_extension")
		if not extension:
			self.debug("init_daemon: parameter extension must be configured", 1)
		pid = self.check_daemon()
		if pid: self.debug("init_daemon: phonepatch daemon is already runnning with pid %d" %pid, 1, delete_pidfile=False)
		try: 
			xindex = extension.index("X")
			self.phonepatch_extension_length = len(extension) - xindex
			extension = extension[:xindex]
		except: self.phonepatch_extension_length = 0
			
		# 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])
		self.create_pidfile()

		uid, gid = self.get_asterisk_id()
		asterisk_groups = [x[2] for x in grp.getgrall() if "asterisk" in x[3]]
		os.setgroups([gid] + asterisk_groups)
		os.setregid(uid, uid)
		os.setreuid(gid, gid)
		
	###################################
	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):
		# Wait for <asktone> button, record number and make a call when received <outcall_button>
		outcall_ctcss = self.getconf("outcall_ctcss")
		while 1:
			self.debug("loop_daemon: waiting for askfortone_button")
			try: key = self.detect_dtmf(self.getconf("askfortone_button"), need_ctcss=outcall_ctcss)
			except IOError: self.debug("loop_daemon: detect_dtmf ended"); break
			if not key: break
			
			# Set CTCSS for output if enabled
			if outcall_ctcss:
				self.phonepatch_extension = self.check_ctcss()
				self.set_ctcss(self.getconf("ctcss"), self.getconf("ctcss_amplitude"))
				self.debug("loop_daemon: using CTCSS tone: %0.1f Hz, amplitude: %0.2f" %self.genctcss)
			
			# AskForTone DTMF button received, now record the destination number
			# TODO: Fullduplex
			self.play(True, False, self.getconf("tone_audio"), max_time=self.getconf("tone_audio_time"), loop=True)
			self.debug("loop_daemon: waiting for number and <outcall_button>")
			timeout_time = time.time() + self.getconf("tone_timeout")
			dtmf_keys = []
		
			while 1:
				if self.sleep_signal:
					self.sleep_daemon(self.getconf("call_limit"))
					break

				now = time.time()
				if now >= timeout_time:
					self.debug("loop_daemon: wait for number timed out")
					self.play(True, False, self.getconf("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("loop_daemon: DTMF button received: %s (current number: %s)" %(key, "".join(dtmf_keys)))
				if self.getconf("clear_button") in dtmf_keys:
					dtmf_keys = dtmf_keys[dtmf_keys.index(self.getconf("clear_button"))+1:]
					self.debug("loop_daemon: clear_button received, restart dial process")
					continue
				if self.getconf("outcall_button") in dtmf_keys: 
					dtmf_keys = dtmf_keys[:dtmf_keys.index(self.getconf("outcall_button"))]
					if len(dtmf_keys) <= self.phonepatch_extension_length:
						self.debug("loop_daemon: outcall_button received, but number length insufficient")
						timeout_time = time.time() + self.getconf("tone_timeout")
					else:
						# We have a number (in a list) to call to, convert to string
						number = "".join(dtmf_keys)						
						noisy_button = self.getconf("dtmf_noisy_mode_button")
						if noisy_button and noisy_button != "off":
							number = self.process_noisy_number(number, noisy_button)
						self.debug("loop_daemon: outcall_button received, making a call to %s" %number)
						if not self.make_call(number):
							self.play(True, False, self.getconf("ring_timeout_audio"))
						dtmf_keys = []
						break

	###################################
	def end_daemon(self):
		try: self.radio.close()
		except: self.debug("end_daemon: error closing radio")
		self.delete_pidfile()
		self.debug("end_daemon: daemon ended")

	###################################
	def set_ctcss(self, ctcss_id, ctcss_amplitude):
		freq = self.get_ctcss_freq(ctcss_id)
		if freq:
			self.genctcss = (freq, ctcss_amplitude)
			self.debug("Using CTCSS tone: %0.1f Hz, amplitude: %0.2f" %self.genctcss)
		else: self.genctcss = None

	###################################
	### MAIN FUNCTIONS: agicall(), incall(), outcall(), daemon()
	###################################

	###################################
	def agicall(self, extension):
		"""Phonepatch acting as AGI"""
		self.set_state("agicall")
		self.get_agi_keys(sys.stdin)
		extensionagi = self.agi_keys["extension"]
		if not extension:
			self.debug("agicall: warning: phonepatch extension not given as first parameter, using the AGI key instead (%s) as Asterisk extension" %extension)
			extension = extensionagi
		if extension not in self.configuration:
			self.debug("agicall: phonepatch extension not defined: %s" %extension, 1)
		self.phonepatch_extension = extension
		self.init_php(self.getconf("phonepatch"))
		self.set_ctcss(self.getconf("ctcss"), self.getconf("ctcss_amplitude"))
		if self.agi_keys.get("accountcode") == OUTCALL_IDNAME: 
			self.outcall()
		else: self.incall()
		

	###################################
	def daemon(self, phonepatch, 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
		self.init_php(phonepatch)
		if not self.getconf("outcall_daemon"):
			return
		if self.background:	
			pid = daemonize.daemonize(return_child=True)
			if pid: return
		self.set_state("daemon")
		self.init_daemon()
		self.open_radio()
		
		if testcall != None:
			if self.getconf("outcall_extension_mode") == "ctcss":
				self.phonepatch_extension = testcall[:self.phonepatch_extension_length]
				testcall = testcall[self.phonepatch_extension_length:]
			if not self.make_call(testcall, reopen=False):
				self.play(True, False, self.getconf("ring_timeout_audio"))
			self.debug("daemon: test outcall ended")
			self.delete_pidfile()
			sys.exit(0)

		self.loop_daemon()
		return True

	###################################
	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.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("daemon: outcall process ended")

	###################################
	def incall(self):	
		"""Phonepatch acting as call-receiver"""
		self.set_state("incall")
		if not self.getconf("incall"):
			self.debug("incall: phonepatch extension %s not allowed to receive incalls" %(self.phonepatch_extension))
			return
		self.destination =  self.getconf("name")
		self.set_signals([signal.SIGHUP])
		
		# Send a <stop_signal> to daemon process (if running)
		self.signal_daemon(signal.SIGUSR1)
		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] [extension]

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.conf.template"
	default_configuration = "/etc/asterisk/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('-f', '--configuration-file',  dest='configuration_file', type = "string", default = default_configuration, help = 'Use configuration file')
	optpar.add_option('-p', '--phonepatch',  dest='phonepatch',  metavar = 'NAME', default="", type = "string", help = 'Use phonepatch in foreground mode')
	optpar.add_option('-c', '--test-outcall',  dest='test_outcall', metavar = 'NUMBER', type = "string", help = 'Make an outcall test')
	optpar.add_option('-b', '--background',  dest='background',  default = False, action = 'store_true', help = 'Run in background')

	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: sys.stderr.write("configuration file not found: %s\n" %options.configuration_file); sys.exit(1)

	# Run daemon (default), incall or outcall mode
	execname = os.path.basename(sys.argv[0])
	if execname == "phonepatch.agi": 
		if len(args) < 1: extension = None
		else: extension = args[0]
		php = Phonepatch(configuration, verbose=options.verbose)
		php.agicall(extension)
	else: 
		if options.phonepatch:
			phonepatchs = [options.phonepatch]
		else:
			phonepatchs = [s for s in configuration if s and not s.isdigit() and s != GLOBAL_SECTION]
			if not phonepatchs: sys.stderr.write("no phonepatchs found in configuration\n"); sys.exit(1)
			if not options.background: 
				phonepatchs.sort()
				phonepatchs = [phonepatchs[0]]
		for phpname in phonepatchs:
			php = Phonepatch(configuration, verbose=options.verbose)
			if not php.daemon(phpname, options.background, options.test_outcall):
				sys.stdout.write("outcall daemon disabled for phonepatch: %s\n" %phpname)
				sys.stdout.flush()
		
	sys.exit(0)

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

if __name__ == "__main__":
	main()
