Сегодня речь о голосовом меню (IVR) для маршрутизаторов Cisco, которое мы будем писать на языке TCL, и подключать на Cisco 3845.

Итак, для начала давайте разберемся в азах


Cisco начиная с версии IOS 12 поддерживает как VXML так и TCL скрипты для работы с голосовым меню. Однако, в отличии от VXML, скрипты на TCL имеют гораздо больше возможностей взаимодействия с Cisco IVR API. Так же существует возможность подключать гибридные IVR скрипты, со встроенными кусками VXML кода внутри TCL скрипта.

Все документы, связанные с IVR от Cisco, которые мне довелось получить можно скачать здесь.

FSM


Первое, что при изучении голосового меню от Cisco мне было очень трудно понять, дак это FSM переходы.
Выглядит это примерно так:
set ivr_fsm(CALLCOMES,ev_setup_indication) "act_Setup same_state"

Переходов таких может быть сколько угодно, и расположены они в конце TCL скрипта.

Давайте разберемся, что это вообще такое.
Общий синтаксис этой команды таков:
set array(CURRSTATE,curr_event) “act_proc NEXTSTATE”

где:
array – это имя FSM массива.
CURRSTATE – имя текущего состояния, при котором получено событие curr_event.
act_proc – имя функции, которую необходимо запустить при поступлении события curr_event в состоянии CURRSTATE.
NEXTSTATE – имя состояния, которое установится после запуска act_proc.

Другими словами, FSM это маркер, по которому Cisco сравнивает полученное от API событие с curr_event и текущий статус с CURRSTATE, если в каком либо FSM переходе они описаны, вызывается процедура act_proc и состояние изменяется на NEXTSTATE.

Самое главное в этом — это то, что текущее событие и состояние сравниваются со всеми описанными FSM переходами одновременно. Т.е. для Cisco не имеет значения порядок, в котором расположены FSM переходы, все они обрабатываются сразу.

Функции


Второй момент, это сами функции, которые должны быть описаны до инициализации скрипта.

Здесь есть ряд моментов, не свойственных обычному языку ООП. Например, если у вас есть 2 команды:
media play leg_incoming $playng_files(noexist)
leg setup $numbers(ckp) callinfo leg_incoming

То по логике объектно-ориентированного (или не очень) языка программирования, у вас сначала выполнится: media play а после нее leg setup.

Здесь тоже выполнится в таком же порядке, но интерпретатор не будет дожидаться выполнения 1й команды, и сразу после запуска media play будет выполнена следующая команда. Окончание проигрывания обрабатывается уже за счет FSM переходов.

Назначение всех команд и состояний подробно описано в файле tcl_ivr_2.0_programming_guide, который вы можете скачать здесь, я поподробнее остановлюсь только на тех, которые буду использовать непосредственно в скрипте

1) Инициализация скрипта


Начало любого TCL IVR скрипта содержит процедуру init, в моем примере эта функция выглядит так:
proc init { } {
    puts "\n proc Init start"
    global param
}

Здесь по сути выполняется вывод на экран командой puts "..." и определение глобальной переменной param

Инициализация скрипта происходит после описания всех функций, и начинается с запуска функции init. На этом простые вещи закончились, дальше все гораздо интереснее.

Последней исполняемой строкой скрипта должна быть строка определения стартового FSM перехода и стартового состояния. В нашем случае это:
fsm define ivr_fsm CALLCOMES

Это значит, что имя FSM массива задано как ivr_fsm, и стартовое состояние CALLCOMES. С инициализацией закончим, дальше будет понятнее, что происходит (я надеюсь).

2) Приветствие


proc Play_Welcome { } {
	puts "\n\n IVR - proc Play_Welcome start \n\n"
	global playng_files
	global param
	global pattern
	global numbers
	global workingtime
	
	#Вызываем процедуру, где описаны все переменные
	init_perCallVars
	
	#Получаем время
	GetDate
	
	#В зависимости рабочее сейчас время или нет, устанавливаем приветствие 
	if {$workingtime} {
	set after_welcome $playng_files(takenumber)	
	} else {
	set after_welcome $playng_files(noworking)
	}
	
	#Устанавливаем параметры подключения входящего вызова
	set param(interruptPrompt) true
	set param(abortKey) *
	set param(terminationKey) #	
	
	#Подключаем входящий вызов
	leg setupack leg_incoming
	leg proceeding leg_incoming
	leg connect leg_incoming
	
	#Запускаем процедуру сбора нажатых цифр со стороны звонящего
	leg collectdigits leg_incoming param pattern
	
	#Запускаем проигрыш файлов звонящему абоненту, после их окончания 
	#начнет действовать таймер param(interDigitTimeout), по истечении которого 
	#Произойдет событие ev_collectdigits_done
	media play leg_incoming %s500 $playng_files(welcome) $after_welcome $playng_files(onhold)
	
	#Запускаем таймер, по истечении которого произойдет событие ev_named_timer
	timer start named_timer $numbers(waiting_time) t1
}

Здесь довольно подробно все описано.

Результатом выполнения данной процедуры будет подключение входящей линии к Cisco за счет команд leg setupack, leg proceeding, leg connect, и проигрыш музыкальных файлов по очереди во входящую линию за счет команды media play leg_incoming.
Тут же запускается процесс сбора нажатых клавиш leg collectdigit и таймер командой timer start.

И проверяется рабочее сейчас время или нет, вызывая функцию GetDate:
proc GetDate { } {
	global workingtime
	
	#Час
	set houris [clock format [clock seconds] -format %H]
	#День недели
	set dayis [clock format [clock seconds] -format %A]
	#Проверяем рабочее время
	if {$houris > 17 || $houris < 8 || $dayis=="Sunday" || $dayis=="Saturday"} {
	set workingtime 0
	} else {
	set workingtime 1
	}
}

В зависимости от того рабочее время или нет мы меняем музыкальный файл, который будет проигран звонящему абоненту.

Так как стартовое состояние задано в нашем случае как fsm define ivr_fsm CALLCOMES, в него попадают сразу 3 FSM:
set ivr_fsm(CALLCOMES,ev_setup_indication)    "Play_Welcome,        same_state"
set ivr_fsm(CALLCOMES,ev_collectdigits_done)  "CheckDestanation,  same_state"
set ivr_fsm(CALLCOMES,ev_named_timer)          "GoToReception,      same_state"

Событие ev_setup_indication произойдет при поступлении звонка, и будет запущена процедура Play_Welcome, в которой описан старт процесса сбора нажатых цифр и старт таймера.

После окончания проигрывания музыки абоненту, начнется обратный отчет таймера, который задается параметром param(initialDigitTimeout) (который можно было задать чуть выше строкой set param(initialDigitTimeout) 15 и установить значение 15 секунд), т.к. он у нас не указан, его стандартное значение 10 секунд, после чего скрипт получит событие ev_collectdigits_done, при наступлении которого, как мы описали в FSM переходе, будет выполнена функция CheckDestanation.

Таймер, запущенный в Play_Welcome командой:
#Тип таймера named_timer, длительность, взята из переменной numbers(waiting_time), имя таймера t1
timer start named_timer $numbers(waiting_time) t1

После своего окончания сгенерирует событие ev_named_timer, которое будет обработано следующим FSM переходом:
set ivr_fsm(CALLCOMES,ev_named_timer)          "GoToReception,      same_state"

и вызовется процедура GoToReception.

3) Проверка введенного номера


proc CheckDestanation { } {
	puts "\n\n IVR - proc CheckDestanation start \n\n"
	global playng_files
	global numbers
	global digit
	#Останавливаем проигрыш медиа
	media stop leg_incoming
	
	#Определяем значение переменным
	set status [infotag get evt_status]
	set digit [infotag get evt_dcdigits]
	
	#Сравниваем полученные цифры и статусы
	#Если введенная цифра соответствует той, что задана в $numbers(fast_reception), 
	#изменяем  digit  на номер ресепшн и передаем $digit в функцию CheckCallersAndConnect,
	# предварительно изменив статус на CALLCONNECTED,
	# благодаря которому, при наступлении события ev_setup_done (подключение к номеру секретаря)
	# будет отработана процедура CallIsConnect
	if {$digit == $numbers(fast_reception)} {
		puts "\n\n IVR - proc CheckDestanation digit = $digit\nGoing to next reception \n\n"
		fsm setstate CALLCONNECTED
		set digit $numbers(reception)
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если введенная цифра соответствует той, что задана в $numbers(fast_ckp), подключаем на ЦКП
	#через CheckCallersAndConnect
	} elseif {$digit == $numbers(fast_ckp)} {
		puts "\n\n IVR - proc CheckDestanation digit = $digit\nGoing to next CKP \n\n"
		fsm setstate CALLCONNECTED
		set digit $numbers(ckp)
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если введенная цифра соответствует той, что задана в $numbers(fast_fax), подключаем на факс
	#через CheckCallersAndConnect
	} elseif {$digit == $numbers(fast_fax)} {
		puts "\n\n IVR - proc CheckDestanation digit = $digit\nGoing to next fax \n\n"
		fsm setstate CALLCONNECTED
		set digit $numbers(fax)
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если статус = cd_004 (введены корректные цифры номера) - подключаем к нужному номеру 
	#через CheckCallersAndConnect
	} elseif {$status == "cd_004"} {
		puts "\n\n IVR - proc CheckDestanation status = $status digit = $digit \n\n"
		fsm setstate CALLCONNECTED
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если статус = cd_005 (совпадение с dial plan) - подключаем к нужному номеру 
	#через CheckCallersAndConnect	
	} elseif {$status == "cd_005"} {
		puts "\n\n IVR - proc CheckDestanation status = $status digit = $digit \n\n"
		fsm setstate CALLCONNECTED	
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit	
	#Если статус = cd_006 (набран не существующий номер) - играем в линию $playng_files(noexist)
	# и изменяем статус на TORECEPTION, при действии которого и наступлении события 
	#ev_media_done (конец проигрывания звукового файла) вызовется процедура Play_TakeNumber
	} elseif {$status == "cd_006"} {
		puts "\n\n IVR - proc CheckDestanation status = $status digit = $digit \n\n"
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(noexist)
	#Во всех остальных случаях изменяем статус на TORECEPTION, при действии которого 
	#и наступлении события ev_media_done (конец проигрывания звукового файла) вызовется 
	#процедура GoToReception
	} else {
		#Проигрываем "Ваш вызов переадресовывается на секретаря"
		fsm setstate TORECEPTION
		media play leg_incoming $playng_files(toreception)		
		puts "\n\n IVR - proc CheckDestanation status = $status \n\n"
	}	
}

В процедуре CheckDestanation, которая будет вызвана после набора номера звонящим абонентом, мы сравниваем полученные при наборе цифры с настройками и переводим скрипт в соответствующее состояние командой fsm setstate.

Все состояния, попавшие в функцию, попадают под следующие FSM переходы:
set ivr_fsm(CALLCONNECTED,ev_setup_done)   "CallIsConnect,  same_state"
set ivr_fsm(TORECEPTION,ev_media_done)           "GoToReception,  same_state"
set ivr_fsm(TRYAGAIN,ev_media_done)           "Play_TakeNumber,  TRYING"
set ivr_fsm(TRYING,ev_collectdigits_done)  "CheckDestanation,  same_state"
set ivr_fsm(TRYING,ev_named_timer)   "GoToReception,  same_state"

Давайте по порядку.

1) Итак, изначально функция CheckDestanation вызывается после окончания процедуры сбора нажатия клавиш.
2) Информацию о нажатых клавишах мы записываем в переменную digit с помощью команды set digit [infotag get evt_dcdigits]
Аналогично записываем состояние линии в переменную status
3) Затем сравниваем полученные результаты с заданными переменными и изменяем состояние скрипта при совпадении:
if {$digit == $numbers(fast_reception)} {
 puts "\n\n IVR - proc CheckDestanation digit = $digit\nGoing to next reception \n\n"
 fsm setstate CALLCONNECTED
 leg setup $numbers(reception) callinfo leg_incoming
}


4) Проверка номера звонящего абонента


proc CheckCallersAndConnect {digit} {
 puts "\n\n IVR - proc CheckCallersAndConnect start \n\n"
 
 set callernumber [infotag get leg_ani]
 
 switch $callernumber {
  "9120000000" {set callInfo(displayInfo) "Director(mobile)"}
  "9130000000" {set callInfo(displayInfo) "Buhgalter(mobile)"}  
  default {} 
 }
 puts "\n\n IVR - caller is $callernumber connect with $digit\n\n"
 
 leg setup $digit callInfo leg_incoming
}

Данная функция позволяет изменить поле, отвечающее за написание имени звонящего. Просто ради эстетики, будет приятнее, когда на телефоне будет написан не только номер но и ID абонента. После изменения ID абонента происходит подключение линии к требуемому номеру.

5) Подключение номера


proc CallIsConnect { } {
	puts "\n\n IVR - proc CallIsConnect start \n\n"
	global playng_files	

	#Определяем чему равен status
	set status [infotag get evt_status]
	
	#Если статус равен ls_000 (успешное соединение с требуемым номером), изменяем состояние на CALLACTIVE
	if {$status == "ls_000"} {
	fsm setstate CALLACTIVE	
	
	#Если статус равен ls_002 (никто не ответил на звонок), запускаем процедуру запроса номера
	} elseif {$status == "ls_002"} {
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(noanswer)
	#Если статус - неверный номер, запускаем процедуру запроса номера
	} elseif {$status == "ls_004" || $status == "ls_005" || $status == "ls_006"} {
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(noexist)
	#Если статус равен ls_007 (абонент занят), запускаем процедуру запроса номера
	} elseif {$status == "ls_007"} {
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(busy)
	}
}

Данная функция вызывается следующим FSM переходом:
set ivr_fsm(CALLCONNECTED,ev_setup_done)   "CallIsConnect,  same_state"

Событие ev_setup_done наступает после подключения звонящего к требуемой линии.

6) Повторный запрос номера


proc Play_TakeNumber { } {
    puts "\n\n IVR - proc Play_TakeNumber start \n\n"
	global playng_files
	global numbers
	global param
	global pattern
	
	#Проверяем какой раз абонент пытается набрать номер
	if {$numbers(cur_try) <= $numbers(max_try)} {
	puts "\n\n IVR - proc Play_TakeNumber current try is: $numbers(cur_try) \n\n"
	incr numbers(cur_try)

	#Запускаем процедуру сбора нажатых цифр со стороны звонящего
	leg collectdigits leg_incoming param pattern

	#Запускаем проигрыш файлов
	media play leg_incoming $playng_files(takenumber)

	#Запускаем таймер, по истечении которого произойдет событие ev_named_timer
	timer start named_timer $numbers(waiting_time) t1
	
	#Если попытка больше чем $numbers(max_try) - разъединяем
	} else { 
		fsm setstate CALLDISCONNECTED
		media play leg_incoming $playng_files(callafter)	
	}
}

Данная функция проверяет какой раз ошибается звонящий, и если значение меньше чем $numbers(max_try) просит ввести номер еще раз.

Данная функция вызывается следующими FSM:
set ivr_fsm(TRYAGAIN,ev_media_done)         		"Play_TakeNumber, 	TRYING"
set ivr_fsm(TRYING,ev_collectdigits_done)			"CheckDestanation, 	same_state"
set ivr_fsm(TRYING,ev_named_timer)				"GoToReception, 	        same_state"


7) Разрыв соединения


proc AbortCall { } {
 puts "\n\n IVR - proc AbortCall start \n\n"
 call close
}

Вызывается следующими FSM:
set ivr_fsm(any_state,ev_disconnected)      "AbortCall,   same_state"
set ivr_fsm(CALLACTIVE,ev_disconnected)     "AbortCall,  CALLDISCONNECTED"
set ivr_fsm(CALLDISCONNECTED,ev_disconnected)   "AbortCall,  same_state"
set ivr_fsm(CALLDISCONNECTED,ev_media_done)    "AbortCall,  same_state"
set ivr_fsm(CALLDISCONNECTED,ev_disconnect_done)  "AbortCall,  same_state"


8) Подключение скрипта


Подключение на маршрутизаторе Cisco проходит в 2 этапа.
Первое, что нужно сделать это определить application:
application
 service voicemunu flash:voicemenu.tcl
  param allowed_pattern 5[5-7]..
  param fastto_reception 1
  param reception_number 5501
  param fastto_ckp 2
  param ckp_number 5604
  param fastto_fax 3
  param fax_number 5555
  param waiting_time 20
  param max_try 3
  param file_noanswer flash:en_noanswer.au
  param file_after flash:en_after.au
  param file_noexist flash:en_noexist.au
  param file_busy flash:en_busy.au
  param file_welcome flash:en_welcome.au
  param file_onhold flash:music-on-hold.au
  param file_noworking flash:en_takenumber2.au
  param file_takenumber flash:en_takenumber2.au

Второе, подключить service к dial-peer:
dial-peer voice 200 pots
 description -= ISP Beeline - INcoming call to number 3300100 =-
 service voicemunu
 incoming called-number 3300100

Таким образом, при поступлении звонка на номер 3300100, произойдет вызов нашего голосового меню voicemunu.

9) Полная версия скрипта


Выше были рассмотрены только основные функции скрипта, далее полный текст, имейте в виду, это практически самый простой вариант:
#######################################################
# Cisco IVR TCL script by Konovalov D.A. v.2
#######################################################
#
#	Для дебага скрипта
#		debug voip application script
#	Более полный дебага (не рекомендуется, может привести к перегрузке)
# 		debug voip ivr
# 
# Скрипт должен быть запущен со следующими параметрами:
# 	param allowed_pattern 5[5-7]..
# 	param fastto_reception 1
# 	param reception_number 5501
#	param fastto_ckp 2
# 	param ckp_number 5604
# 	param fastto_fax 3
# 	param fax_number 5555
# 	param waiting_time 20
# 	param max_try 3
# 	param file_welcome flash:en_welcome.au
# 	param file_takenumber flash:en_takenumber.au
# 	param file_after flash:en_after.au
# 	param file_busy flash:en_busy.au
# 	param file_noexist flash:en_noexist.au
# 	param file_noanswer flash:en_noanswer.au
# 	param file_onhold flash:music-on-hold.au
#	param file_noworking flash:music-on-hold.au

#Процедура инициализации скрипта
proc init { } {
    puts "\n proc Init start"
    global param
}

#Процедура с переменными
proc init_perCallVars { } {
	global pattern
	global numbers
	global playng_files
	
	#####Допустимая нумерация
	#Если в параметрах скрипта не указана допустимая нумерация, будет установлено значение .... - 4 любых цифры
	if {[infotag get cfg_avpair_exists allowed_pattern]} {
		set pattern(1) [string trim [infotag get cfg_avpair allowed_pattern]]
		puts "\n\n IVR - Allowed pattern set as: $pattern(1) \n\n"
		} else {
			set pattern(1) ....
			puts "\n\n IVR - Allowed pattern set as DEFAULT: $pattern(1) \n\n"
			}	
	#####Номера
	#Секретарь. Если в параметрах скрипта не указан номер секретаря, номер будет установлен в 0000
	if {[infotag get cfg_avpair_exists reception_number]} {
		set numbers(reception) [string trim [infotag get cfg_avpair reception_number]]
		puts "\n\n IVR - reception number set as: $numbers(reception) \n\n"
		} else {			
			set numbers(reception) 0000
			puts "\n\n IVR - reception number set as DEFAULT: $numbers(reception) \n\n"
			}	
	#ЦКП 
	if {[infotag get cfg_avpair_exists ckp_number]} {
		set numbers(ckp) [string trim [infotag get cfg_avpair ckp_number]]
		puts "\n\n IVR - ckp number set as: $numbers(ckp) \n\n"
		} else {			
			set numbers(ckp) 0000
			puts "\n\n IVR - ckp number set as DEFAULT: $numbers(ckp) \n\n"
			}
	#Факс 
	if {[infotag get cfg_avpair_exists fax_number]} {
		set numbers(fax) [string trim [infotag get cfg_avpair fax_number]]
		puts "\n\n IVR - fax number set as: $numbers(fax) \n\n"
		} else {			
			set numbers(fax) 0000
			puts "\n\n IVR - fax number set as DEFAULT: $numbers(fax) \n\n"
			}			
	#Быстрый перевод на Ресепшн 
	if {[infotag get cfg_avpair_exists fastto_reception]} {
		set numbers(fast_reception) [string trim [infotag get cfg_avpair fastto_reception]]
		puts "\n\n IVR - fast to reception set as: $numbers(fast_reception) \n\n"
		} else {			
			set numbers(fast_reception) 1
			puts "\n\n IVR - fast to reception set as DEFAULT: $numbers(fast_reception) \n\n"
			}
	#Быстрый перевод на ЦКП 
	if {[infotag get cfg_avpair_exists fastto_ckp]} {
		set numbers(fast_ckp) [string trim [infotag get cfg_avpair fastto_ckp]]
		puts "\n\n IVR - fast to ckp set as: $numbers(fast_ckp) \n\n"
		} else {			
			set numbers(fast_ckp) 2
			puts "\n\n IVR - fast to ckp set as DEFAULT: $numbers(fast_ckp) \n\n"
			}
	#Быстрый перевод на факс 
	if {[infotag get cfg_avpair_exists fastto_fax]} {
		set numbers(fast_fax) [string trim [infotag get cfg_avpair fastto_fax]]
		puts "\n\n IVR - fast to fax set as: $numbers(fast_fax) \n\n"
		} else {			
			set numbers(fast_fax) 3
			puts "\n\n IVR - fast to fax set as DEFAULT: $numbers(fast_fax) \n\n"
			}
	#Время ожидания введения номера (должно быть больше времени проигрыша всех файлов приветствия)
	if {[infotag get cfg_avpair_exists waiting_time]} {
		set numbers(waiting_time) [string trim [infotag get cfg_avpair waiting_time]]
		puts "\n\n IVR - wait number set as: $numbers(waiting_time) \n\n"
		} else {			
			set numbers(waiting_time) 10
			puts "\n\n IVR - wait number set as DEFAULT: $numbers(waiting_time) \n\n"
			}
	#Количество попыток ввести правильный номер, прежде чем звонок будет переведен на секретаря
	if {[infotag get cfg_avpair_exists max_try]} {
		set numbers(max_try) [string trim [infotag get cfg_avpair max_try]]
		puts "\n\n IVR - max try set as: $numbers(max_try) \n\n"
		set numbers(cur_try) 0
		} else {			
			set numbers(max_try) 5
			puts "\n\n IVR - max try set as DEFAULT: $numbers(max_try) \n\n"
			set numbers(cur_try) 0
			}
	#####Музыкальные файлы, которые будут проигрываться		
	#Файл приветствия
	if {[infotag get cfg_avpair_exists file_welcome]} {
		set playng_files(welcome) [string trim [infotag get cfg_avpair file_welcome]]
		puts "\n\n IVR - file_welcome set as: $playng_files(welcome) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(welcome) %s1
			puts "\n\n IVR - file_welcome set as DEFAULT: $playng_files(welcome) \n\n"
			}
	#Файл запроса ввести требуемый номер
	if {[infotag get cfg_avpair_exists file_takenumber]} {
		set playng_files(takenumber) [string trim [infotag get cfg_avpair file_takenumber]]
		puts "\n\n IVR - file_takenumber set as: $playng_files(takenumber) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(takenumber) %s1
			puts "\n\n IVR - file_takenumber set as DEFAULT: $playng_files(takenumber) \n\n"
			}
	#Файл "Пожалуйста перезвоните позднее"
	if {[infotag get cfg_avpair_exists file_after]} {
		set playng_files(callafter) [string trim [infotag get cfg_avpair file_after]]
		puts "\n\n IVR - file_after set as: $playng_files(callafter) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(callafter) %s1
			puts "\n\n IVR - file_after set as DEFAULT: $playng_files(callafter) \n\n"
			}
	#Файл "Номер занят"
	if {[infotag get cfg_avpair_exists file_busy]} {
		set playng_files(busy) [string trim [infotag get cfg_avpair file_busy]]
		puts "\n\n IVR - file_busy set as: $playng_files(busy) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(busy) %s1
			puts "\n\n IVR - file_busy set as DEFAULT: $playng_files(busy) \n\n"
			}
	#Файл "Номер не существует"
	if {[infotag get cfg_avpair_exists file_noexist]} {
		set playng_files(noexist) [string trim [infotag get cfg_avpair file_noexist]]
		puts "\n\n IVR - file_noexist set as: $playng_files(noexist) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(noexist) %s1
			puts "\n\n IVR - file_noexist set as DEFAULT: $playng_files(noexist) \n\n"
			}
	#Файл "Соеденяю с секретарем/оператором"
	if {[infotag get cfg_avpair_exists file_toreception]} {
		set playng_files(toreception) [string trim [infotag get cfg_avpair file_toreception]]
		puts "\n\n IVR - file_toreception set as: $playng_files(toreception) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(toreception) %s1
			puts "\n\n IVR - file_toreception set as DEFAULT: $playng_files(toreception) \n\n"
			}
	#Файл "Номер не отвечает, перезвоните позднее"
	if {[infotag get cfg_avpair_exists file_noanswer]} {
		set playng_files(noanswer) [string trim [infotag get cfg_avpair file_noanswer]]
		puts "\n\n IVR - file_noanswer set as: $playng_files(noanswer) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(noanswer) %s1
			puts "\n\n IVR - file_noanswer set as DEFAULT: $playng_files(noanswer) \n\n"
			}
	#Файл музыки, которая будет проигрываться при ожидании
	if {[infotag get cfg_avpair_exists file_onhold]} {
		set playng_files(onhold) [string trim [infotag get cfg_avpair file_onhold]]
		puts "\n\n IVR - file_onhold set as: $playng_files(onhold) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(onhold) %s1
			puts "\n\n IVR - file_onhold set as DEFAULT: $playng_files(onhold) \n\n"
			}
	#Файл музыки, которая будет проигрываться В нерабочее время
	if {[infotag get cfg_avpair_exists file_noworking]} {
		set playng_files(noworking) [string trim [infotag get cfg_avpair file_noworking]]
		puts "\n\n IVR - file_noworking set as: $playng_files(noworking) \n\n"
		} else {
			#Если файл не найден, он будет заменен на тишину в 1мс
			set playng_files(noworking) %s1
			puts "\n\n IVR - file_noworking set as DEFAULT: $playng_files(noworking) \n\n"
			}
}

proc GetDate { } {
	global workingtime
	
	#Час
	set houris [clock format [clock seconds] -format %H]
	#День недели
	set dayis [clock format [clock seconds] -format %A]
	#Проверяем рабочее время
	if {$houris > 17 || $houris < 8 || $dayis=="Sunday" || $dayis=="Saturday"} {
	set workingtime 0
	} else {
	set workingtime 1
	}
}

#Процедура проигрыша приветствия
proc Play_Welcome { } {
    puts "\n\n IVR - proc Play_Welcome start \n\n"
	global playng_files
	global param
	global pattern
	global numbers
	global workingtime
	
	#Вызываем процедуру, где описаны все переменные
	init_perCallVars
	
	#Получаем время
	GetDate
	#В зависимости рабочее сейчас время или нет, устанавливаем приветствие 
	if {$workingtime} {
	set after_welcome $playng_files(takenumber)	
	} else {
	set after_welcome $playng_files(noworking)
	}
	
	#Устанавливаем параметры подключения входящего вызова
	set param(interruptPrompt) true
	set param(abortKey) *
	set param(terminationKey) #	
	
	#Подключаем входящий вызов
    leg setupack leg_incoming
    leg proceeding leg_incoming
    leg connect leg_incoming
	
	#Запускаем процедуру сбора нажатых цифр со стороны звонящего
	leg collectdigits leg_incoming param pattern
	#Запускаем проигрыш файлов звонящему абоненту, после их окончания начнет 
	#действовать таймер param(interDigitTimeout), по истечении которого 
	#будет событие ev_collectdigits_done
    media play leg_incoming %s500 $playng_files(welcome) $after_welcome $playng_files(onhold)
	#Запускаем таймер, по истечении которого произойдет событие ev_named_timer
	timer start named_timer $numbers(waiting_time) t1
}

#Процедура запроса ввести номер
proc Play_TakeNumber { } {
    puts "\n\n IVR - proc Play_TakeNumber start \n\n"
	global playng_files
	global numbers
	global param
	global pattern
	
	#Проверяем какой раз абонент пытается набрать номер
	if {$numbers(cur_try) <= $numbers(max_try)} {
	puts "\n\n IVR - proc Play_TakeNumber current try is: $numbers(cur_try) \n\n"
	incr numbers(cur_try)
	#Запускаем процедуру сбора нажатых цифр со стороны звонящего
	leg collectdigits leg_incoming param pattern
	#Запускаем проигрыш файлов
	media play leg_incoming $playng_files(takenumber)
	#Запускаем таймер, по истечении которого произойдет событие ev_named_timer
	timer start named_timer $numbers(waiting_time) t1
	
	#Если попытка больше чем $numbers(max_try) - разъединяем
	} else { 
		fsm setstate CALLDISCONNECTED
		media play leg_incoming $playng_files(callafter)	
	}
}

#Процедура перевода звонка на секретаря
proc GoToReception { } {
	puts "\n\n IVR - proc GoToReception start \n\n"
	global numbers
	#Останавливаем проигрыш медиа
	media stop leg_incoming
	#Меняем состояние
	fsm setstate CALLCONNECTED
	
	set digit $numbers(reception)
	
	#Передаем $digit в функцию CheckCallersAndConnect
	CheckCallersAndConnect $digit
}

#Здесь проверяем введенные или не введенные звонящим цифры 
proc CheckDestanation { } {
    puts "\n\n IVR - proc CheckDestanation start \n\n"
	global playng_files
	global numbers
	global digit
	#Останавливаем проигрыш медиа
	media stop leg_incoming
	
	#Определяем значение переменным
    set status [infotag get evt_status]
	set digit [infotag get evt_dcdigits]
	
	#Сравниваем полученные цифры и статусы
	#Если введенная цифра соответствует той, что задана в $numbers(fast_reception), 
	#изменяем  digit  на номер ресепшн и передаем $digit в функцию CheckCallersAndConnect,
	# предварительно изменив статус на CALLCONNECTED,
	# благодаря которому, при наступлении события ev_setup_done (подключение к номеру секретаря)
	# будет отработана процедура CallIsConnect
	if {$digit == $numbers(fast_reception)} {
		puts "\n\n IVR - proc CheckDestanation digit = $digit\nGoing to next reception \n\n"
		fsm setstate CALLCONNECTED
		set digit $numbers(reception)
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если введенная цифра соответствует той, что задана в $numbers(fast_ckp), подключаем на ЦКП 
	#через CheckCallersAndConnect
	} elseif {$digit == $numbers(fast_ckp)} {
		puts "\n\n IVR - proc CheckDestanation digit = $digit\nGoing to next CKP \n\n"
		fsm setstate CALLCONNECTED
		set digit $numbers(ckp)
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если введенная цифра соответствует той, что задана в $numbers(fast_fax), подключаем на факс 
	#через CheckCallersAndConnect
	} elseif {$digit == $numbers(fast_fax)} {
		puts "\n\n IVR - proc CheckDestanation digit = $digit\nGoing to next fax \n\n"
		fsm setstate CALLCONNECTED
		set digit $numbers(fax)
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если статус = cd_004 (введены корректные цифры номера) - подключаем к нужному номеру 
	#через CheckCallersAndConnect
	} elseif {$status == "cd_004"} {
		puts "\n\n IVR - proc CheckDestanation status = $status digit = $digit \n\n"
		fsm setstate CALLCONNECTED
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit
	#Если статус = cd_005 (совпадение с dial plan) - подключаем к нужному номеру 
	#через CheckCallersAndConnect	
	} elseif {$status == "cd_005"} {
		puts "\n\n IVR - proc CheckDestanation status = $status digit = $digit \n\n"
		fsm setstate CALLCONNECTED	
		#Передаем $digit в функцию CheckCallersAndConnect
		CheckCallersAndConnect $digit	
	#Если статус = cd_006 (набран не существующий номер) - играем в линию $playng_files(noexist) 
	#и изменяем статус на TRYAGAIN, при действии которого и наступлении события ev_media_done 
	#(конец проигрывания звукового файла) вызовется процедура Play_TakeNumber
	} elseif {$status == "cd_006"} {
		puts "\n\n IVR - proc CheckDestanation status = $status digit = $digit \n\n"
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(noexist)
	#Во всех остальных случаях изменяем статус на TORECEPTION, при действии которого и 
	#наступлении события ev_media_done (конец проигрывания звукового файла) вызовется процедура GoToReception
	} else {
		#Проигрываем "Ваш вызов переадресовывается на секретаря"
		fsm setstate TORECEPTION
		media play leg_incoming $playng_files(toreception)		
		puts "\n\n IVR - proc CheckDestanation status = $status \n\n"
	}	
}

#Проверяем звонящего, если совпадает, будем менять отображаемое имя
proc CheckCallersAndConnect {digit} {
	puts "\n\n IVR - proc CheckCallersAndConnect start \n\n"
	
	set callernumber [infotag get leg_ani]
	
	 switch $callernumber {
	 "9120000000" {set callInfo(displayInfo) "Director(mobile)"}
	 "9130000000" {set callInfo(displayInfo) "Buhgalter(mobile)"}  
	 default {} 	
	}
	
	leg setup $digit callInfo leg_incoming
}

#Процедура проверки состоянии линии после подключения звонящего к требуемому номеру
proc CallIsConnect { } {
	puts "\n\n IVR - proc CallIsConnect start \n\n"
	global playng_files	

	#Определяем чему равен status
	set status [infotag get evt_status]
	
	#Если статус равен ls_000 (успешное соединение с требуемым номером), изменяем состояние на CALLACTIVE
	if {$status == "ls_000"} {
	fsm setstate CALLACTIVE	
	
	#Если статус равен ls_002 (никто не ответил на звонок), запускаем процедуру запроса номера
	} elseif {$status == "ls_002"} {
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(noanswer)
	#Если статус - неверный номер, запускаем процедуру запроса номера
	} elseif {$status == "ls_004" || $status == "ls_005" || $status == "ls_006"} {
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(noexist)
	#Если статус равен ls_007 (абонент занят), запускаем процедуру запроса номера
	} elseif {$status == "ls_007"} {
		fsm setstate TRYAGAIN
		media play leg_incoming $playng_files(busy)
	}
}

#Процедура прерывания звонка
proc AbortCall { } {
	puts "\n\n IVR - proc AbortCall start \n\n"
	call close
}

#Исполнение скрипта
init
#init_perCallVars

#Это набор состояний и возникающих при данных состояних событий
#По сути именно это и описывает работу скрипта
	
	#Если в любом состоянии возникнет событие отключения ev_disconnected, вызвать AbortCall
	set ivr_fsm(any_state,ev_disconnected)    			"AbortCall, 		same_state"
	
	#Если в состоянии CALLCOMES возникнет событие ev_setup_indication (входящий вызов)
	#запускается Play_Welcome, и состояние меняется на same_state (т.е. остается прежним)
	set ivr_fsm(CALLCOMES,ev_setup_indication)  		"Play_Welcome, 		same_state"
	
	#Если в состоянии CALLCOMES возникнет событие ev_collectdigits_done (закончен ввод цифр) 
	#запускается CheckDestanation, и состояние остается прежним
	set ivr_fsm(CALLCOMES,ev_collectdigits_done)		"CheckDestanation, 	same_state"
	
	#Если в состоянии CALLCOMES возникнет событие ev_named_timer (закончился таймер ожидания ввода цифр) 
	#запускается GoToReception, и состояние остается прежним
	set ivr_fsm(CALLCOMES,ev_named_timer)				"GoToReception, 	same_state"
	
	#Если в состоянии TORECEPTION возникнет событие ev_media_done (закончился проигрыш файла) 
	#запускается GoToReception, и состояние остается прежним
	set ivr_fsm(TORECEPTION,ev_media_done)         		"GoToReception, 	same_state"
	
	#Данные настройки описывают поведение скрипта при ошибке в номере
	set ivr_fsm(TRYAGAIN,ev_media_done)         		"Play_TakeNumber, 	TRYING"
	set ivr_fsm(TRYING,ev_collectdigits_done)			"CheckDestanation, 	same_state"
	set ivr_fsm(TRYING,ev_named_timer)					"GoToReception, 	same_state"
	
	#Если в состоянии CALLCONNECTED возникнет событие ev_setup_done 
	#(установлено/неустановлено соединение с требуемым номером) запускается CallIsConnect, и состояние остается прежним
	set ivr_fsm(CALLCONNECTED,ev_setup_done) 			"CallIsConnect,		same_state"
	
	#Эти события отрабатывают отключение линии
	set ivr_fsm(CALLACTIVE,ev_disconnected)   			"AbortCall,			CALLDISCONNECTED"
	set ivr_fsm(CALLDISCONNECTED,ev_disconnected) 		"AbortCall,			same_state"
	set ivr_fsm(CALLDISCONNECTED,ev_media_done)  		"AbortCall,			same_state"
	set ivr_fsm(CALLDISCONNECTED,ev_disconnect_done) 	"AbortCall,			same_state"

fsm define ivr_fsm CALLCOMES

Комментарии (2)


  1. htol
    27.08.2015 09:19

    Первое, что при изучении голосового меню от Cisco мне было очень трудно понять, дак это FSM переходы.

    FSM — Finite-state Machine или по русски конечный автомат. В случае используемом в IVR еще и детерминированный. Если вдруг кто займется, советую начать с теории по FSM. Она не большая, но сильно упростит понимание.


  1. antirek
    28.08.2015 08:20

    круто.
    итоговый скрипт хорошо разместить на гитхабе, я бы себе форкнул и поэкспериментировал.