Professional Documents
Culture Documents
development tutorial
writing SEMS applications
[ Version 0.1 / 12.10.2006 ]
iptego GmbH
Am Borsigturm 40
D-13507 Berlin
Germany
0700 - IPTEGODE
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page1/22
Table of Contents
1 Introduction.......................................................................................................................................................3
2 Writing a C++ application plugin........................................................................................................................4
2.1 Overview...................................................................................................................................................4
2.2 MyApp the empty template.....................................................................................................................4
2.3 MyConfigurableApp adding configurability..............................................................................................7
2.4 MyAnnounceApp an announcement.......................................................................................................8
2.5 MyJukebox - reacting to user input .........................................................................................................9
2.6 Calling Card application in B2BUA mode................................................................................................11
3 Writing an IVR application...............................................................................................................................18
3.1 Ivr Overview............................................................................................................................................18
3.2 Simple announcement script...................................................................................................................18
3.3 RFC 4240 announcement service...........................................................................................................19
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page2/22
1 Introduction
This is the application development tutorial for SEMS-ng. SEMS-ng is a high performance, carrier grade,
extensible media server for SIP (RFC3261) based VoIP services.
SEMS-ng can be extended through loadable modules, so called plug-ins. Application modules that implement
applications logic can use the core SEMS-ng API on various levels of abstraction, therefore flexibility in the
development of services is provided. Other modules extend SEMS' functionality, for example with additional
codecs, file formats, or signaling logic.
While a part of this guide also applies to older versions of SEMS (SEMS v0.8.12, SEMS v0.9.0), this
documentation is for SEMS-ng (SEMS v0.10.0). For further information about SEMS version 0.8.12 and 0.9.0
please refer to the documentation provided with the respective source packages. Nevertheless, for simplicity
from now on SEMS-ng is referred to in this document as SEMS. For further information about SEMS versioning
please refer to the SEMS-versions document.
This tutorial has two parts: The first one describes how to write application modules in C++ using the SEMS
framework API, the second part explains how to write an application in the IVR as python script. While the API
and the use of the framework is different or sometimes has differing names, one basically does the same things
in both C++ plugins: On session start one sets input and output, and then one reacts to various events (BYE,
DTMF, ...). So even if the reader only wants to use the IVR, the first part gives some probably useful insight.
The C++ tutorial starts from an empty application plugin template, and subsequently adds functionality, while the
code is explained. First, the plugin is made configurable, and then, the plugin will play an announcement. As a
next step we react on the caller's keypresses. Then a calling card application will describe how to use
component plugins and SEMS' B2BUA capabilities.
The IVR tutorial has a minimal announcement application and announcement service following rfc4240: Basic
Media Services with SIP.
The reader is encouraged to try the plugins while reading this document, modify and adapt them. For each step
there is a separate archive with all the source available.
The author welcomes comments, suggestions, and corrections, and can best be reached via email at the sems
or semsdev mailing lists (see http://lists.iptel.org) or directly at mailto:stefan.sayer@iptego.de.
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page3/22
sems-tutorial-appmyapp.tgz
An application plugin's source resides in the SEMS source tree under the /apps folder. In this folder there is a
directory for each plugin. The empty, but functional template contains the following files:
myapp/MyApp.h
Header file
myapp/MyApp.cpp
myapp/Makefile
The Makefile
myapp/Readme
Documentation
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page4/22
int onLoad();
AmSession* onInvite(const AmSipRequest& req);
};
This is the session factory class for our application. It will be instantiated on startup, and at this point onLoad() is
called. Whenever an INVITE comes for this application, it is requested to react on this, and return an
AmSession object. This object is the one that will handle the call itself. As parameter we get the SIP request, so
we can act accordingly.
Lets have a look at the class we derive from AmSession that will handle our call:
class MyAppDialog : public AmSession
{
public:
MyAppDialog();
~MyAppDialog();
EXPORT_SESSION_FACTORY(MyAppFactory,MOD_NAME);
This is a macro so that SEMS finds the exports while loading the module
MyAppFactory::MyAppFactory(const string& _app_name)
: AmSessionFactory(_app_name)
{
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page5/22
int MyAppFactory::onLoad()
{
return 0;
}
MyAppDialog::~MyAppDialog()
{
}
chdir to sems/
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page6/22
restart sems
sems-tutorial-appmyconfigurableapp.tgz
The next step is to add configurability, so we can set the file which will be played in the following chapter, which
will be a simple announcement application.
We call this application myconfigurableapp. So we have to rename the source files, the module name in the
Makefile and MOD_NAME in the source. In
SEMS has one main configuration file (sems.conf) , and each module has its own configuration file, which
usually has the name of the module plus .conf.
We use the ConfigReader from AmConfigReader.h, which will read the config file for us:
#include "AmConfigReader.h"
When the module is loaded, we configure it in the onLoad function:
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page7/22
int MyConfigurableAppFactory::onLoad()
{
AmConfigReader cfg;
if(cfg.loadFile(AmConfig::ModConfigPath + string(MOD_NAME ".conf")))
return -1;
AnnouncementFile = cfg.getParameter("announcement_file","/tmp/default.wav");
Read the parameter, the second is the default value which will be used if the there is no configuration for
announcement_file.
if(!file_exists(AnnouncementFile)){
ERROR("announcenment file for configurableApp module does not exist
('%s').\n",
AnnouncementFile.c_str());
return -1;
}
We check whether the file exists with the file_exists function form AmUtils. If we return error (-1) here, SEMS
will not startup on failure.
return 0;
}
Return OK.
Now that this application is called myconfigurableapp and not myapp any more, we must not forget to change
the line in the ser.cfg to execute the new application:
t_write_unix("/tmp/your_sems_socket","myconfigurableapp")
sems-tutorial-appmyannounceapp.tgz
While the plugin didn't do much with the caller until now, now the time has come to let it do something: it will play
the file to the caller indicated in the configuration.
To read a file, we use AmAudioFile from AmAudio, which already does all the work for us: it opens the file, looks
for the appropriate file format and codec. We add a member wav_file to the session class as
MyAnnounceAppDialog::wav_file;
When the session starts, we open the file and set it as output:
void MyAnnounceAppDialog::onSessionStart(const AmSipRequest& req)
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page8/22
{
DBG("MyAnnounceAppDialog::onSessionStart - file is '%s'\n",
MyAnnounceAppFactory::AnnouncementFile.c_str());
if(wav_file.open(MyAnnounceAppFactory::AnnouncementFile,AmAudioFile::Read))
throw string("MyAnnounceAppDialog::onSessionStart: Cannot open file\n");
If an error has occured, the return value is != 0
setOutput(&wav_file);
}
As we maybe remember from the developersguide, each AmSession has an input and an output. Audio is read
from the output and sent via RTP, and received audio is written to the input. It is sufficient for the Session to set
the output the SEMS core does the rest.
Additionally, when all the file is played, the audio engine sends an event to the session (an AmAudio event with
the type AmAudio::cleared), and the default behaviour in this case is to stop the session, which will send a bye.
We can set anything derived from AmAudio as input or output. In the core there is several useful things already
implemented:
AmAudioFile
AmPlaylist
AmConferenceChannel
AmAudioBridge
AmAudioDelay
AmAudioQueue
Holds several items, the Audio is written/read through all of them (e.g. for putting filters beore
a conference)
For a more detailed description and examples have a look at the developersguide section
insert reference
2.5
sems-tutorial-appmyjukebox.tgz
use playlist
This application shows how to react on user input via DTMF. Additionally the playlist (AmPlaylist) is introduced.
If the user presses a key while in the call, the corresponding file is added to the back of the playlist.
The MyJukeboxDialog gets an AmPlaylist as member:
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page9/22
public:
...
The playlist is initialized with the dialog object as event receiver; when the playlist is empty we will get an event
from the playlist.
MyJukeboxDialog::MyJukeboxDialog()
: playlist(this)
{
}
When the session starts, we set input and output to the playlist (well... we don't need input actually, but we'll still
set it), and enable DTMF detection:
void MyJukeboxDialog::onSessionStart(const AmSipRequest& req)
{
DBG("MyJukeboxDialog::onSessionStart - jukedir is '%s'\n",
MyJukeboxFactory::JukeboxDir.c_str());
setInOut(&playlist, &playlist);
setDtmfDetectionEnabled(true);
}
By default DTMF detection is disabled as the signal processing involved use quite some processing power.
When the caller presses a key, the onDtmf event handler is executed, where we just open the file and add it to
the playlist:
void MyJukeboxDialog::onDtmf(int event, int duration) {
DBG("MyJukeboxDialog::onDtmf, got event %d, duration %d.\n", event, duration);
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
10/22
playlist.addToPlaylist(item);
}
We also get an event when the playlist is empty. As this is an AmAudio event, we have to filter this on the
process routine before the AmAudio base class processes the events, while not forgetting to send the rest of
the events to AmAudio::process for correct handling:
void MyJukeboxDialog::process(AmEvent* ev)
{
DBG("MyJukeboxDialog::process\n");
AmSession::process(ev);
}
To try this application, we create a config file myjukebox.conf which contains for example the following:
#CFGOPTION_SEMS_MYJUKEBOX_JUKEBOXDIR
jukebox_dir=/tmp/jukebox/
#ENDCFGOPTION
and we create a directory /tmp/jukebox where we put some sound files, named 0.wav, 1.wav, 2.wav, and so on.
Even though this not that much fun like real music files, using the following line we can get the numbers as wav
files from the CVS of the 0.9 version of SEMS:
for ((f = 0; f < 10; f++)); do cvs -d
:pserver:anonymous@cvs.berlios.de:/cvsroot/sems co answer_machine/wav/$f.wav; done
; mv answer_machine/wav /tmp/jukebox; rm -rf answer_machine
use DI API
sems-tutorial-appmycc.tgz
use user_timer
ccard_wav.tar
B2BUA application
As next example we will use SEMS' B2BUA capabilities to create a calling card application, for example for a
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
11/22
PSTN gateway. When a user dials in, his PIN number is collected and verified, and if his credit is sufficient, he
is asked for a number to dial. With SEMS acting as Back-to-Back user agent with this number then a session is
established (the callee leg). The session is terminated when the user is out of credit. On termination, the users'
credit is reduced by the amount of time the session was established.
To test this application there is a set of wave files available with the messages to be played back to the caller, as
ccard_wav.tar. They were created with the flite speech synthesizer and are of course only for testing purposes.
We want to build the application in a modular manner: On module, called cc_acc provides the accounting,
which is used by the other one, mycc, which is the implementation of the call logic. This shows how to provide
and use a di-interface (di stands for dynamic invocation), and how to create component modules. The
application also uses the user_timer, a timer from the session_timer plugin, which has seconds granularity,
which should be enough for a calling card application.
Lets start with the accounting module cc_acc, it should provide only two functions:
/** returns credit for pin, -1 if pin wrong */
int getCredit(string pin);
/** returns remaining credit */
int subtractCredit(string pin, int amount);
The implementation of these functions is straightforward, we simply use a singleton CCAcc, in which we have a
map to hold the credits. As an example we add a credit for 12345 on startup.
As every class that provides a di-interface, the class must implement AmDynInvoke (from AmApi.h), which
consists of the function invoke(method, args, ret):
#include "AmApi.h"
/**
* accounting class for calling card.
* this illustrates the DI interface
* and component modules
*/
class CCAcc : public AmDynInvoke
{
/** returns credit for pin, -1 if pin wrong */
int getCredit(string pin);
/** returns remaining credit */
int subtractCredit(string pin, int amount);
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
12/22
To use the API , the caller gets the DI Interface and calls invoke with the function name as the first parameter,
the arguments as the second, and an empty array for return values as the third. The AmArgArray type is a
dynamic array that can hold entries of types int, double or c string.
In the implementation of the invoke function we check the function name and call the appropriate function:
void CCAcc::invoke(const string& method, const AmArgArray& args, AmArgArray& ret)
{
if(method == "getCredit"){
ret.push(getCredit(args.get(0).asCStr()));
}
else if(method == "subtractCredit"){
ret.push(subtractCredit(args.get(0).asCStr(),
args.get(1).asInt()));
}
else
throw AmDynInvoke::NotImplemented(method);
}
To export the interface, so that the plugin-loader knows that we export a DI-API from the plugin, we must export
a class that implements AmDynInvokeFactory, which is the factory class for instances of the DI-interface. In our
case we have the implementation of the accounting as singleton as well so we just give out the same
CCAcc::instance every time:
class CCAccFactory : public AmDynInvokeFactory
{
public:
CCAccFactory(const string& name)
: AmDynInvokeFactory(name) {}
AmDynInvoke* getInstance(){
return CCAcc::instance();
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
13/22
int onLoad(){
DBG("CCAcc calling card accounting loaded.\n");
return 0;
}
};
As every plugin factory (AmDynInvokeFactory is a AmPluginFactory), it also has a function onLoad().
To export the CCAccFactory for the plugin loader, we use
EXPORT_PLUGIN_CLASS_FACTORY(CCAccFactory,"cc_acc");
The second parameter is the interface factory name, which is used by the user of the DI-API.
Lets now see how we use the DI API, in the application mycc (apps/MyCC.cpp and apps/MyCC.h). In the
SessionFactory we get a Factory for the Interface, in
int MyCCFactory::onLoad()
...
cc_acc_fact = AmPlugIn::instance()->getFactory4Di("cc_acc");
if(!cc_acc_fact){
ERROR("could not load cc_acc accounting, please provide a module\n");
return -1;
}
...
This Factory, AmDynInvokeFactory* cc_acc_fact, is later used to get a DI interface, which we pass to the
CCDialog class (the session class):
AmSession* MyCCFactory::onInvite(const AmSipRequest& req)
{
...
AmDynInvoke* cc_acc = cc_acc_fact->getInstance();
if(!user_timer){
ERROR("could not get a cc acc reference\n");
throw AmSession::Exception(500,"could not get a cc acc reference");
}
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
14/22
available credit.
To use the DI interface we fill the args with the arguments, call the function with the appropriate name, and read
the result from the return value array, as here in MyCCDialog::onDtmf:
AmArgArray di_args,ret;
di_args.push(pin.c_str());
cc_acc->invoke("getCredit", di_args, ret);
credit = ret.get(0).asInt();
Now to the call control class, the MyCCDialog. As we want to get into B2BUA mode later, we derive the Call
control class from AmB2BCallerSession. A AmB2BCallerSession behaves like a normal AmSession, if
sip_relay_only == false. We create another leg of the call, the so-called callee leg, with the function
connectCallee, which will set sip_relay_only to true, such that all events (SIP requests and replys) coming from
the caller will be relayed to the other leg to the callee leg.
A AmB2BCallerSession also has two more callbacks: onOtherReply and onOtherBye. onOtherReply is
called when there is a reply in the callee leg; if we sense this we can start the accounting when the callee is
connected:
void MyCCDialog::onOtherReply(const AmSipReply& reply) {
DBG("OnOtherReply \n");
if (state == CC_Dialing) {
if (reply.code < 200) {
DBG("Callee is trying... code %d\n", reply.code);
} else if(reply.code < 300){
if (getCalleeStatus()
== Connected) {
state = CC_Connected;
start the accounting:
startAccounting();
set our input and output to NULL such that we don't send RTP to the caller:
setInOut(NULL, NULL);
set the call timer from the session timer, this will post us an event on timeout:
// set the call timer
AmArgArray di_args,ret;
di_args.push(TIMERID_CREDIT_TIMEOUT);
di_args.push(credit); // in seconds
di_args.push(dlg.local_tag.c_str());
user_timer->invoke("setTimer", di_args, ret);
}
We can get the call back and start again with collecting the number if the callee could not be reached:
} else {
DBG("Callee final error with code %d\n",reply.code);
addToPlaylist(MyCCFactory::DialFailed);
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
15/22
number = "";
state = CC_Collecting_Number;
}
}
// we dont call
//
AmB2BCallerSession::onOtherReply(reply);
By sensing onOtherBye we can stop the accounting when the other side hangs up:
void MyCCDialog::onOtherBye(const AmSipRequest& req) {
DBG("onOtherBye\n");
stopAccounting();
AmB2BCallerSession::onOtherBye(req); // will stop the session
}
We also stop the accounting when we receive a BYE from the caller leg:
void MyCCDialog::onBye(const AmSipRequest& req)
{
DBG("onBye: stopSession\n");
if (state == CC_Connected) {
stopAccounting();
}
terminateOtherLeg();
setStopped();
}
Our implementation of the calling card service logic uses a state machine with four states, Collecting_PIN,
Collecting_ Number, CC_Dialing and CC_Connected. Depending on this state we add the number to the PIN or
the Number in onDtmf, or we do connectCallee.
void MyCCDialog::onDtmf(int event, int duration) {
DBG("MyCCDialog::onDtmf, got event %d, duration %d.\n", event, duration);
switch (state) {
...
case CC_Collecting_Number:
if(event <10) {
number +=int2str(event);
DBG("number is now '%s'\n", number.c_str());
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
16/22
} else {
if (getCalleeStatus() == None) {
state = CC_Dialing;
connectCallee(number+MyCCFactory::ConnectSuffix,
"sip:"+number+MyCCFactory::ConnectSuffix);
addToPlaylist(MyCCFactory::Dialing);
}
}
break;
If there is a timeout of the timer we set before using the user_timer API, we get an event in our event queue.
This event is of the type AmPluginEvent, and has name==timer_timeout . So in MyCCDialog::process we filter
out this event, and tear down the call:
void MyCCDialog::process(AmEvent* ev)
{
...
AmPluginEvent* plugin_event = dynamic_cast<AmPluginEvent*>(ev);
if(plugin_event && plugin_event->name == "timer_timeout") {
int timer_id = plugin_event->data.get(0).asInt();
if (timer_id == TIMERID_CREDIT_TIMEOUT) {
DBG("timer timeout: no credit...\n");
stopAccounting();
terminateOtherLeg();
terminateLeg();
ev->processed = true;
return;
}
}
AmB2BCallerSession::process(ev);
}
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
17/22
sems-tutorial-appivr_announce.tgz
A basic ivr script which plays back a file configured as announcement configuration parameter is a twelve
liner:
from log import *
from ivr import *
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
18/22
class IvrDialog(IvrDialogBase):
announcement = None
def onEmptyQueue(self):
self.bye()
self.stopSession()
onEmptyQueue is called every time that the playlist is empty, and then we have to end the call ourselves.
sems-tutorial-appannc_service.tgz
use user_timers
This script implements announcement service following rfc4240. The file to be played back is encoded in the
request uri of the INVITE as parameter play (e.g. INVITE
sip:annc@mediaserver.net:5060;play=http://mediaserver.net/example.wav). It can be a remote (http uri) or a
local file. The script uses urlparse and urllib libraries to parse the parameter and get the file.
Additional optional parameters are the maximum play time, the repeat count and delay between two repeats.
The script uses two timers from user_timer (session_timer plugin) to control this. Every timer has an ID, which
must be >0. This timer id is used when setting the timer with the setTimer method, and on timeout the onTimer
method is called with the corresponding timer ID. The timers have seconds granularity so the second parameter
to the setTimer function is the timeout in seconds.
We react to onEmptyQueue (empty playlist) and onTimer events, by hanging up, setting the timer or rewinding
and enqueuing the file once more.
# Simple implementation of (a part) of RFC4240
# announcement service.
#
# supported parameters:
#
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
19/22
TIMEOUT_TIMER_ID = 1
DELAY_TIMER_ID
= 2
class IvrDialog(IvrDialogBase):
announcement=None
filename = ""
repeat="1"
delay=0
duration=-1
play=""
delete_onbye = False
repeat_left = 0
def onSessionStart(self,hdrs):
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
20/22
resource = urlparse(self.play)
if (resource[0] == "http"):
self.delete_onbye = True
self.filename = urlretrieve(self.play)[0]
self.enqueue(self.announcement, None)
def onBye(self):
self.stopSession()
self.cleanup()
def onEmptyQueue(self):
if (self.repeat_left>0):
if (int(self.delay) > 0):
self.setTimer(DELAY_TIMER_ID, int(self.delay)/1000)
else:
self.repeat_left-=1
self.announcement.rewind()
self.enqueue(self.announcement, None)
else:
self.bye()
self.stopSession()
self.cleanup()
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
21/22
def onDtmf(self,key,duration):
pass
def cleanup(self):
if (self.delete_onbye):
unlink(self.filename)
debug("cleanup..." + self.filename + " deleted.")
self.removeTimers()
References
ThisdocumentiscopyrightofiptegoGmbH.
Distributionisonlyallowedwithwritten
permissionofiptegoGmbH.
http://www.iptego.de
Filename: semsngapp_module_tutorial.odt
Author: StefanSayer
Version: 0.1
Page
22/22