Tuesday, September 2, 2008

Sending DTMF tones inband part 2.

Well, I finished my first TCL IVR script. As mentioned earlier, I have a problem where I am required to create "short-cut" dial-string. This will enable users to dial a short number e.g. 5551 that will then make a call to the PSTN and send a string of digits via DTMF.

As there seems to be many occasions when this may be handy I decided to try and make what ever script I wrote as generic as possible so that I could easily change the number that was dialed, and the DTMF strings that would be sent.

As this is my first TCL IVR script, if anyone sees any obvious mistakes or short-cuts please let me know.

There really does not seem to be to much information out there regarding the development or explanation of TCL IVR scripts for IOS, so started by reading the Cisco TCL IVR 2.0 Programming guide, It is a good starting point, though I did find things still very murky. This is where I turned to the two other helpful resources... Google, and the TCL wiki page.

The other thing I found quite frustrating, is the whole development process debugging and testing involved many steps:
  • Script would edited/corrected on my laptop
  • Script would then copied to the router
  • then application would have to be reloaded
  • finally I would dail into the script, observer any errors then start the process again...
After 50 or 60 times this becomes quite laborious.

The following paragraphs go through the script I have written, and hopefully covers some of the things that I learned. Once again, if there any obvious mistakes then please let me know, also If you would like a copy of the script, and prompts let me know.

First up a bit about the FSM

TCL IVR scripts make use of a FSM (finite state machine) depending what you want your script to do, you define a set of actions (procedures) that are run when the state of the FSM changes. These procedure may change the state of the FSM directly , or alternatively the reponse to a procedure may cause an event that can be acted upon if the FSM has state defined that matches the action.. Confused hmm you arent the only one.

An example:

the FSM is in a "CONNECTED" state a procedure has been actioned that plays a media prompt. once the prompt has played out, an event is fired "ev_media_done" if the FSM has an entry defined "CONNECTED" and "ev_media_done" then the procedure that is a associated with this state and event is called.

It does take some getting you head around, especially if you have not have not had much to do with FSM's. The otehr thing to note, is that when a procedure is called, the code in the procedure is run one after the other, there is no waiting for a function to complete before the next step is taken.. eg. If a procedure is called that plays a prompt then sends some DTMF tones, if you do not make use of the FSM to manage the steps, you willl start playing the prompt, and then the tones will be immediatley played over the top of the prompt.

Anyway back to my script.

The first thing to do is to initalise a pile of variables, some of these are read from the router configuration, others are internal to the script.

proc init { } {
global param
global callInfo
global legConnected
global instructionsPrompt
global pleaseWaitPrompt
global invalidOptionByePrompt
global divertSetPrompt
global DTMFStrings
global pattern
global validKeys
global PSTNDivertNumber
global currentDigit
set currentDigit 0
set param(interruptPrompt) true
set param(abortKey) *
set param(terminationKey) #
set instructionsPrompt "flash:en_welcome_prompt.au"
set pleaseWaitPrompt "flash:en_dialing_prompt.au"
set invalidOptionByePrompt "flash:en_invalid_option_prompt.au"
set divertSetPrompt "flash:en_divert_set_prompt.au"
set legConnected false #check that the PSTNDivertNumber has been configured
if [infotag get cfg_avpair_exists PSTN-Divert-Number] {
set PSTNDivertNumber [string trim [infotag get cfg_avpair PSTN-Divert-Number]]
} else {
set PSTNDivertNumber "NONE" puts -nonewline "TCL EMG: -- ERROR: Mandatory parameter PSTN-Divert-Number does not exist --"
set configFail 1
call close
}
set DTMFStrings(0) 0
set pattern(0) 0
set validKeys {{0}}
for {set var 1} { $var <= $numberOfOptions } {incr var} {
if [infotag get cfg_avpair_exists DTMFString$var] {
set DTMFStrings($var) [string trim [infotag get cfg_avpair DTMFString$var]]
lappend validKeys $var
set pattern($var) $var
}
}
}

The next thing that is required is to set the FSM this defines what actions/functions are called based on the "state" of the script.

set fsm(any_state,ev_disconnected)   "act_Cleanup same_state"
set fsm(CALL_INIT,ev_setup_indication) "act_Setup   GETDEST"
set fsm(GETDEST,ev_collectdigits_done) "act_GotDest   CONNECT"
set fsm(CONNECT,ev_media_done)  "act_OutboundCall   CONNECTED"
set fsm(CONNECTED,ev_setup_done)  "act_SendDigit   same_state"
set fsm(CONNECTED,ev_media_done)  "act_SendDigit   same_state"
set fsm(FINISHED,ev_media_done) "act_Cleanup    same_state" 
fsm define fsm CALL_INIT

The first thing that happens when a call is received, is that we receive a ev_setup_indication message.  From the state table, we can see that when this message is received, the act_Setup function is called and the "state" is set to GETDEST.

proc act_Setup { } {
    global param
    global dest
    global instructionsPrompt
    global pattern
    global currentDigit
    set currentDigit 0
    set param(maxDigits) 1
    leg setupack leg_incoming
    leg proceeding leg_incoming
    leg connect leg_incoming
    media play leg_incoming $instructionsPrompt
    leg collectdigits leg_incoming param pattern
}

From this function we can see that once the call leg has been set up, a prompt is played, then we collect the digit that is input by the end user.  Once the digit is receive an "ev_collectdigits_done" is received,  from the FSM, we can see that when we receive this message and we are in the GETDEST state, the script should run the act_GotDest function and set the state to CONNECT.

proc act_GotDest { } {
    global digit
    global validKeys
    global pleaseWaitPrompt
    global invalidOptionByePrompt
    set digit [infotag get evt_dcdigits]
    puts "\nDIGIT DIALED: $digit"
    #check that digit is valid:
    set x [lsearch $validKeys $digit]
    if {$x >= 1} {
            media play leg_incoming $pleaseWaitPrompt
        } else {
            puts "\n WRONG NUMBER!!!"
            media play leg_incoming $invalidOptionByePrompt
            fsm setstate FINISHED
        }
}

This function does a basic check, that will terminate the call if an incorrect digit was received.  If a valid digit was received the user is told to wait while the outbound call is made and DTMF string is sent.

proc act_OutboundCall {} {
    global PSTNServiceNumber
    leg setup $PSTNServiceNumber callInfo leg_outgoing
}

On completion of the call setup, a ev_setup_done message is received, all that is done now is to send the digits:

proc act_SendDigit { } {
    global digit
    global DTMFStrings
    global currentDigit
    global stringLength
    #global silencePrompt
    global pleaseWaitPrompt
    global divertSetPrompt
    #global shortSilencePrompt
    set stringLength [string length $DTMFStrings($digit)]
    set d [string index $DTMFStrings($digit) $currentDigit]
    if {$currentDigit < $stringLength}  {
        switch $d  {
           "," {
                media play leg_outgoing %s2000
                set currentDigit [expr {$currentDigit + 1}]
                } 
        default {
                leg senddigit leg_outgoing $d -t 200    
                set currentDigit [expr {$currentDigit + 1}]
                media play leg_outgoing %s400
                }
        }
    } else {
        media play leg_incoming $divertSetPrompt
        fsm setstate FINISHED
        }
}

This function is responsible for sending the digits.  As we can only send one digit at a time, we have to loop through the DTMF string.   I have added the ability to add pauses by using a comma.  As the digits need to be separated by silence, we use the "media play" function to introduce this silence, it is also used as the trigger to send the next digit.

Once all this is completed, the service needs to be configured on the rtr.

application
service sendDTMF flash:send-dtmf-1.0.0.10.tcl
param PSTN-Service-Number 95556789 #the PSTN number that is dialed
param number-Of-Options 5 #number of options
param DTMFString1 ,,045433333,*,5555*,,*2,045433333

Once this is done, the last thing that is required is for a dial-peer to be set up.

Well thats it, if you have any questions, please contact me.