Рубрика:
Администрирование /
Продукты и решения
|
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|
МИХАИЛ ЗАГРАЕВСКИЙ
Создание и настройка IVR для голосовых шлюзов Cisco Systems
Президента компании AT&T на пресс-конференции спросили:
- Почему телефонная трубка не изменилась за последние 100 лет?
- Потому что все изменения претерпела телефонная станция.
В последнее время трудно найти провайдера сетевых услуг, который бы не уделял должного внимания IP-телефонии. Услуги IP-телефонии или VoIP можно условно разделить на 3 группы – это «терминация» трафика удаленных операторов в сети PSTN, проксирование VoIP-звонков и предоставление услуг связи, используя IP-сети.
Конечного пользователя (клиента некого оператора VoIP), как правило, всегда интересует 3-я группа – для него она фактически означает получение возможности сделать междугородний или международный звонок по несколько более выгодным тарифам, чем те, которые готов предоставить ему городской узел связи. Среди множества способов реализации такого сервиса особую популярность занимает так называемая IP-карта. Приобретая ее, клиент может позвонить по местному (городскому) телефонному номеру, указанному на карте, и, перейдя в тональный режим, ввести по запросу системы пин-код, указанный на карте, и непосредственно код страны, города и номер телефона, куда он желает сделать звонок.
Когда клиент дозванивается по номеру, указанному на карточке, начинает работать IVR (Interactive Voice Response) – специальное приложение, подгружаемое или частично встроенное в голосовой шлюз, и обеспечивающее интерактивное «общение» с клиентом: запрос необходимых данных для авторизации, получение информации о том, куда клиент желает сделать звонок, выдача сведений об ошибках и балансе карты.
Мы рассмотрим создание и настройку IVR для маршрутизаторов и голосовых шлюзов фирмы CISCO (http://www.cisco.com) с аналоговыми FXO (Foreing Exchange Office) портами или цифровыми голосовыми портами. Все примеры, рассмотренные ниже, были протестированы на маршрутизаторе CISCO 3725 IOS 12.3(7)T, но должны работать на любом голосовом шлюзе фирмы CISCO с IOS, поддерживающим TCL IVR API 2.0 и авторизацию по протоколу RADIUS. Нашей целью будет создание и отладка полноценного приложения для предоставления услуги междугородней связи с использованием предоплаченных (дебетных) телефонных карт.
IVR для оборудования CISCO представляет собой скрипт, написанный на языке TCL (Tool Command Language) с использованием CISCO TCL IVR API (Application Program Interface). Мы будем работать с версией API 2.0.
Итак, давайте прежде всего четко определим наши цели и основные стадии звонка.
Предполагается, что информация от клиента к IVR будет поступать с использованием DTMF (Dual Tone Multi-frequency) (тонального донабора).
Клиенту предлагается:
- Выбрать язык общения с системой.
- Ввести номер (пин-код) его карточки.
- Ввести номер телефона, куда он желает сделать звонок.
- Клиента соединяют с требуемым номером.
Естественно, при любых ошибках клиенту выдается соответствующее диагностическое сообщение. Например, при неверном вводе номера карты или неправильном номере назначения.
Попутно мы запрограммируем некоторые приятные сервисные функции, такие как: сообщение о занятости или недоступности желаемого номера, возможность прервать звонок и сделать новый, не кладя трубки.
По завершении звонка маршрутизатор может предоставить информацию о клиенте (номере его карты), длительности звонка и назначении звонка серверу RADIUS для ведения статистики.
Как уже говорилось ранее, IVR – это программа, написанная на языке TCL с использованием CISCO IVR API, которая начинает свое выполнение при поступлении звонка на маршрутизатор (голосовой порт). Наше TCL-приложение может находиться на различных носителях или удаленных серверах, а именно FTP, TFTP и FLASH-памяти маршрутизатора. При поступлении звонка шлюз запускает IVR-скрипт, определенный в конфигурации соответcтвующего dial-peer, связанного с портом, на который поступил звонок. Например, в шлюзе определен голосовой аналоговый FXO-порт:
voice-port 1/0/0
cptone RU
timeouts initial 20
timeouts interdigit 20
timeouts wait-release 10
С ним связан соответствующий dial-peer:
dial-peer voice 1 pots
application debitcard
port 1/0/0
То есть при поступлении звонка на голосовой порт 1/0/0 запустится IVR-приложение с именем debitcard, которое должно быть определено глобально в конфигурации маршрутизатора:
call application voice debitcard slot0:/ivr/debitcard.tcl
call application voice debitcard uid-len 8
call application voice debitcard pin-len 3
call application voice debitcard warning-time 10
call application voice debitcard language 1 ru
call application voice debitcard language 2 en
call application voice debitcard set-location ru 0 slot0:/ivr/prompts/ru/
call application voice debitcard set-location en 0 slot0:/ivr/prompts/en/
Первая строчка сообщает маршрутизатору о местонахождении IVR-скрипта с именем debitcard.tcl, в нашем случае он находится на slot0: – дополнительном модуле Flash-памяти.
Вторая и третья определяют количество цифр в идентификаторе и пин-коде карты.
Четвертая строчка определяет количество секунд до окончания звонка, за которое клиента следует предупредить.
Пятая и шестая указывают маршрутизатору, где искать необходимые звуковые файлы, из которых будут формироваться голосовые сообщения для клиента.
Вообще говоря, для каждого IVR-скрипта можно определить любой параметр или параметры, значение которых он потом сможет считать через соответствующие IVR API.
Например, наличие следующей строчки в конфигурации шлюза:
call application voice debitcard say_seconds yes
определяет для IVR-скрипта debitcard параметр say_seconds, имеющий значение yes. Позже, в процессе выполнения скрипта, мы сможем изменить его поведение в зависимости от значения этого параметра, что, несомненно, увеличивает универсальность приложения.
IVR-приложение базируется на трех составляющих: процедуры инициализации, функции обработки событий и конечное состояние (FSM – Finite State Machine).
Процедуры или функции инициализации применяются для объявления и присвоения значений переменным, используемых скриптом.
Эти функции бывают двух типов. К первому типу относятся те, которые будут выполнены всего один раз в начале работы скрипта. Они, как правило, используются для инициализации глобальных переменных, значение которых не должно меняться. Например, в этих процедурах мы можем считать параметры, определенные для скрипта в глобальной конфигурации маршрутизатора.
Второй тип процедур инициализации – это те, которые вызываются при каждом поступлении звонка на маршрутизатор, а именно при получении события ev_setup_indication или ev_handoff. В них, как правило, принято определять и инициализировать глобальные переменные, которые могут меняться при каждом звонке.
При получении IVR-скриптом определенного события вызываются соответствующие функции обработки. Примером таких событий могут быть нажатие клавиши на клавиатуре телефонного аппарата, сигнал о завершении проигрывания клиенту голосовых файлов или окончании процесса авторизации на RADIUS-сервере.
Для IVR-скрипта назначен четкий набор событий, который он может обрабатывать, и на каждое из них можно привязать функцию-обработчик этого события, которая будет автоматически выполнена при поступлении этого события в каждом конечном состоянии скрипта.
FSM – (конечные состояния) выполнения IVR-скрипта определяют, какие именно обработчики будут вызываться при получении тех или иных событий, и в какое состояние должен перейти скрипт после вызова обработчика.
Итак, приступим к написанию скрипта. Мы создадим текстовый файл с именем debitcard.tcl. Далее приводится полный листинг скрипта с подробным описанием процедур и команд. В тексте слова «процедура» и «функция» являются синонимами.
proc init { } {
global param;
global retryCnt;
global LangPattern;
global ParamForCard;
global ParamForDest;
global AccountLen;
global PinLen;
global CardLen;
global WarnTime;
set param(abortKey) *
set param(interruptPrompt) true
set param(ignoreInitialTermKey) true
set LangPattern(1) {[1,2]}
set AccountLen [string trim [infotag get cfg_avpair uid-len]];
set PinLen [string trim [infotag get cfg_avpair pin-len]];
set retryCnt [string trim [infotag get cfg_avpair retry-count]];
set WarnTime [string trim [infotag get cfg_avpair warning-time]];
set CardLen [expr $AccountLen + $PinLen];
set ParamForCard(abortKey) *
set ParamForCard(initialDigitTimeout) 10
set ParamForCard(terminationKey) #
set ParamForCard(maxDigits) $CardLen;
set ParamForCard(interruptPrompt) true
set ParamForDest(abortKey) *
set ParamForDest(initialDigitTimeout) 10
set ParamForDest(terminationKey) #
set ParamForDest(interruptPrompt) true
set ParamForDest(dialPlanTerm) true;
set ParamForDest(ignoreInitialTermKey) true;
return;
}
Процедура init вызывается всего один раз при первом запуске скрипта, и в ней производится объявление и инициализация переменных, не меняющих свое значение для всех звонков, обслуживаемых IVR-скриптом.
Она вызывается в глобальной области TCL-приложения командой init, и все переменные, объявленные и установленные таким образом, будут сохранять свои значения на всем протяжении работы шлюза для всех поступающих звонков. В этой процедуре объявляются следующие переменные: param, ParamForDest, ParamForCard – ассоциативные массивы, содержащие параметры для сбора информации о набранных клиентом цифрах для выбора языка сообщений системы, номере карты и номере телефона назначения.
Массивы могут содержать следующие именованные индексы и значения:
- param(abortKey) – определяет клавишу, которая будет использоваться как клавиша отмены набора (например, если клиент по ошибке нажал не ту клавишу и вовремя это заметил, то он будет иметь возможность нажать клавишу, определенную параметром param(abortKey), и IVR-скрипт получит событие ev_collectdigits_done, определяющее завершение процесса сбора информации о полученных от клиента цифрах со статусом «cd_002», который означает, что клиент нажал клавишу отмены. В нашем случае это клавиша с символом «*» (звездочка).
- param(interDigitTimeout) – устанавливает тайм-аут в секундах между нажатиями клавиш. Если за этот промежуток времени не будет нажата ни одна клавиша, то скрипт получит событие ev_collectdigits_done со статусом завершения «cd_001».
- param(initialDigitTimeout) – определяет тайм-аут до момента нажатия первой клавиши. По истечении этого времени возникает событие ev_collectdigits_done со статусом завершения «cd_001».
- param(interruptPrompt) – может принимать значения true или false – определяет возможность прерывать клиентом голосовые сообщения нажатием любой клавиши.
- param(terminationKey) – по аналогии с abortKey позволяет задать клавишу, которая будет использоваться в качестве индикатора завершения клиентов ввода цифр. При нажатии этой клавиши возникает событие ev_collectdigits_done со статусом «cd_005». В нашем случае это клавиша с символом «#» (решетка).
- param(maxDigits) – задает максимальное количество цифр, по истечении набора которых возникает событие ev_collectdigits_done со статусом «cd_005».
Мы вычисляем этот параметр для количества цифр, ожидаемых от клиента при вводе номера карты, зная длину идентификатора (логина) и пароля. Сначала мы с помощью команды infotag get устанавливаем значения переменных AccountLen и PinLen в те значения, которые определены для этих параметров в глобальной конфигурации маршрутизатора. Затем вычисляем сумму этих значений, которую присваиваем переменной CardLen. И наконец, переменная LangPattern представляет собой массив, содержащий шаблоны, при совпадении с которыми процесс сбора цифр завершается со статусом «cd_005». В нашем случае мы хотим получить только цифры 1 или 2 для русского и английского языков соответственно.
proc init_perCallVars { } {
global NumLangPrompt;
global NumCardPrompt;
global NumDestPrompt;
global PromptFlag;
global DestPromptFlag;
global NoPlayWarn;
global NoTimeLimit;
global SetupDone;
set NumLangPrompt 0;
set NumCardPrompt 0;
set NumDestPrompt 0;
set PromptFlag 0;
set DestPromptFlag 0;
set NoPlayWarn 0;
set NoTimeLimit 0;
set SetupDone 0;
return;
}
Процедура init_PerCallVars отвечает за объявление и инициализацию глобальных переменных, меняющих свое значение в процессе каждого звонка. В описании других процедур будет дано пояснение каждой из них.
proc act_Setup { } {
init_perCallVars;
leg setupack leg_incoming;
infotag set med_language prefix "ru";
SelectLanguageMenu;
return;
}
Процедура act_Setup вызывается IVR-скриптом при поступлении нового звонка. Именно с нее, по сути, начинается обработка звонка клиента. Почему именно с нее? Имя этой функции определено как обработчик события ev_setup_indication, которое генерируется при каждом новом звонке. За это отвечает строчка:
set ivr_fsm(CALLCOMES,ev_setup_indication) "act_Setup same_state";
в конце нашего скрипта.
Тут следует ненадолго перейти к теме FSM. Перед выполнением IVR-скрипта требуется определить имя массива, который будет содержать в себе все состояния звонка, функции-обработчики событий, которые могут возникнуть при этом состоянии, и имя следующего состояния, в которое перейдет звонок при выполнении этой функции, а также стартовое состояние звонка. Заметьте: не после выполнения функции, а непосредственно сразу (одновременно). Учтите также, что все вызовы функции и команд в TCL IVR API «не блокирующие», и для выполнения последующей команды скрипт не будет дожидаться завершения предыдущей. Это может смутить программистов, работавших ранее с такими языками, как Си, но к этому необходимо привыкнуть. Например, если в процедуре написаны следующие команды:
leg collectdigits 1 callInfo
leg collectdigits 2 callInfo
leg setup 295786 setupInfo $callID5
puts " This will be executed immediately i.e. before the collect digits or call setup is actually complete"
то все они будут выполнены одна за другой, не дожидаясь завершения предыдущей. В данном случае начнется процесс сбора цифр на виртуальных составляющих звонка leg 1 и leg 2, процесс установки соединения на $callID5 и выдано отладочное сообщение командой puts. Далее IVR-приложение начнет ожидать получения событий от этих процессов и вызвать соответствующие функции-обработчики.
Командой fsm define (в конце нашего скрипта) определяется имя массива, описанного выше ivr_fsm, и первоначальное состояние звонка CALLCOMES. Командой set ivr_fsm мы будем добавлять к этому массиву новые элементы, создавая так называемые «FSM-переходы». Общий синтаксис этой команды таков:
set array(curr_state,curr_event) “act_proc NEXTSTATE”
где:
- array – это имя определенного командой fsm define массива.
- curr_state – имя текущего состояния скрипта, при котором получено событие curr_event.
- act_proc – имя функции-обработчика события, которую необходимо выполнить при поступлении события curr_event в состоянии curr_state.
- NEXTSTATE – имя состояния, в которое должен перейти звонок.
При определении FSM можно использовать следующие мета-определения:
- any_state – определяет любое состояние звонка, для которого не установлен иной обработчик в другом FSM-переходе; может быть использовано в левой части (индексе массива) FSM-перехода.
- same_state – трактуется как «то же состояние»; может быть использовано в правой части (значении элемента массива) определения FSM-перехода.
- ev_any_event – определяет любое событие, которое может получить скрипт.
Теперь, глядя на строчку:
set ivr_fsm(CALLCOMES,ev_setup_indication) "act_Setup same_state";
мы легко можем понять, что при получении события ev_setup_indication в состоянии CALLCOMES будет выполнена функция act_Setup и звонок останется в прежнем состоянии (same_state).
Возвращаясь к нашей функции act_Setup, мы видим, что в ней вызывается процедура init_perCallVars для объявления и инициализации глобальных переменных, которым необходимо иметь возможность менять свое значение при каждом отдельно взятом звонке. Командой leg setupack посылается сообщение setup acknowledgement, чтобы перевести наш звонок в состояние, при котором возможно получение клиентом голосовых сообщений.
Команда infotag set med_language prefix «ru» устанавливает текущий язык голосового интерфейса и фактически указывает скрипту, где ему следует искать звуковые файлы для этого языка. Помните строчку в глобальной конфигурации шлюза:
call application voice debitcard set-location ru 0 slot0:/ivr/prompts/ru/
теперь скрипт «знает», что все файлы, из которых следует составлять голосовые сообщения, находятся на носителе slot0:/ivr/prompts/ru/. Перед именем файла при его поиске он автоматически будет добавлять префикс ru, например, команда media play _wecome.au «проиграет» клиенту файл, полный путь, к которому будет slot0:/ivr/prompts/ru/ru_wecome.au.
И в конце этой процедуры вызывается функция Select LanguageMenu:
proc SelectLanguageMenu { } {
global param;
global retryCnt;
global NumLangPrompt;
global LangPattern;
if {$NumLangPrompt < $retryCnt} {
media play leg_incoming %s2000 _RUS_lang_sel1.au %s500 _ENG_lang_sel2.au
leg collectdigits leg_incoming param LangPattern;
} else {
media play leg_incoming _final.au;
fsm setstate CALLDISCONNECT;
}
return;
}
В ее задачи входит следующее: пользователю проигрываются два приглашения на разных языках с предложением нажать соответствующую клавишу для выбора нужного языка. После проигрывания команда leg collectdigits переводит скрипт в состояние ожидания поступления события о завершении процесса сбора цифр. Это событие поступит после однократного нажатия клиентом любой клавиши – за такое поведение отвечает массив LangPattern, позволяющий клиенту нажать только одну клавишу. Но если эта клавиша не была цифрой 1 или 2, то статус завершения события ev_collectdigits_done будет отличен от «cd_005».
При получении этого события определенный нами FSM-переход:
set ivr_fsm(CALLCOMES,ev_collectdigits_done) "CheckLangSelection CHECKLANG";
обеспечит вызов функции CheckLangSelection, которая и проверит статус завершения события и при неудовлетворительном результате должна увеличить значение глобальной переменной NumLangPrompt для того, чтобы функция SelectLanguageMenu не повторяла приглашения больше раз, чем записано в переменной retryCnt, тем самым не позволяя звонку зациклиться на проигрывании звуковых файлов, если нам попался туго соображающий пользователь.
Именно здесь становится понятна необходимость использования функции init_perCallVars, в которой определяются подобные переменные, регулирующие максимальное количество проигрывания приглашений. Нам необходимо, чтобы при следующем звонке они обнулились, что невозможно реализовать через глобальную процедуру init. К тому же при нескольких одновременных звонках у каждого из них должен быть свой экземпляр переменной.
proc CheckLangSelection { } {
global NumLangPrompt;
set collect_status [infotag get evt_status];
set collect_digits [infotag get evt_dcdigits];
switch $collect_status {
"cd_001" {
incr NumLangPrompt;
media play leg_incoming _no_digits_entered.au;
return;
}
"cd_002" {
SelectLanguageMenu;
fsm setstate CALLCOMES;
return;
}
"cd_005" {
infotag set med_language $collect_digits;
fsm setstate CARDSELECTION;
act_GetCard;
return;
}
"cd_006" {
incr NumLangPrompt;
media play leg_incoming _wrong_lang_sel.au;
return;
}
default {
media play leg_incoming _no_aaa.au;
fsm setstate CALLDISCONNECT;
}
}
return;
}
В процедуре CheckLangSelection мы получаем статус события ev_collectdigits_done командой infotag get evt_status; и набранные цифры(у) командой infotag get evt_dcdigits, затем для удобства присваиваем их временным переменным collect_status и collect_digits. При получении интересующего нас статуса «cd_005» мы командой fsm setstate «вручную» переводим звонок в состояние CARDSELECTION и вызываем процедуру act_GetCard для запроса информации о пин-коде карты клиента и последующей его авторизации.
proc act_GetCard { } {
global ParamForCard;
global NumCardPrompt;
global PromptFlag;
global retryCnt;
if {$NumCardPrompt < $retryCnt} {
switch $PromptFlag {
0 {media play leg_incoming %s500 _enter_card_num.au; #first play}
1 {media play leg_incoming _invalid_digits.au; #Not enuf digits pressed}
3 {media play leg_incoming _no_card_entered.au; #Timeout - no digits entered}
default {
media play leg_incoming _no_aaa.au;
fsm setstate CALLDISCONNECT;
return;
}
}
leg collectdigits leg_incoming ParamForCard;
} else {
media play leg_incoming _final.au;
fsm setstate CALLDISCONNECT;
}
return;
}
Процедура act_GetCard проигрывает клиенту соответствующие звуковые файлы не более $retryCnt раз в зависимости от значения переменной PromptFlag, которая изначально установлена в 0 в процедуре init_perCallVars. Соответственно при первом вызове этой процедуры клиент слышит приглашение ввести номер (пин-код) карточки. Значение 1 соответствует неверному количеству набранных цифр и значение 3 – статусу, при котором клиент не набрал ни одной цифры. Далее скрипт выполняет команду leg collectdigits и ожидает получения события ev_collectdigits_done, которое проанализирует функция act_GotCardNumber, что обеспечит FSM-переход:
proc act_GotCardNumber { } {
global NumCardPrompt
global AccountLen;
global PinLen;
global CardLen;
global PromptFlag;
global retryCnt;
global account;
global pin;
set status [infotag get evt_status];
switch $status {
"cd_005" {
set card_number [infotag get evt_dcdigits];
set card_len [string length $card_number];
if {$card_len == $CardLen} {
set account [string range $card_number 0 [expr $AccountLen - 1]];
set pin [string range $card_number $AccountLen [expr $card_len - 1]]
puts "account = $account pin = $pin";
aaa authorize $account $pin "" "" leg_incoming;
} else {
incr NumCardPrompt;
set PromptFlag 1;
act_GetCard;
return;
}
}
"cd_001" {
incr NumCardPrompt;
set PromptFlag 3;
act_GetCard;
return;
}
"cd_002" {
set PromptFlag 0
act_GetCard;
return;
}
}
return;
}
Процедура act_GotCardNumber выполняется при получении события ev_collectdigits_done. Глобальная переменная ParamForCard, инициализированная в функции init, обеспечивает нам получение определенного количества цифр – в нашем конкретном случае это число 11 ($Cardlen = $AccountLen + $PinLen). При неудовлетворяющем нас статусе события функция act_GotCardNumber инкремирует переменную NumCardPrompt для счетчика количества приглашений ввода карты, устанавливает значение переменной PromptFlag и снова вызывает процедуру act_GetCard, которая проверяет, не превышен ли счетчик приглашений, и указывает клиенту на ошибку, основываясь на значении PromptFlag. При успешно завершившемся процессе сбора цифр (статус «cd_005») функция вычисляет логин и пароль, и с помощью команды:
aaa authorize $account $pin "" "" leg_incoming;
указывает маршрутизатору отправить запрос на авторизацию карты RADIUS-серверу. При получении ответа от него скрипту поступит событие ev_authorize_done. Далее скрипт выполнит функцию act_CardAuthorize и останется в том же состоянии, для этого используем следующий FSM-переход:
set ivr_fsm(CARDSELECTION,ev_authorize_done) "act_CardAuthorize same_state";
определенный в конце нашего скрипта:
proc act_CardAuthorize { } {
global PromptFlag;
global NumCardPrompt;
global ParamForDest;
global ParamForCard;
global retryCnt;
set status [infotag get evt_status];
if {$status == "ao_000"} {
if {[infotag get aaa_avpair_exists h323-credit-amount]} {
set amt [infotag get aaa_avpair h323-credit-amount]
} else {
media play leg_incoming _no_aaa.au;
fsm setstate CALLDISCONNECT;
return;
}
fsm setstate DESTSELECTION;
if {$amt <= 999999.99} {
media play leg_incoming _you_have.au %a$amt _enter_dest.au;
} else {
media play leg_incoming _enter_dest.au;
}
leg collectdigits leg_incoming ParamForDest;
return;
}
if {[infotag get aaa_avpair_exists h323-return-code]} {
set return_code [infotag get aaa_avpair h323-return-code];
} else {
media play leg_incoming _no_aaa.au;
fsm setstate CALLDISCONNECT;
return;
}
incr NumCardPrompt;
if {$NumCardPrompt < $retryCnt} {
leg collectdigits leg_incoming ParamForCard;
act_PlayCardReturnCode $return_code;
} else {
ct_GetCard;
}
return;
}
Процедура act_CardAuthorize так же анализирует статус события ev_authorize_done, и при успешной авторизации («ao_000») получает с помощью команды infotag get aaa_avpair h323-credit-amount сумму на счете клиента, сохраняет ее в переменной amt и проигрывает ее клиенту с помощью команды media play %a$amt. Тут необходимо сделать небольшое отступление.
TCL IVR API 2.0 предоставляет функции TTS (Text To Speech), которые позволяют воспроизводить клиенту голосовые сообщения, основанные на параметрах команды media play. Например, параметр %a считает значение num денежной суммой в центах (в нашем случае в копейках), и клиент услышит эквивалентную сумму в долларах и центах или в рублях и копейках. К великому сожалению, IVR API могут работать только с несколькими языками, как правило это английский, китайский и испанский, а для русского языка необходимо купить у фирмы Cisco Systems скрипт, обеспечивающий поддержку русского языка. Любителям экстремальных ощущений предлагается написать такой скрипт самим, который потом можно будет подгрузить в маршрутизатор командой call language voice в режиме глобальной конфигурации.
Скрипт этот должен быть написан с применением «чистого» TCL без использования CISCO IVR API, и из него будет вызываться функция translate с передачей ей параметров из команды media play. Функция translate должна возвратить строку, состоящую из имен *.au-файлов, подлежащих воспроизведению, разделенных пробелами. Кроме этого, по невыяcненной пока причине, TCL IVR-скрипт, получив от TTS TCL-строку с перечисленными аудио-файлами, требующими проигрывания, игнорирует весь текст до первого пробела (лидирующие пробелы не учитываются). То есть, если ваш скрипт вернет следующую строку: «_200.au _40.au _7.au», то проиграны будут только файлы _40.au и _7.au.
Несмотря на то, что такое поведение не зарегистрировано как официальная ошибка, оно все же присутствовало во всех тестированных автором версиях IOS и оборудовании. По этому поводу официальные лица Cisco ничего не говорят, кроме того, что они не несут никакой ответственности за написанный третьими лицами код, и подобное программное обеспечение надо приобретать у них.
Далее скрипт, вызывая команду fsm state DESTSELEC-TION, устанавливает для звонка новое состояние и, используя команду leg collectdigits, переходит в режим ожидания завершения сбора цифр от клиента, которые определят номер телефона назначения.
При отказе в авторизации (статус события ev_authorize_done не равен «ao_000») функция act_CardAuthorize получает код ответа от RADIUS-сервера командой:
infotag get
aaa_avpair_exists h323-return-code
и присваивает его переменной return_code, затем в случае, если количество попыток ввода номера телефона еще не превышено, вызывается функция act_PlayCard ReturnCode c передачей ей переменной return_code в качестве параметра для проигрывания соответствующего звукового сообщения об ошибке, и скрипт вызывает команду leg collectdigits leg_incoming ParamForCard для повторного сбора цифр – номера карты. Вот тут, кстати, мы и используем свойство «не блокирующего» вызова функций: leg collectdigits и act_PlayCardReturnCode идут одна за другой, но выполнены они будут условно одновременно – скрипт будет проигрывать голоcовые сообщения об ошибке и ожидать события ev_collectdigits_done, после которого опять отработает процедура act_GotCardNumber, согласно уже использованному нами ранее FSM-переходу:
set ivr_fsm(CARDSELECTION,ev_collectdigits_done)
поскольку скрипт остался в прежнем состоянии: CARD SELECTION.
proc act_PlayCardReturnCode { return_code } {
switch $return_code {
2 {media play leg_incoming _auth_fail.au;}
7 {media_play leg_incoming _zero_bal.au;}
default {
media play leg_incoming _no_aaa.au;
fsm setstate CALLDISCONNECT;
return;
}
}
return;
}
proc act_GetDestination { } {
global retryCnt;
global ParamForDest;
global DestPromptFlag;
global NumDestPrompt;
if {$NumDestPrompt < $retryCnt} {
switch $DestPromptFlag {
0 {media play leg_incoming _enter_dest.au; #Abortkey pressed}
1 {media play leg_incoming _no_dest_entered.au; #Timeout -no digits entered}
2 {media play leg_incoming _reenter_dest.au; #Not mutch to the dial plan}
default {
media play leg_incoming _no_aaa.au;
fst setstate CALLDISCONNECT;
return;
}
}
leg collectdigits leg_incoming ParamForDest;
} else {
media play leg_incoming _dest_collect_fail.au;
fsm setstate CALLDISCONNECT;
}
return;
}
Процедура act_GetDestination выполняет в некоторой степени вспомогательную функцию – она будет вызываться в случае какой-либо ошибки, например, при тайм-ауте ввода цифр, нажатии клиентом клавиши, определенной как aborKey, или при несоответствии с планом набора, определенным в маршрутизаторе.
proc act_GotDestination { } {
global NumDestPrompt;
global DestPromptFlag;
global account;
global pin;
global destination;
set status [infotag get evt_status];
switch $status {
"cd_001" {
incr NumDestPrompt;
set DestPromptFlag 1;
act_GetDestination;
return;
}
"cd_002" {
set DestPromptFlag 0;
act_GetDestination;
return;
}
"cd_004" {
set destination [infotag get evt_dcdigits];
puts " ************dest_number = $destination";
aaa authorize $account $pin "" $destination leg_incoming;
return;
}
default {
incr NumDestPrompt;
set DestPromptFlag 2;
act_GetDestination;
return;
}
}
return;
}
Процедура act_GotDestination будет выполняться в соответствии с FSM-переходом:
set ivr_fsm(DESTSELECTION,ev_collectdigits_done) "act_GotDestination same_state";
Она проверяет статус завершения события ev_collect digits_done. В случае неустраивающего нас статуса вызывает функцию act_GetDestination, установив предварительно переменную DestPromptFlag в значение, которое укажет функции act_GetDestination, какой именно звуковой файл следует проиграть, и, как всегда, следует проверка, не превышен ли счетчик максимально допустимых попыток ввести информацию. При устраивающем нас номере телефона, введенном пользователем (статус «cd_004» – совпадение с планом набора) с помощью команды:
aaa authorize $account $pin "" $destination leg_incoming;
RADIUS-серверу будет отправлен запрос на авторизацию звонка. По завершении авторизации скрипт получит событие ev_authorize_done, которое согласно определенному нами FSM-переходу:
set ivr_fsm(DESTSELECTION,ev_authorize_done) "act_CallAuthorize same_state";
обработает процедура act_CallAuthorize:
proc act_CallAuthorize { } {
global NumDestPrompt;
global DestPromptFlag;
global WarnTime;
global NoPlayWarn;
global NoTimeLimit;
global creditTime;
global retryCnt;
global ParamForDest;
global param;
set status [infotag get evt_status];
set param(enableReporting) true;
set param(interruptPrompt) false;
if {$status == "ao_000"} {
if {[infotag get aaa_avpair_exists h323-credit-time]} {
set creditTime [infotag get aaa_avpair h323-credit-time];
} else {
media play leg_incoming _no_aaa.au;
fsm setstate CALLDISCONNECT;
return;
}
if {$creditTime <= $WarnTime} {set NoPlayWarn 1;}
if {$creditTime == "unlimited"} {set NoTimeLimit 1;}
media play leg_incoming _you_have.au %t$creditTime;
leg collectdigits leg_incoming param; #For Fast leg setup
fsm setstate PLACECALL;
return;
}
incr NumDestPrompt; #Call authorize failed
if {[infotag get aaa_avpair_exists h323-return-code]} {
set return_code [infotag get aaa_avpair h323-return-code];
} else {
media play leg_incoming no_aaa.au;
fsm setstate CALLDISCONNECT;
return;
}
if {$NumDestPrompt < $retryCnt} {
act_PlayDestReturnCode $return_code;
leg collectdigits leg_incoming ParamForDest;
} else {
act_GetDestination;
}
return;
}
Процедура act_CallAuthorize анализирует статус события ev_authorize_done. При успешной авторизации клиенту проговаривается, сколько времени у него на балансе и возвращенное скрипту RADIUS-сервером в переменной h323-credit-time, далее скрипт запускает команду leg collectdigits leg_incoming param и командой fsm setstate PLACECALL переходит в новое состояние: PLACECAL, в котором по завершении проигрывания звуковых файлов обработает событие ev_media_done и через FSM-переход:
set ivr_fsm(PLACECALL,ev_media_done) "act_CallSetup same_state;
будет выполнена функция act_CallSetup, которая наконец-то соединит нашего клиента с требуемым номером.
При ошибке в авторизации функция получит код ошибки из переменной h323-return-code и будет вызвана процедура act_PlayDestReturnCode, которая озвучит соответствующее сообщение об ошибке.
Если счетчик попыток ввести номер не превышен, скрипт будет ожидать от клиента повторного ввода номера, по завершении которого с помощью того же FSM-перехода:
set ivr_fsm(DESTSELECTION,ev_collectdigits_done) "act_GotDestination same_state";
управление будет передано функции act_GotDestination.
Теперь объясним, зачем мы используем команду leg collectdigits при успешной авторизации, ведь казалось бы больше ждать ввода от клиента не стоит.
Это позволит клиенту прервать сообщение о доступном времени звонка и перейти непосредственно к соединению. Данная возможность будет реализована с помощью команд:
set param(enableReporting) true
и
set param(interruptPrompt) false;
и FSM-перехода:
set ivr_fsm(PLACECALL,ev_digit_end) "act_FastSetup same_state";
Если клиент в момент прослушивания информации об оставшемся времени звонка нажмет клавишу «#», скрипт получит событие ev_digit_end, сигнализирующее о том, что была нажата некая клавиша, и будет вызвана функция act_FastSetup, которая проверит эту клавишу на соответствие с символом «#». В случае совпадения прервет проигрывание звуковых файлов командой media stop и вызовет процедуру act_CallSetup. Переменная SetupDone будет установлена в 1, чтобы избежать повторного вызова функции act_CallSetup.
proc act_FastSetup { } {
global SetupDone;
if {[infotag get evt_digit] != "#" || $SetupDone} {
return;
}
media stop leg_incoming;
act_CallSetup;
set SetupDone 1;
return;
}
proc act_CallSetup { } {
global destination;
global account;
global SetupDone;
set callinfo(accountNum) $account;
set callinfo(alertTime) 60;
set SetupDone 1;
leg setup $destination callinfo leg_incoming;
return;
}
Процедура act_CallSetup инициализирует ассоциативный массив callinfo необходимыми значениями – опциями для будущего соединения, в частности устанавливает максимальное время ожидания поднятия трубки вызываемой стороной 60 секунд. И наконец, запускает команду, ради которой, собственно, и создавался этот скрипт:
leg setup $destination callinfo leg_incoming
с передачей ей в качестве параметров номера назначения, этот массив и условное обозначение голосового канала, к которому подключен наш клиент. При завершении команды leg setup скрипт получит событие ev_setup_done, которое будет обработано FSM-переходом:
set ivr_fsm(PLACECALL,ev_setup_done) "act_CallSetupDone same_state";
и вызвана функция act_CallSetupDone:
proc act_CallSetupDone { } {
global DestPromptFlag;
global ParamForDest;
global param;
global NoTimeLimit;
global creditTime;
global WarnTime;
global NoPlayWarn;
global SetupDone;
set status [infotag get evt_status];
switch $status {
"ls_000" {
if {!$NoTimeLimit} { #Setting call timer
if {$NoPlayWarn} {
timer start leg_timer [expr $creditTime - 1] leg_incoming;
fsm setstate CALLLASTACTIVE;
} else {
set delay [expr $creditTime - $WarnTime];
timer start leg_timer $delay leg_incoming;
fsm setstate CALLACTIVE;
}
}
set param(enableReporting) true;
leg collectdigits leg_incoming param; #For long pound
return;
}
"ls_007" {
set DestPromptFlag 0;
set SetupDone 0;
media play leg_incoming _dest_busy.au;
leg collectdigits leg_incoming ParamForDest;
fsm setstate DESTSELECTION;
return;
}
default {
set DestPromptFlag 0;
set SetupDone 0;
media play leg_incoming _dest_unreachable.au %s200 _enter_dest.au;
leg collectdigits leg_incoming ParamForDest;
fsm setstate DESTSELECTION;
return;
}
}
return;
}
Процедура act_CallSetupDone получает статус завершения процесса установки соединения и при удачном соединении (статус «ls_000»), если не установлена переменная NoTimeLimit, сигнализирующая о неограниченном времени звонка, переводит звонок в состояние CALLACTIVE или CALLLASTACTIVE, в зависимости от оставшегося времени, и с помощью команды timer start «заводит» таймер, при срабатывании которого скрипт получит сообщение ev_leg_timer, который будет обработан FSM-переходами:
set ivr_fsm(CALLACTIVE,ev_leg_timer) "act_ActiveTimer CALLWARN";
или
set ivr_fsm(CALLLASTACTIVE,ev_leg_timer) "act_LastActiveTimer same_state";
Время таймера рассчитывается как разность значений переменной, полученной от RADIUS-сервера h323-credit-time, и глобальной переменной WarnTime, проинициализированной в функции init.
Также мы опять вводим приятную возможность для клиента – прервать разговор в любой момент путем длительного (более 300ms) нажатия и удерживания клавиши с изображением решетки. В этот момент скрипт получит событие ev_digit_end, которое обработает FSM-переход:
set ivr_fsm(CALLACTIVE,ev_digit_end) "act_LongPound CONNDESTROY";
При неуспешном завершении процесса установки соединения процедура act_CallSetupDone проиграет клиенту соответствующее сообщение об ошибке, предложение ввести новый номер телефона назначения и перейдет в состояние DESTSELECTION, а согласно FSM-переходу:
set ivr_fsm(DESTSELECTION,ev_collectdigits_done) "act_GotDestination same_state";
после окончания проигрывания файлов при получении события ev_collectdigits_done будет вызвана функция act_Got Destination:
proc act_PlayDestReturnCode {return_code} {
switch $return_code {
9 {media play leg_icoming _dest_blocked.au %s500 _enter_dest.au;}
12 {media play leg_incoming _not_enuf.au %s500 _enter_dest.au;}
default {
media play leg_incoming _no_aaa.au;
fsm setstate CALLDISCONNECT;
return;
}
}
return;
}
proc act_ActiveTimer { } {
global WarnTime;
global incoming;
global outgoing;
set incoming [infotag get leg_incoming];
set outgoing [infotag get leg_outgoing];
connection destroy con_all;
timer start leg_timer [expr $WarnTime - 1] leg_incoming;
return;
}
Процедура act_ActiveTimer выполнится в момент получения скриптом события ev_leg_timer.
В ее задачи входит временно отсоединить вызывающую и вызываемую стороны звонка (это необходимо, чтобы клиент мог получить информацию о том, что время его звонка заканчивается), т.к. нельзя проигрывать звуковые файлы при установленном звуковом канале между двумя сторонами разговора, и установить новый таймер на оставшиеся у клиента $WarnTime секунд. После разрыва соединения скрипт получит событие ev_destroy_done, обрабатываемое FSM-переходом:
set ivr_fsm(CALLWARN,ev_destroy_done) "act_CallWarnDestroy same_state";
proc act_LastActiveTimer { } {
connection destroy con_all;
return;
}
Процедура act_LastActiveTimer, так же как и act_Active Timer, разрывает голосовой канал между двумя сторонами звонка согласно FSM-переходу:
set ivr_fsm(CALLLASTACTIVE,ev_leg_timer) "act_LastActiveTimer same_state";
После этого скрипт будет ожидать получения события ev_destroy_done, которое обработается FSM-переходом:
set ivr_fsm(CALLLASTACTIVE,ev_destroy_done) "act_PlayDisconnect CALLDISCONNECT";
и выполнение будет передано процедуре act_PlayDisconnect, которая сообщит клиенту о завершение его звонка и переведет скрипт в состояние CALLDISCONNECT:
proc act_PlayDisconnect { } {
media play leg_incoming _disconnected.au
fsm setstate CALLDISCONNECT;
return;
}
proc act_CallWarnDestroy { } {
global WarnTime;
media play leg_incoming _you_have.au %t$WarnTime;
return;
}
Процедуре act_CallWarnDestroy управление передается согласно FSM:
set ivr_fsm(CALLWARN,ev_destroy_done) "act_CallWarnDestroy same_state";
В ее задачи входит сообщить клиенту, что до конца его разговора осталось $WarnTime секунд. Когда это сообщение будет проиграно, скрипт получит событие ev_media_done и FSM-переход:
set ivr_fsm(CALLWARN,ev_media_done) "act_CallWarnMedia CALLLASTACTIVE";
позаботится о том, чтобы воcстановить канал между сторонами звонка, запустив функцию act_CallWarnMedia, и переведет звонок в состояние CALLLASTACTIVE:
proc act_CallWarnMedia { } {
global incoming;
global outgoing;
connection create $incoming $outgoing;
return;
}
proc act_LongPound { } {
if {[infotag get evt_digit] != "#"} {
fsm setstate same_state;
return;
}
set duration [infotag get evt_digit_duration];
if {$duration < 300} {
fsm setstate same_state;
return;
}
connection destroy con_all;
return;
}
Процедура act_LongPound интересна тем, что дает клиенту возможность во время разговора (состояния CALL ACTIVE, CALLLASTACTIVE) прервать звонок и сделать новый, не кладя трубки. За это отвечают следующие FSM-переходы:
set ivr_fsm(CALLACTIVE,ev_digit_end) "act_LongPound CONNDESTROY";
set ivr_fsm(CALLLASTACTIVE,ev_digit_end) "act_LongPound CONNDESTROY";
Если клиент нажмет любую клавишу, скрипт в этих состояниях получит событие ev_digit_end, и будет вызвана процедура act_LongPound, которая проверит клавишу на совпадение с символом «#». В случае соответствия она разрушит канал связи между участниками звонка. При успешном разрушении канала связи скрипт получит событие ev_destroy_done. Поскольку согласно предыдущему FSM-переходу звонок находился в состоянии CONNDESTROY, будет введен в работу FSM-переход:
set ivr_fsm(CONNDESTROY,ev_destroy_done) "act_ConnDestroyed same_state";
который вызовет процедуру act_ConnDestroyed. Последняя окончательно и полностью отключит вызываемую сторону и переведет скрипт в состояние DESTSELECTION, попутно передав управление функции act_GetDestination и не забыв заново проинициализировать глобальные переменные, необходимые для осуществления нового звонка.
proc act_ConnDestroyed { } {
leg disconnect leg_outgoing;
init_perCallVars;
act_GetDestination;
fsm setstate DESTSELECTION;
return;
}
proc act_Cleanup { } {
call close;
}
requiredversion 2.0
init
set ivr_fsm(any_state,ev_disconnected) "act_Cleanup same_state";
set ivr_fsm(CALLCOMES,ev_setup_indication) "act_Setup same_state";
set ivr_fsm(CALLCOMES,ev_collectdigits_done) "CheckLangSelection CHECKLANG";
set ivr_fsm(CHECKLANG,ev_media_done) "SelectLanguageMenu CALLCOMES";
set ivr_fsm(CARDSELECTION,ev_collectdigits_done) "act_GotCardNumber same_state";
set ivr_fsm(CARDSELECTION,ev_authorize_done) "act_CardAuthorize same_state";
set ivr_fsm(DESTSELECTION,ev_collectdigits_done) "act_GotDestination same_state";
set ivr_fsm(DESTSELECTION,ev_authorize_done) "act_CallAuthorize same_state";
set ivr_fsm(PLACECALL,ev_media_done) "act_CallSetup same_state";
set ivr_fsm(PLACECALL,ev_setup_done) "act_CallSetupDone same_state";
set ivr_fsm(PLACECALL,ev_digit_end) "act_FastSetup same_state";
set ivr_fsm(CALLACTIVE,ev_digit_end) "act_LongPound CONNDESTROY";
set ivr_fsm(CALLACTIVE,ev_leg_timer) "act_ActiveTimer CALLWARN";
set ivr_fsm(CALLWARN,ev_destroy_done) "act_CallWarnDestroy same_state";
set ivr_fsm(CALLWARN,ev_media_done) "act_CallWarnMedia CALLLASTACTIVE";
set ivr_fsm(CALLLASTACTIVE,ev_leg_timer) "act_LastActiveTimer same_state";
set ivr_fsm(CALLLASTACTIVE,ev_destroy_done) "act_PlayDisconnect CALLDISCONNECT";
set ivr_fsm(CALLLASTACTIVE,ev_digit_end) "act_LongPound CONNDESTROY";
set ivr_fsm(CONNDESTROY,ev_destroy_done) "act_ConnDestroyed same_state";
set ivr_fsm(CALLDISCONNECT,ev_media_done) "act_Cleanup same_state";
set ivr_fsm(CALLDISCONNECT,ev_disconnect_done) "act_Cleanup same_state";
fsm define ivr_fsm CALLCOMES;
Facebook
Мой мир
Вконтакте
Одноклассники
Google+
|