An IVR for collecting rainfall data

Aug 30, 2019 • Eugenio Pace •
Reading time: 14 mins.

I put together a small app for collecting rainfall information. It is essential to get precise rainfall information in specific locations of our family farm, and the one practical way I found is just to allow people working in the fields to report it back via their phones.

Until recently this was just a phone call to someone that took note or got the voice mail, but that meant transcribing the information, errors, etc.

Weather reports don’t have the precision and accuracy we needed. Rainfall can vary enormously in a different field only a few kilometers away from each other.

An IVR!

We thought about deploying automated weather stations, but some of the areas to cover are remote, with no power, and not excellent connectivity (and even more complicated maintenance, since I live 10,000 km away :-) ).

The simplest solution I came up with is simply an IVR (Interactive Voice Response). People working in the fields have all a cell-phone. Why not have them call a number to enter the rainfall? (which is very close to what they are used to doing anyway)

Welcome to rainfall collector. Please select the location to enter rainfall for: 1 for location A, 2 for B, 3 for C, 4 for D
...wait for input...
Please enter the millimeters of rainfall followed by the pound sign
...wait for input...
Press 1 to confirm X mm of rainfall. 2 to cancel

Architecture

The front end of the app is, of course, Twilio, which does all the voice interfacing.

The backend is a little Express app that handles requests sent by Twilio and records info in a database eventually.

Twilio publishes a nodejs SDK, including a module you can require in your app that takes care of the messages exchanged back and forth.

The nodejs SDK simplifies the creation of the messages Twilio expects: TwiML. These are just XML messages that encode instructions for their engine to do something for you: gather input from the user, say something, etc. I guess you could craft these by yourself, but the SDK makes it super easy. And besides, who wants to craft XML messages these days…

Scaffold of a simple app

The Twilio client conforms to the HTTP spec, meaning that it will honor responses like redirect, sessions, send cookies, etc.

The main app consists of a bunch of handlers for each path of the menu.

Twilio will happily follow the redirect to rainfall handler with all the query string, so we’ll need an express route to /rainfall:

server.post('/rainfall/mm?',rainfall);server.get('/rainfall',rainfall);constrainfall=(req,res)=>{// First promptif(!req.isResponse()){req.gather("Please enter millimeters of rain followed by the pound sign",3,"rainfall",null,"main");returnres.sendVoice();}//Is confirmation?if(req.isConfirm()){returnreq.confirm((done)=>{save_weather_sample(req.body.From,{mm:parseFloat(req.params.mm),location:req.body.From},done);},"main");}//Ask for confirmationreq.askConfirm(mm+" millimeters of rain","rainfall/"+mm);};

Notice that the handler is wired both to a GET and a POST. the GET will mostly happen on redirect the first time the user lands here.

Here the handler can deal with three situations:

The very first time it is called. In this case, I call req.gather (we’ll see what gather does later)

It is the final confirmation of the input (2nd if). In that case, we store the value of the captured value.

It is the actual response with the mm, then we ask for confirmation.

Extension methods

The ivr middleware, injects a bunch of methods in the request and response objects for convenience. Almost all of them end up calling the Twilio SDK.

exports.middleware=function(options){return[twilio.webhook({validate:true}),function(req,res,next){functionbuildRouteAndParams(route,params){if(!route)return"";if(params){returnroute+"?"+qs.stringify(params);}returnroute;}//The TwiML response objectreq.voice=newVoiceResponse();/*
A helper menu function
msg: the menu to say to the user. "Press 1 for foo, 2 for bar"
route: is the route to the handler of this menu. e.g. "main"
route_params: optional parmeters for the route
no_response_route: is the route to go to if there's no input. If ommited, it will be the *same* as *route*
no_response_route_paramshandler
*/req.menu=(msg,route,route_params,no_response_route,no_response_route_params)=>{constgather=req.voice.gather({numDigits:1,action:options.baseRoute+buildRouteAndParams(route,route_params),method:'POST'});gather.say(options.say,msg);if(no_response_route){no_response_route+=buildRouteAndParams(no_response_route,no_response_route_params);}else{no_response_route=route;}req.voice.redirect(options.baseRoute+no_response_route);};/*
Sends the "final" response object back to Twilio.
Notice that this is the last method that can be called.
Also notice that this is the only extension method added to "response"
*/res.sendVoice=()=>{res.send(req.voice.toString());}/*
Helper for "Say" verb. If route is specified, it will add a redirect directive.
It will not "Send" the response back to Twilio
*/req.say=(msg,route,route_params)=>{req.voice.say(options.say,msg);if(route)req.voice.redirect(options.baseRoute+buildRouteAndParams(route,route_params));};//Checks whether the information returned contains any user inputreq.isResponse=()=>{if(req.body.Digits)returntrue;returnfalse;};/*
Prompts for confirmation. 1: yes, 2: no
msg: the confirmation message
confirmation_route: where to go for confirmation. Usually the default route for the option
route_params: optional parameters to pass to the route on confirmation
*/req.askConfirm=(msg,confirmation_route,route_params)=>{if(!route_params){route_params={};}// The confirmation flagroute_params.confirm=true;varroute=options.baseRoute+buildRouteAndParams(confirmation_route,route_params);constgather=req.voice.gather({numDigits:1,action:route,method:'POST'});gather.say(options.say,"Press one to confirm "+msg+". Two to return to the menu.");req.voice.redirect(route);res.sendVoice();};/*
Validates that the request is a confirmation response.
Simply checks that there's a query parameter with "confirm=true"
*/req.isConfirm=()=>{returnreq.query.confirm;}/*
Processes the confirmation response.
action: a callback to process positive confirmation (if user presses "1")
route: where to go next, after confirmation is processed (postively or not).
route_params: option additional parameters for the route
If user cancels the request (anythign but "1"), then no action is performed and user is redirected to "route"
*/req.confirm=(action,route,route_params)=>{functionsayRedirect(msg){if(msg){req.voice.say(options.say,msg);}req.voice.redirect(options.baseRoute+buildRouteAndParams(route,route_params));res.sendVoice();}if(req.query.confirm){if(req.body.Digits==="1"){//Confirm input//Call the confirm action callbackaction((e)=>{if(!e){returnsayRedirect("The information was saved.");}console.log(e);sayRedirect("The information could not be saved. Please try again.");});}else{//CancelsayRedirect("Cancelled");}}};/*
Collects information from user
msg: the prompt for input
digits: expected length of the response
route: where to go after input collection is done
route_params: any paramters to forward on the route
no_response_route: where to go if no input is entered
no_response_route_params: any extra params for the route
*/req.gather=(msg,digits,route,route_params,no_response_route,no_response_route_params)=>{route=options.baseRoute+buildRouteAndParams(route,route_params);constgather=req.voice.gather({numDigits:digits,action:route,method:'POST'});gather.say(options.say,msg);//If no responseif(!no_response_route){no_response_route=route;}else{no_response_route=options.baseRoute+buildRouteAndParams(no_response_route,no_response_route||"");}req.voice.redirect(no_response_route);};next();}];};

The middleware makes the main app quite compact. I like that as I plan to add other functions to the menu later on for other collection activities.

Also, notice that the middleware returns an array of functions. The first one is Twilio’s method signature that ensures all requests come from your account.