Popular Posts

Tuesday, August 16, 2011

Develop IP-IVR on Cisco CallManager platform: 3

Now let's begin our first IVR application. As we mentioned before, an IVR application needs working with two fundamental interfaces: TAPI and Wave Driver for call control and media processing respectivelly.
Using TAPI:
1. Initialize the TAPI environment:   lineInitializeEx
eg. 
  nRet = lineInitializeEx(
    &gb_hLineApp,
    AfxGetInstanceHandle(),
    NULL,
    THEAPP_NAME,
    &dwChannelCnt,
    &gb_dwgTAPIVersion,
    lineParams );
2. Enum TAPI lines: lineGetDevCaps
eg.
  lv_lstLines.RemoveAll();
  for( DWORD i = 0;i < dwChannelCnt; i++ )
  {
   LINEEXTENSIONID lineExten;
   ZeroMemory(&lineExten, sizeof(lineExten));
   DWORD dwApiVer=0;
   nRet = lineNegotiateAPIVersion(gb_hLineApp, i, 0x00010003, 0x00030000, &dwApiVer, &lineExten);
   outputError(nRet);
   if( nRet == 0 )
   {
    LPLINEDEVCAPS pDev = new LINEDEVCAPS;
    pDev->dwTotalSize = sizeof(LINEDEVCAPS);
    nRet = lineGetDevCaps(gb_hLineApp, i, dwApiVer, 0, pDev);
    outputError(nRet);
    if( pDev->dwNeededSize > pDev->dwTotalSize )
    {
     dwSize = pDev->dwNeededSize;
     delete pDev;
     pDev = (LPLINEDEVCAPS)new BYTE[dwSize];
     pDev->dwTotalSize = dwSize;
     nRet = lineGetDevCaps(gb_hLineApp, i, dwApiVer, 0, pDev);
    }
    if( nRet == 0 )
    {
     sLineName = (LPCTSTR)((LPBYTE)pDev + pDev->dwLineNameOffset);
     WriteTrace(TraceDebug, "Debug - Enum get TAPI line: %d - %s, media mode: 0x%08X", i, sLineName, pDev->dwMediaModes);
     // Only accept Cisco CTI Ports
     /// Line Device
     if( (pDev->dwMediaModes & (LINEMEDIAMODE_AUTOMATEDVOICE | LINEMEDIAMODE_INTERACTIVEVOICE)) == (LINEMEDIAMODE_AUTOMATEDVOICE | LINEMEDIAMODE_INTERACTIVEVOICE) )
     {
      sUpperLineName = sLineName;
      sUpperLineName.MakeUpper();
      if( sUpperLineName.Find("CISCO LINE") >= 0 )
      {
       /// Sort by DN
       lv_nPos = sUpperLineName.Find("(");
       lv_nPos2 = sUpperLineName.Find(")");
       if( lv_nPos > 0 && lv_nPos2 > lv_nPos )
       {
        sTemp = sUpperLineName.Mid(lv_nPos+1, lv_nPos2-lv_nPos-1);
        sUpperLineName.Format("DN%s-%s:%d", sTemp, sLineName, i);
       }
       else
        sUpperLineName.Format("%s:%d", sLineName, i);
 
       // Sort the List
       lv_pos = lv_lstLines.GetHeadPosition();
       while( lv_pos != NULL )
       {
        lv_posTemp = lv_pos;
        sTemp = lv_lstLines.GetNext(lv_pos);
        if( sTemp > sUpperLineName )
        {
         lv_lstLines.InsertBefore(lv_posTemp, sUpperLineName);
         lv_pos = lv_posTemp;
         break;
        }
        else if( sTemp == sUpperLineName )
        {
         lv_pos = lv_posTemp;
         break;
        }
       }
       if( lv_pos == NULL )
        lv_lstLines.AddTail(sUpperLineName);
      }
     }
     }
    }
    delete pDev;
   }
  }
3. Open TAPI line and set events filter: lineOpen, lineSetStatusMessages
eg.
 ret = lineOpen(gb_hLineApp,
  m_ID,
  &m_hLine,
  m_dwApiVersion,
  0, 0, LINECALLPRIVILEGE_OWNER,
  m_mediaMode,
  NULL);
ret = lineSetStatusMessages(
  m_hLine,
  m_pLineDevCaps->dwLineStates,
  0);

  // Pickup after ? rings
  lineSetNumRings(m_hLine, 0, m_intRingCnt);
4.  Basic Call control APIs: lineAccept, lineAnswer, lineDrop, lineMakeCall...
eg:
  if( m_hCall )
    ret = lineAnswer(m_hCall, NULL, 0);
5. TAPI events processing: IVR should start a thread to track and process TAPI events
eg.
//*******************************************************************
//* A thread to receive TAPI line event to this thread
//*******************************************************************
UINT CTAPIManager::TapiEventThread(LPVOID pParam)
{
 CTAPIManager* pCM = (CTAPIManager*) pParam;
 MSG msg;
 CString sz;
 WriteTrace(TraceInfo, _T("SysInfo - TapiEventThread Started."));
 while( !s_threadTerminate )
 {
  switch( WaitForSingleObject(gb_hLineEvent, 10) )
  {
  // Have we got any events?
  case WAIT_OBJECT_0:
   // I did not catch errors here
   try
   {
    msg.message = WM_TAPI_LINEEVENT;
    pCM->ProcessMessage(msg);
   }
   catch( CTapiObj::TEx e )
   {
    sz.Format(_T("TapiEventThread Err: [code:%d],")
     _T("[date:%s],")
     _T("[time:%s],")
     _T("[reason:%s]\n"), e.code, e.date, e.time, e.result);
    WriteTrace(TraceError, sz);
   }
   catch(...)
   {
    WriteTrace(TraceError, _T("TapiEventThread Error!!!"));
   }
   break;

  // Sure we have nothing to process now
  case WAIT_TIMEOUT:
   break;
  // Go on waiting and processing
  default: continue;
  }
 }
 WriteTrace(TraceInfo, _T("SysInfo - TapiEventThread Stopped."));
 return 0;
}
6. When a call reaches a CTI RoutePoint, CTI RoutePoint should find an available IVR channel and redirect the call to that channel.
eg.
 if( lv_hCall )
 {
  ret = lineRedirect(lv_hCall, lpszDestAddress, 0);
 }
7. When get a call-offered event, usually the IVR channel (TAPI line) should accept the call, gether ANI/DNIS and enable DTMF collecting.
eg.
ret = lineGetCallInfo(hCall, lpCallInfo);
ret = lineMonitorDigits(lv_hCall, LINEDIGITMODE_DTMF);

Using Cisco Wave Driver to play and record:
1. Get Wave Handlers (In/Out) while opening TAPI line
eg.
long CTapiLine::GetWaveID()
{
 VARSTRING *pVs;
 LONG  ret=0;
 DWORD  dwWaveDev;
 DWORD  dwSize;
 CString  strDevClass;
 int   patience;
 //---------------------------------------------------------------------------
 for( int lv_loop = 0; lv_loop < 2; lv_loop++ )
 {
  // allocate memory for structure
  pVs = (VARSTRING *) calloc (1, 1024);
  memset(pVs, 0x00, 1024);
  // set structure size
  pVs->dwTotalSize = 1024; //sizeof(VARSTRING);
  if( lv_loop == 0 )
   strDevClass = _T("wave/out");
  else
   strDevClass = _T("wave/in");
  patience = 0;
  do
  {
   // We have not more patience, stop the loop
   if( ++patience > 100)
   {
    WriteTrace(TraceError, _T("TapiObj.cpp, IVR Channel: %d Line:%d GetWaveID(), Recovered from unlimited loop"), GetDeviceID(), m_ID);
    return -1;
   }
   // Get wave/out ID
   ret = lineGetID(
    m_hLine,
    0L,
    0L,
    LINECALLSELECT_LINE,  // LINECALLSELECT_CALL, LINECALLSELECT_LINE
    pVs,
    (LPCSTR)strDevClass);
   // check errors
   if( ret )
   {
    free( pVs );
    WriteTrace(TraceError, _T("TapiObj.cpp, IVR Channel: %d Line:%d lineGetID error: 0x%x"), GetDeviceID(), m_ID, ret);
    return -1;
   }
   // reallocate and try again
   if( pVs->dwTotalSize < pVs->dwNeededSize )
   {
    dwSize = pVs->dwNeededSize;
    free( pVs );
    pVs = (VARSTRING *) calloc(1, dwSize);
    pVs->dwTotalSize = dwSize;
    continue;
   } // end if (need more space)
   break; // success
  } while (TRUE);
  // copy wave id
  dwWaveDev = (DWORD) *((DWORD *)((LPSTR)pVs + pVs->dwStringOffset));
  free (pVs);
  if( lv_loop == 0 )
   m_dwWaveOutID = dwWaveDev;
  else
  {
   m_dwWaveInID = dwWaveDev;
   CCtrlWaveIn::SetMinDiskBuffer(0);
   CCtrlWaveIn::SetMaxDiskBuffer(640);
   CCtrlWaveIn::SetMaxSilenceTime(5);
   CCtrlWaveIn::SetActiveThreshold(500);
  }
 }
 return 0;
}

2.Open Wave In/Out devices
eg,
 if( m_dwWaveInID == 0 )
 {
  m_mmr = waveInOpen(0, WAVE_MAPPER, &m_pcmWaveFormat, 0, 0, WAVE_FORMAT_QUERY);
  if( m_mmr )
  {
   WriteTrace(TraceError, "IVR Channel: %d - CWaveInDevice(%d)::OpenDev0: waveInOpen error.", m_lngIVRChannelID, m_dwWaveInID);
   return FALSE;
  }
  m_mmr = waveInOpen(&m_hIn, WAVE_MAPPER, &m_pcmWaveFormat, m_dwAudioDevId, s_dwInstance, CALLBACK_THREAD);
 }
 else
 {
  m_mmr = waveInOpen(&m_hIn, m_dwWaveInID, &m_pcmWaveFormat, m_dwAudioDevId, s_dwInstance, CALLBACK_THREAD | WAVE_MAPPED);
 }
 if( m_mmr )
 {
  WriteTrace(TraceError, "IVR Channel: %d - CWaveInDevice(%d)::OpenDev: waveInOpen error.", m_lngIVRChannelID, m_dwWaveInID);
  return FALSE;
 }

 if( m_dwWaveOutID == 0 )
 {
  m_mmr = waveOutOpen(0, WAVE_MAPPER, &m_pcmWaveFormat, 0, 0, WAVE_FORMAT_QUERY);
  if( m_mmr )
  {
   WriteTrace(TraceError, "IVR Channel: %d - CWaveOutDevice::OpenDev0: waveOutOpen error 0x%x.", m_lngIVRChannelID, m_mmr);
   return FALSE;
  }
  m_mmr = waveOutOpen(&m_hOut, WAVE_MAPPER, &m_pcmWaveFormat, m_dwAudioDevId, s_dwInstance, CALLBACK_THREAD);
 }
 else
 {
  m_mmr = waveOutOpen(&m_hOut, m_dwWaveOutID, &m_pcmWaveFormat, m_dwAudioDevId, s_dwInstance, CALLBACK_THREAD | WAVE_MAPPED);
 }
3. Start wave working threads

4. Play / Record
eg.
m_mmr = waveOutPrepareHeader(m_hOut, pwh, sizeof(WAVEHDR));
m_mmr = waveOutWrite(m_hOut, pwh, sizeof(WAVEHDR));

5. Close Wave Devices and stop threads
eg.
m_mmr = waveOutReset(m_hOut);
m_mmr = waveOutClose(m_hOut);

Call Transfer:
Following the above steps, we can implement an Automaic Response System (ARS). But in most cases, callers need talk to the Customer Service Representative (CSR). IVR should be able to transfer call to an agent or a skillset. Generally, there're two methods to accomplish call transfering:
1. SingleStepTransfer (or BlindTransfer)
It means IVR transfers calls regardless of the result of operation. This method is offen used in Callcenters with ACD component.
eg.
ret = lineBlindTransfer(m_hConsultCall, lpszDigits, 0);

2. Consultant & Transfer/Conference
It means IVR should make sure the transfer destination receives the call or even answer the call before finishing the operation. So this method is a two-step operation: Consultant  and Transfer/Conference. If the destination doesn't receive or answer the call, IVR may retrieval the call for further treatment.
eg.
Step1:
ret = lineSetupConference(m_hCall, NULL, &m_hConferenceCall, &m_hConsultCall, 3, &lv_Param);
if( ret > 0 )
{
    ret = lineDial(m_hConsultCall, lpszDigits, 0);
}
Step2:
ret = lineCompleteTransfer(m_hCall, m_hConsultCall, NULL, LINETRANSFERMODE_TRANSFER);
or
ret = lineAddToConference(m_hConferenceCall, m_hConsultCall);

Call Flow designer:
Most of IVR products provide GUI for call flow design, users can customerize call flow by drag-and-drop. VXML is a wide accepted standard in this scenario. The following diagram is the structure of a typical IVR system named PowerVoice.

1 comment:

  1. I have UCCX installed. How can I call to my IPIVR script as there is no ICM.

    ReplyDelete