SMS Bustracker works with “Obama Phone”

TropoBustracker

A pinch of Tropo, a dash of Bustracker, fold in a pound of jata with PHP, bake for an hour and you have jata-sms.

My Android smartphone suffered a breakdown last week.    New parts should arrive soon.  But in the meantime, I was at a loss without my favorite Android app jata.  I use it to get bus arrival times with the local WRTA Bustracker.  Bustracker has a text message interface, and I thought I would try that with the backup TracFone (Safelink Obamaphone[1]).  That didn’t work.  The TracFone Terms and Conditions states that “TracFone does not generally participate in Premium SMS services or campaigns.”  The Bustracker SMS number is 41411, a shortcode for Textmarks, and I guess Textmarks is a premium service.  Then, I remembered Tropo, a gateway for voice and SMS.  It offers a free developer account and phone number, and now I had a motivation to work with it.[2]

Tropo Basics

I had looked at Tropo a while back, and didn’t really get it.  They offer some examples, ranging from a simple JSON web page to an overblown PHP example using the Limonade PHP framework.  I did not want to add another framework to my site and did some digging into the nuts and bolts. I’ll skip over the stuff that’s easy to locate on the Tropo site, such as getting a free developer account, and creating an application.  The short explanation is that once registered, you create a webAPI application, and specify a web page on your server which  will be invoked when the associated assigned phone number is called.  There is an option to set the app status as Development or Production.  In development, the app and phone number are free, in Production, the application starts at $50 / month and the number claims to be $3 / month. Having done that, you start to work on your application on your publicly accessible server.  I normally do PHP development on a local server, and use xdebug for debugging.  But because Tropo needs to contact my server, I would need to set up the port forwarding, and have yet another domain name and / or Dynamic DNS service.   My public server doesn’t enable xdebug. so debugging requires a little creativity.  I included debugging lines that put writes debug info to a local file.  If a session does not update the debug file, I assume that bad code caused PHP to exception or die, and I look at the server logs through the server control panel.  Here’s my simple logging function:

function append_log($s) {
    global $log_file;
    $tmstmp = date('Ymd:His');
    $handle = fopen($log_file, 'a');
    if($handle != FALSE) {
        fwrite($handle, $tmstmp.' '.$s.'\n');
        fclose($handle);
    }
}

I would normally use print_r(), but because my program, jatasms.php, is invoked by the Tropo service, I don’t get to see the output, as all the output goes back to Tropo as JSON, using the Tropo class RenderJson() function.

Tropo Classes and JSON

An early issue was locating the required Tropo class tropo.class.php.  I didn’t find any links to it in the Tropo tutorials, but a search engine located a Tropo blog post where the github link is mentioned.  You can download a zip file or use

$ git clone git@github.com:tropo/tropo-webapi-php.git

The Tropo class files are in there, along with some example programs. On thing I did not realize is that when Tropo sends a JSON request, the JSON is actually in the body of the POST.   All the information about the incoming call or SMS is found in the Session() object, and I was looking at PHP $_POST and friends to find the JSON.  It turns out, JSON is sent in the body and can be examined by using:

$jsonString = file_get_contents("php://input");

The same construct is used in tropo.class.php.  The two classes absolutely required are Session and Tropo.  The Session class encapsulates delivers the incoming call information to your script.

require_once 'tropo.class.php';
require_once 'jutils.php';
require_once 'tropo-bustracker-api.php';

try {
    $session = new Session();
} catch (TropoException $e) {
    append_log('Could not open session');
    die('Could not open session');  // not useful in this case
}
// Create a new instance of the Tropo object.
$tropo = new Tropo();
$initial_text = $session->getInitialText();

At this point, $initial_text contains the message sent from the user.  I send that off to another routine that parses it, does a request to Bustracker and gets a response.  After that, I format the Bustracker response into one or more SMS messages (160 max characters), and have Tropo reply to the caller using the say() function.

$result = parse_message($initial_text); 
$channel = $session->getFromChannel();   
if ($channel == 'VOICE') {
    tropo->say('Welcome to  jata S M S!  Text W R T A STOP ID to get predictions');
} else {
    $count = 0;
    if(isset($result) && (count($result > 0))) {
        foreach ($result as $s) {
            if($count >= 3) {	// just send max 3
                break;
            }
            $tropo->say($s);
            $count++;
        }
    } else {
        $tropo->say('No results');
    } 
}
JataSmsMessages

The initial title of the message shows ETA without opening message.

I initially took the entire Bustracker response, which can contain multiple predictions, and concatenated them into a single say() response.  Using a busy stop with lots of buses coming and going, that resulted in some undesirable results.  First, the concatenated responses got broken up into multiple SMS messages, and the breaks weren’t logical, so a single prediction could be split across 2 messages.  Second, the shear volume of messages could be too much, 15 or more text messages for a single request. I reworked the parse_message() routine in tropo-bustracker-api.php to handle this.  First, I re-arranged the bustracker response fields to be in the order of most important first.   The first thing a user wants to see id how many minutes until the bus arriveal, so I put that first.  Next is the Route ID, and the bus ID, in case there are multiple buses coming, and you’re only interested in a certain one.  Next, the destination for that route, so the user can verify it’s going in the right direction.   Finally, the name of the stop that the message is for, so the user can verify that (s)he entered the correct Stop ID. JataSmsOnAlcatel After all that is concatenated, I use strlen() to check that the message is not greater than 160 chartacters, and if so, I trim the length using substr(). The second change was to have parse_message return an array of prediction strings rather than a single string.  This allows the main program to use a separate say() call for each prediction, resulting in a single SMS for each prediction.  And a third change was to limit the number of results to 3, an arbitrary number. Another change I made was to create a new syntax for the test message.  The local WRTA Bustracker instructs users to “Text WRTA [Stop ID] to 41411”.  Stop ID is a number assigned to the stop.  It is sometimes available on a sign at the stop (along with a QR-Code, which jata can read), else you’ll need to identify it on the Bustracker web page or map.  Jata-sms uses the same syntax, with the addition of an optional parameter.  To avoid getting flooded with messages in the case of a busy stop, the user can add one or more bus route IDs that (s)he is interested in.  For instance, in testing, the following message:

wrta 3130

texted to 1-857-239-0165 resulted in 16 responses, until I put in the arbitrary limit of 3.  Adding the optional route parameter(s) cuts it down, for example

wrta 3130 18 30 31

will return predictions only for routes 18, 30, and 31.

More detail about the Bustracker API

You are probably wondering why both jata-sms and WRTA bustracker need the “wrta” part of the text message.  With the WRTA Bustracker, the answer us that Textmarks handles many texting services and the first noun of the message seems to indicate the client’s  name.  Jata-sms uses it to identify which of the 4 transit agencies that jata has access to, which are CTA, CCCTA, PSTA, and WRTA.  The file tropo-bustracker-api.php has an array containing the agency names, access URLs, and API keys, so if the first word of the messages matches an agency name, it handles it as a prediction request.

class AgencyInfo 
{
    public $name = '';
    public $url = '';
    public $key = '';

    function __construct($name, $url, $key) {
        $this->name = $name;
        $this->url = $url;
        $this->key = $key;
    }
}

If this project got larger than the 4 agencies, the array would be moved into a MySQL database. The actual Bus Tracker system in use by these agencies is a product of Clever Devices and their description of BusTime is here.    Parsing it with PHP is easier than the XMLPullParser I used in jata.    In PHP, I simply use simplexml_load_string(), which parses the XML and puts the elements in an array as shown here:

if(($theAgency != '' ) && ($theStop != '')) {
    $myUrl = $myAgencyObject->url.'/';
    $myUrl .= 'getpredictions?key='.$myAgencyObject->key.'&stpid='.$theStop;
    if($numRoutes > 0) {
        $myUrl .= '&rt='.$routes;
    }
    $result = file_get_contents($myUrl);	

    $xml = simplexml_load_string($result);
    if(($errorObject = libxml_get_last_error()) != FALSE) {
        append_log('xml error: '.$errorObject->message);
    }

    if($xml != FALSE) {
        if(!empty($xml->error)) {
            $reply[] = 'Stop '.$xml->error->stpid.' '.$xml->error->msg;
        } elseif (empty($xml->prd)) {
            $reply[] = 'No predictions';
        } else {
            foreach ($xml->prd as $pred) {
                $minutes = calc_minutes($pred->tmstmp, $pred->prdtm);
                $eta = $minutes.' MIN';
                if($minutes <= 2) {
                    $eta = 'Due!';
                }
                $s = $eta.' RT '.$pred->rt.' To '.$pred->des.' '.$pred->rtdir.' at '.$pred->stpid.' '.$pred->stpnm;
                if (strlen($s) > 160) {
                    append_log('Trimmed: '.$s);
                    $s = substr($s, 0, 160);
                    append_log('To: '.$s);
                }
                $reply[] = $s; 
            } 
        }
    }
}
return $reply;

 

Since the first word of the message identifies the intent if the message (remind you of the Android API much?), it would be easy to add more services to the same tropo number.  I just may extend it to deliver a Zippyism, using a Zippy intent.   The Zippyism page was an early attempt at delivering active content, I would re-craft it to use PHP at this point.

Try it

I can’t predict how long this will exist, it’s up to Tropo, and the existence of the Stillwater Engineering web site[3], but go ahead and Text:

  • wrta 3130
    To 857-239-0165 for WRTA Central Hub buses

Wrapping up

I hope that helps you with your Tropo programming.  If not, and you need a voice or SMS project set up for your social or marketing needs, contact me, and I can take care of it.

  1. [1]There could be a whole other discussion about giving away the razors (phones) to sell the blades (20 ¢ / min airtime), but I’ll leave that for another day.
  2. [2]But, the TracFone does have web browsing, and after starting jata-sms, I learned how to use that effectively on the Tracfone .
  3. [3]There is a donate button on the jata blog page.  ;-)