Dialer: tracking calls with Asterisk/AMI

Long story short: I was asked to write a real time dialer callable from a web page that takes the number from the parameters received, make the call, and play a pre-recorded message when the called picks up. This post is not about the dialer itself, it’s about tracking the call originated by it, detect the “answer” event, detect the “hangup” event and save a CDR.

The approach I chose involves AMI to generate and track the calls. It offers a really nice interface based on a text protocol and the most important thing, the API can be mapped in almost any programming language like C# (Asterisk.NET).

Some of the protocol internals

  1. The connection is made via TCP/IP sockets using user/password authentication.
  2. Once logged in, you receive every event on the system, even if that event doesn’t belong to you. This could be a real mess because it introduces a lot of noise into the dialer and the software must support some kind of filtering mechanism.
  3. Every event comes with certain data, including a field named Uniqueid which relates the events to a transaction.
  4. The order the events were received not necessarily match the order they were generated.

The native way to generate a call is through the OriginateAction but the problem is that OriginateAction doesn’t reply with the uniqueID of the transaction, it just tells you whether the call was successfully queued or not. Then, you start receiving the events with the uniqueIDs but you have no way to know if that call belongs to you, to another dialer instance, to a human making a call, etc.

Searching the web I found what I thought was the solution: use asynchronous origination.  The usual OriginateAction is a blocking call that returns only if success or failure. With async origination the answer to the request is delivered through the OriginateResponse event and with the parameters you receive, the so wanted uniqueID. I built the dialer on top of this “fix” just to later discover that if the call fails, the uniqueID is empty. Once again, you have no way to know which call failed and for what reason.

I was so pissed off because something as basic as this wasn’t correctly available, or at least, there is no documentation to expose an standard solution. I started considering parallel approaches to abandon AMI but reading the input parameters for OriginateAction a dirty workaround came to my mind.

OriginateAction takes as input a field named CallerID, this field is always available and always filled with information. I used this field to uniquely identify the call whether it succeeded or not. In my particular case, the calls goes to a GSM network so overwriting the callerID has no effect and the call follows its normal destination.

Bellow is part of the dialer class. If you can read C#, it would explain itself.

[csharp]

public class Dialer
{
ManagerConnection _managerConnection;
Timer _timer;
public delegate void CallFinalizedCallback(Dialer sender);
string _uniqueID = string.Empty;
bool _wasOriginated = false;
object _locker = new object();

List<HangupEvent> _hangupList = new List<HangupEvent>();

CallFinalizedCallback _callFinalized;
public CallFinalizedCallback CallFinalized
{
get {
return this._callFinalized;
}
set {
_callFinalized = value;
}
}

CallInfo _callInfo;
public CallInfo CallInfo
{
get
{
return this._callInfo;
}
}

public Dialer()
{
_managerConnection = new ManagerConnection(Configuration.ManagerHost,
Configuration.ManagerPort,
Configuration.ManagerUserName,
Configuration.ManagerPassword);

//_timer = new Timer(Handle_timeoutCallback, Configuration.CallNotificationTimeout);

AddEventHandlers();
}

void Handle_managerConnectionOriginateResponse (object sender, OriginateResponseEvent e)

{
if (!e.CallerIdNum.Equals(_callInfo.CallId.ToString()))
return;

Logger.Print(string.Format(“Call to {0}-{1} was originated”, _callInfo.CallId, e.Exten));

bool succeeded = e.Response.ToLower().Equals(“success”);

if (succeeded)
UpdateDialoutQueue(CallStatus.Established);
else
UpdateDialoutQueue(CallStatus.FinishedWithError);

lock(_locker)
{
_wasOriginated = true;
_uniqueID = _callInfo.CallId.ToString();
}

foreach(HangupEvent hEvent in _hangupList)
{
if (hEvent.CallerIdNum.Equals(e.CallerIdNum)) // the call failed for some reason
{
Handle_managerConnectionHangup(this, hEvent);
break;
}
}
_hangupList.Clear();

}

public Dialer(CallInfo callInfo, CallFinalizedCallback callback) : this()
{
_callInfo = callInfo;

if (callback == null)
throw new ArgumentException(“CallFinalizedCallback cannot be null”);

_callFinalized = callback;
}

void AddEventHandlers()
{
_managerConnection.Hangup += Handle_managerConnectionHangup;
_managerConnection.OriginateResponse += Handle_managerConnectionOriginateResponse;
}

public void DisconnectFromAMI()
{
_managerConnection.Hangup -= Handle_managerConnectionHangup;
_managerConnection.OriginateResponse -= Handle_managerConnectionOriginateResponse;
}

void Handle_timeoutCallback()
{
try
{
Logger.Print(string.Format(“Call {0}-{1} forced to finalize after timeout”, _callInfo.CallId, _callInfo.Extension));

UpdateDialoutQueue(CallStatus.Timedout);
//_timer.CancelAlarm();

if (_callFinalized != null)
_callFinalized(this);

DisconnectFromAMI();
}
catch(Exception ex)
{
Logger.Error(string.Format(“Handle_timeoutCallback: {0}”, ex.Message));
DisconnectFromAMI();
}
}

void Handle_managerConnectionHangup(object sender, HangupEvent e)
{
try
{
lock(_locker)
if (!_wasOriginated || !_uniqueID.Equals(e.CallerIdNum))
{
_hangupList.Add(e); // register this event in case it was fired before the OriginateAction event
return;
}

UpdateDialoutQueue(CallStatus.Finished, e.Cause, e.CauseTxt);
//_timer.CancelAlarm();

Logger.Print(string.Format(“Call {0}-{1} was hung up”, _callInfo.CallId, e.Channel));

if (_callFinalized != null)
_callFinalized(this);

DisconnectFromAMI();
}
catch(Exception ex)
{
Logger.Error(string.Format(“Handle_managerConnectionHangup: {0}”, ex.Message));
DisconnectFromAMI();
}
}

public void Call()
{
Call(_callInfo);
}

public void Call(CallInfo callInfo)
{
try
{
OriginateAction originate = new OriginateAction();

originate.Channel = callInfo.Channel;
originate.Priority = callInfo.Priority;
originate.Context = callInfo.Context;
originate.Exten = callInfo.Extension;
originate.CallerId = callInfo.CallId.ToString(); // <——–
originate.Timeout = 500000;
originate.Async = true;

_managerConnection.Login();

MarkCallAsProcessed();

//_timer.SetAlarm();

UpdateDialoutQueue(CallStatus.Trying);
ManagerResponse r = _managerConnection.SendAction(originate, originate.Timeout);
}
catch(Exception ex)
{
Logger.Error(string.Format(“Call: {0}”, ex.Message));

//_timer.CancelAlarm();

if (ex is Asterisk.NET.Manager.TimeoutException)
{
UpdateDialoutQueue(CallStatus.Timedout);
DisconnectFromAMI();
}
else
UpdateDialoutQueue(CallStatus.FinishedWithError);
}
}
}

[/csharp]

 

3 thoughts on “Dialer: tracking calls with Asterisk/AMI

  1. I hit your page trying to track this same problem, but I *am* seeing the ActionID from the my Originate action in the OriginateResponse event, even if the call fails (e.g. if the SIP endpoint I am testing with is unregistered.) This is with Asterisk 11.4.0. I wonder if this was an issue addressed later?

    Compare (this is StarPy debug output):

    DEBUG:AMI:MSG OUT: {‘application’: ‘AGI’, ‘actionid’: ‘localhost.localdomain-28788496-2’, ‘variable’: ”, ‘async’: ‘yes’, ‘data’: ‘agi:async,hello’, ‘action’: ‘originate’, ‘channel’: ‘SIP/200’}

    and

    DEBUG:AMI:Incoming Message: {‘uniqueid’: ”, ‘exten’: ”, ‘calleridnum’: ”, ‘event’: ‘OriginateResponse’, ‘reason’: ‘0’, ‘actionid’: ‘localhost.localdomain-28788496-2’, ‘context’: ”, ‘calleridname’: ”, ‘privilege’: ‘call,all’, ‘response’: ‘Failure’, ‘channel’: ‘SIP/200’}

  2. @Matt, the action id is to track your initiated action, it is not the same as the unique id, which identifies the call between events.

    I also stumbled across this problem and solved it in a different way. I’m also using Asterisk.NET and noticed the VarSetEvent, but it wasn’t wired up. So I wired up the event (look at the other events on howto do this), and added my own tracking value to the OriginateAction by calling SetVariable(). When the VarSetEvent is triggered, you will receive both the unique id and your own variable (so you can match them).

  3. I am try to make sip phone using your code but i have some error in the code like in call info function. Please help me. i am new in this field. i hope for your reply

    Thanks
    Mangesh

Leave a Reply

Your email address will not be published. Required fields are marked *