Modify FreePBX call reports to show destination channel

FreePBX‘s Reports module does not show the destination channel–for example, the outbound trunk, or the device that received an incoming call–by default. But you can customize the call log display without editing the module itself. The Reports module looks for a file at /etc/asterisk/call-log-table.php, and if it finds the file, uses the declarations within to redefine the column layout. I took the default and modified it to include the Destination Channel column.

Just create a new file at /etc/asterisk/call-log-table.php with the following contents:

<?php   $FG_TABLE_COL[]=array ("Timestamp", "calldate", "12%", "center", "SORT", "19");
        $FG_TABLE_COL[]=array ("Src Chan", "channel", "14%", "center", "", "30", "",
"", "", "", "", "display_acronym");
        $FG_TABLE_COL[]=array ("Source", "src", "10%", "center", "", "30");
        $FG_TABLE_COL[]=array ("CLID", "clid", "25%", "center", "", "80",'','','','',
'','filter_html');
        $FG_TABLE_COL[]=array ("Dest", "dst", "10%", "center", "SORT", "30");
        $FG_TABLE_COL[]=array ("Dst Chan", "dstchannel", "14%", "center", "", "30",
"", "", "", "", "", "display_acronym");
        $FG_TABLE_COL[]=array ("Disposition", "disposition", "9%", "center", "", "30");
        if ((!isset($resulttype)) || ($resulttype=="min"))
$minute_function= "display_minute";
        $FG_TABLE_COL[]=array ("Duration", "duration", "6%", "center", "SORT", "30",
"", "", "", "", "", "$minute_function");

        $FG_TABLE_DEFAULT_ORDER = "calldate";
        $FG_TABLE_DEFAULT_SENS = "DESC";

        // This Variable stores the argument for the SQL query
        $FG_COL_QUERY='calldate, channel, src, clid, dst, dstchannel, disposition,
duration';
        $FG_COL_QUERY_GRAPH='calldate, duration';

        // The variable LIMITE_DISPLAY define the limit of record to display by page
        $FG_LIMITE_DISPLAY=25;

        // Number of column in the html table
        $FG_NB_TABLE_COL=count($FG_TABLE_COL);

        // The variable $FG_EDITION define if you want process to the edition of the
// database record
        $FG_EDITION=true;

        //This variable will store the total number of columns
        $FG_TOTAL_TABLE_COL = $FG_NB_TABLE_COL;
        if ((isset($FG_DELETION) && $FG_DELETION) || $FG_EDITION)
$FG_TOTAL_TABLE_COL++;

        //This variable define the Title of the HTML table
        $FG_HTML_TABLE_TITLE=" - Call Logs - ";

        //This variable define the width of the HTML table
        $FG_HTML_TABLE_WIDTH="100%";
?>

Appendix: analog remix debugging and notes

Part 1: Intro, Part 2: Tropo/PHP, Part 3: Asterisk Manager/Perl

These notes are from a weekend of hacking and debugging with Michigan Telephone, the first guinea pig to try out the “analog remix” scripts. Thanks for the extra challenges. 🙂

Asterisk 1.4, 1.8 and the Asterisk Manager Interface

  • Asterisk 1.4’s Manager calls the channel that is bridged to the current channel “Link.” Newer versions, more sensibly, call it “BridgedChannel.” I updated the Perl CGI to use whichever is defined, so now the CGI will work with newer versions of Asterisk, and not just 1.4.
  • A bug exists on specific versions of Asterisk 1.4, 1.6 and 1.8 that breaks call transfer from the Manager (as well as SIP blind transfers). The current -rc1 versions, as of this writing, have the fix: 1.4.39-rc1, 1.6.2.16-rc1, and 1.8.2-rc1. This bug has been in 1.4 since 1.4.38-rc1; in 1.6 since 1.6.2.15-rc1; and in 1.8 since 1.8.0-rc4. If you’re on a buggy version, the transfer will just hang up the channel.
  • How to set up the Manager user may not be obvious. Just make a new user block in manager.conf or manager_custom.conf (FreePBX) with the following permissions:

    [tropousername]
    secret = tropopassword
    deny=0.0.0.0/0.0.0.0
    permit=127.0.0.1/255.255.255.0
    read = call
    write = call

    Set the username and password to whatever you want, and then use these in the Perl CGI in the AMI::Common::new() function. Your manager user needs only “call” permissions to perform the actions in the CGI.

Perl

  • MT notes that you may have to install the AnyEvent and parent Perl modules before Asterisk::AMI. Use CPAN’s installer if you can: perl -MCPAN -e shell and install parent, AnyEvent, and Asterisk::AMI
  • The Data::Dumper module can be really helpful if you find that things aren’t going well with your CGI. Just use Data::Dumper at the top of the CGI, then print STDERR Dumper($variablename) wherever you need it. Try this on a hash, such as $currchan, and you’ll get a dump of the whole thing.

Firewall/ACL

You may want to set up a separate Apache VirtualHost with access controls (username/password and/or IP access list) for your Tropo CGI. Or, if your web server is firewalled, you’ll need to let Tropo in to make the request. Tropo’s support team indicated that requests will come from 66.193.54.21, so this IP can be allowed through your firewall. They will announce any changes to this on changes.tropo.com.

Analog remix part 3: some Perl CGI to route the call

Part 3 of the analog remix ties it all together. We need a way for Tropo to tell Asterisk to transfer the call, since we’re not going to use SIP REFER. A simple Perl CGI that connects to Asterisk Manager Interface (AMI) will do the trick.

I’m sticking with old-school Perl for this one because I really like Ryan Bullock’s Asterisk::AMI modules. His code is easy to use and he keeps it up to date.

Referring back to part 2, you can see where the curl function calls tropo.cgi (the Perl script below) with parameters f=xfer (the only function for now), callid=the SIP Call-ID, and dest=the number the caller requested. The Perl script finds the Call-ID by iterating through the channel list, finding one that’s connected to Tropo, and then checking the Call-ID to make sure it’s the exact one. (You could have more than one channel open to Tropo.) Once it finds that, it transfers the second leg of that call, which is the caller. Output is to STDERR so that I can review the transaction in the Apache error log.

Updated 12/19/2010: This line

my $link = $currchan->{'Link'} || $currchan->{'BridgedChannel'};

is updated to use either the key returned by Asterisk 1.4 (“Link”) or by later versions (“BridgedChannel”) in the channel listing.

The code:

#!/usr/bin/perl

use strict;
use Asterisk::AMI::Common;
use CGI;
use URI::Escape;

select(STDERR);
local $| = 1;
select(STDOUT);
local $| = 1;

my $cgi = CGI->new;
my $func = $cgi->param('f');
my $callid = uri_unescape($cgi->param('callid'));
my $dest = $cgi->param('dest');

print $cgi->header("text/plain");

if(defined $func) {
  if($func eq "xfer") {
    if(defined $dest && $dest ne "") {
      my $ami = Asterisk::AMI::Common->new (PeerAddr => 'localhost',
                                    PeerPort => '5038',
                                    Username => 'USERNAME',
                                    Secret => 'PASSWORD',
                                    Events => 'off',
                                    on_error => &errorHandler
                                   )
      or die "Unable to connect to Asterisk Manager.n";

      my $channels = $ami->channels;
      if (defined($channels)) {
        foreach my $channel (keys %{$channels}) {
          my $currchan = %{$channels}->{"$channel"};
          if ($channel =~ m/tropo.com/) { # find channels connected to Tropo
            my $currchan_callid = $ami->get_var($channel, "SIPCALLID");
            if ($currchan_callid == $callid) { # make sure it's the right call,
              print STDERR "Channel $channel"; # identified by Call-ID
              print STDERR "Callid $callid";

              my $link = $currchan->{'Link'} || $currchan->{'BridgedChannel'};
              if (defined($link)) { # this is the initiating leg of the call
                my $transfer = $ami->transfer($link, $dest, "from-internal");
                if (defined($transfer)) {
                  if ($transfer == 1) {
                    print STDERR "Tropo successfully transferred $link to $dest.";
                  } else {
                    print STDERR "Tropo FAILED to transfer $link to $dest.";
                  }
                } else {
                  print STDERR "System error performing transfer.";
                }
              } else {
                print STDERR "Local channel leg not found.";
              }
            }
          }
        }
      } else {
        print STDERR "Could not get channel list from Asterisk manager.";
      }
      $ami->disconnect; # close Manager connection
    } else {
      print STDERR "xfer requested but no dest specified.";
    }
  } else {
    print STDERR "Invalid function sent in CGI.";
  }
} else {
  print STDERR "No function sent in CGI.";
}

print "Done.n";
exit;

#### Functions ####

sub errorHandler {  # A fatal error occurred
  my ($amiobj, $errmsg) = @_;
  print STDERR "AMI Fatal error: $errmsgn";
  exit 1;
}

Summary

Starting with part 1, configure a hot line or warm line for your old analog device to automatically ring an extension on your system. You’ll define that extension as the SIP URI to the Tropo application you made in part 2. The Tropo application, in turn, will connect back to your Asterisk server running web services (Apache) with a CGI that can take the input and connect to the Asterisk Manager Interface and transfer your call. Simple! 🙂

SIP softphone pick for the iPad

I decided to find a SIP phone for the family iPad. I started out with three simple criteria:

  • It’s free (I’m a cheapskate)
  • It works, especially in speakerphone mode, because this is an iPad after all
  • It pops up an incoming call alert if backgrounded

Of those that are free, I eliminated any that require you to make an account with their own service.

I didn’t keep a top-ten list or even a top-three. I just installed and subsequently deleted a bunch of free SIP phones. To my surprise, most didn’t really work. But I found one that does, has nice sound quality with speakerphone, a good clean interface and can even register to multiple accounts. That app is NetDial SIP Phone by NeoMecca. It’s a Korean company, and their web site is all in Korean, but the app description on iTunes and the app itself are in English. Configuration is straightforward. To make it pop up incoming calls when in the background, because I’m using Asterisk 1.4 as my PBX, I had to choose the UDP “keep alive” option. (Asterisk 1.4 does not have TCP SIP.)

The mic is at the top of the iPad and the speaker at the bottom. This separation pretty much eliminates any typical echo problem you might expect, but I’m sure if you have the volume cranked way up you could generate some. At what I consider to be normal volume, it sounds great.

Analog remix part 2: Tropo

Tropo, a project of Voxeo Corporation, adds voice and short-message-communications functions to PHP, JavaScript, Ruby, Python, and Groovy through a set of classes. They host the platform and your scripts, which are then triggered through the web or by PSTN/SIP, SMS, Twitter, or chat events. You can use the system as long as you want for free for development, or pay reasonable hosting rates to go production. Oh, and their support team is terrific, even for developers who haven’t yet paid them a dime.
So what can you do with it?
  • Interactive voice-response (IVR) systems
  • Chat or Twitter robot
  • Dialer
  • Call routing based on logic in the script
  • Text-to-speech
  • Speech-to-text
  • Audio recording
  • Whatever you can normally do with the aforementioned scripting languages, but note that the stdout will be a communications outlet such as a SIP channel, a Twitter feed or conversation, a chat, or an SMS–not a web page
My first Tropo application, the “local operator,” is the topic of part two of the analog remix. The platform has so many features, though, that I look forward to spending more time developing in it.
I chose to use PHP because it’s the language with which I am most familiar and also has Curl libraries built in, which I am using to make a CGI callback to my own Asterisk server. The purpose should become clear when you read the code, but you may ask, “why not just send a SIP REFER to do the call transfer?” The answer is that you can’t with the Tropo platform. All transfers go through Tropo; there’s no option for a REFER. So if you want to use your own dialplan and not have your call going out to Tropo and back to use local resources, you have to do the transfer through the Asterisk Manager, thus the CGI call.
Number, Please?
The script answers the call (which will come in by SIP URI), asks, “Number, please?”, validates and confirms the spoken number with the caller, then uses the Curl functions to go back to the calling Asterisk server (running a web server) and transfer the call.
Getting There From Asterisk
Once you’ve written a script and uploaded it to  Tropo (or typed it directly into Tropo’s file-editing interface), you go to the Applications section and choose your script as the source. Once you’ve submitted it, you’ll be given a SIP URI (amongst other methods) by which to access the application. So as part of a FreePBX Custom Extension or in your extensions.conf file, you can make an extension for your new application by referencing SIP/somenumber@sip.tropo.com. I set mine up as extension 1100, which I mentioned in the previous post.
Now, if I go off hook from one of my internal extensions and dial 1100, or simply pick up the analog extension on which I configured the Warm Line/Hot Line, I’ve got the operator ready to take my number and transfer my call. Only one piece left after this: the Asterisk Manager CGI script.

Appendix: the PHP code (Number, Please?)

<?php

// Main

// SSML tags needed for advanced text-to-speech manipulation
$ssml_start = "<?xml version='1.0'?><speak>";
$sayas_end="</say-as>";
$sayas_digits ="<say-as interpret-as='vxml:digits'>";
$ssml_end = "</speak>";

answer();

// These IDs are used to identify the caller and the Asterisk box
$callid = $currentCall->getHeader('x-sbc-call-id');
$hostid = trim((strstr($callid, '@')),'@');

_log("Call-ID is $callid");
_log("Host is $hostid");

wait(1000);

$haveNumber = false;

while (!$haveNumber) {
$number = ask($ssml_start . "Number <emphasis level='reduced'>please?</emphasis>" .
$ssml_end,
array(voice => "allison",
attempts => 3,
bargein => false,
choices => "[4-16 DIGITS]",
minConfidence => 0.4,
timeout => 8,
onBadChoice => "handlerBadChoice",
onError => "handlerError",
onHangup => "handlerHangup",
onTimeout => "handlerTimeout"
));

// Confirm the number with the caller
// 1 and 0 are options for testing with DTMF
$yesno = ask($ssml_start . $sayas_digits . $number->value . $sayas_end .
", is this correct?" . $ssml_end,
array(voice => "allison",
attempts => 2,
bargein => false,
choices => "yes, no, 1, 0",
minConfidence => 0.5,
timeout => 3,
onBadChoice => "handlerBadChoice",
onError => "handlerError",
onHangup => "handlerHangup",
onTimeout => "handlerTimeout"
));

_log("Caller said $yesno->value.");

if ($yesno->value == "yes" || $yesno->value == "1") {
$haveNumber = true;
} else {
say("OK, let's try again.");
}
}

// The CGI callback. Refer to a future posting for the design
// Customize for your server. Use SSL for security if you can.
$url = "http://$hostid/tropo.cgi?f=xfer&callid=" . urlencode($callid) .
"&dest=" . $number->value;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "USERNAME:PASSWORD");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);

_log("Received from CGI: " . $result = curl_exec($ch));

if (!$result) {
_log(curl_error($ch));
}
curl_close($ch);

// If the transfer didn't happen, the caller is still here...

if ($currentCall->isActive()) {
_log("Transfer failed and caller still on the line.");
say("I'm sorry, something went wrong. Please try again later.");
hangup();
}

return;
// END OF MAIN

// Function definitions

function handlerBadChoice($event) {
say("I'm sorry, I didn't understand you.");
if($event->attempt >= 3) {
say("Goodbye.");
hangup();
}
return;
}

function handlerError($event) {
_log("Error occurred: $event->value");
say("I'm sorry, an error occurred. Goodbye");
hangup();
return;
}

function handlerHangup($event) {
_log("Caller hung up.");
exit;
}

function handlerTimeout($event) {
say("I'm sorry, I didn't hear you.");
if($event->attempt >= 3) {
say("Goodbye.");
hangup();
}
return;
}

?>

Asterisk 1.8+Google Voice fixed

The Asterisk team released a small patch that fixes the protocol issue that was keeping Asterisk 1.8 users from dialing out through Google Voice.

You can apply the patch against version 1.8.0 or 1.8.1. And if you’ve never used the patch command before, it’s easy:
  • Go to the Asterisk bug tracker and get the attached file named fix

  • Put it in your Asterisk source directory (the root, where the configure script, Makefile, and other such files exist). Call it gtalkfix.patch, to be a little less ambiguous.
  • From that same directory, run the command patch < gtalkfix.patch . If it says it can’t find the file to patch, specify channels/chan_gtalk.c. Oh, and if you don’t have the patch command, just yum install patch first.
  • You should see something like this:
    patching file channels/chan_gtalk.c
    Hunk #1 succeeded at 193 with fuzz 2 (offset -3 lines).
    Hunk #2 succeeded at 463 (offset -5 lines).
    Hunk #3 succeeded at 1006 (offset -3 lines).
    Hunk #4 succeeded at 1814 (offset -5 lines).
  • Now run make and you will see it compile chan_gtalk again. Of course, if you are applying it against the raw distribution, you’ll go through the entire build process.
  • make install and start Asterisk and you’ll be able to call through Google Voice again.

Asterisk 1.8+Google Voice broken; FreeSWITCH still works

As some folks commented, Google Voice seems to have stopped accepting outbound calls from Asterisk 1.8. There’s a bug report open with Asterisk on this issue, and several discussions in user forums.

I fired up FreeSWITCH again, without updating any code (still the source from mid-October, 1.0.head (git-dc40a77 2010-10-17 16-19-38 -0400)), and it works perfectly. Apparently, the FreeSWITCH guys are using a different method to make the Google Voice connection, or the Google folks are specifically filtering Asterisk or have made a change that affects Asterisk and to which FreeSWITCH is immune.

Hold the line… maintenance needed

There must be a Murphy-like law for this. Whenever creativity, development, and other forward-progress are at their best, a maintenance issue pops up and puts everything on hold while it demands attention.

I’m fixing–or at this point, still troubleshooting–quality issues that have suddenly come up on my main line at home. It is an IAX2 trunk, and suddenly it’s noisy, jittery, a big mess. Network seems clean, but it seems like the Internet is eating voice packets. What happened? The logs are puzzling, and even with a very bright fellow at the other end of the trunk, it’s not working. Repair time.
As a systems administrator (speaking of my job now, though I tend to be a systems admin at home, too), my first priority is to fix what is broken. Things break right when I have some creativity going in another area. Time to refocus the creativity from development to troubleshooting. Sometimes a creative repair is almost as good as a creative design. But never as good.