Open SMPP Erlang Library

Copyright © 2003 - 2004 Enrique Marcote Peña

Version: 1.1

Authors: Enrique Marcote Peña (mpquique_at_users.sourceforge.net) [web site: http://oserl.sourceforge.net/].

OSERL is an Erlang implementation of the Short Message Peer to Peer Protocol version 5.0.

As a guideline, some comments include references to the specific section numbers on [SMPP 5.0].

Contents

  1. Introduction
  2. Install OSERL
  3. SMPP protocol implementation overview
  4. Developing SMS based applications with OSERL
    1. How to implement an ESME
      1. ESME skeleton
      2. Submit SM sample (java vs. erlang)
      3. Sample echo ESME
    2. How to implement a SMSC
      1. SMSC skeleton
      2. Test SMSC
    3. Fine tune OSERL's SMPP implementation
      1. smpp_pdu.hrl
      2. smpp_param.hrl
      3. smpp_base.hrl
  5. PDU data storage
  6. Translating the SMPP specification
  7. Base Syntax
  8. Base Syntax Macros
  9. Param Syntax
  10. Param Syntax Macros
  11. PDU Syntax
  12. PDU Syntax Macros
  13. Behaviours
  14. PICS Proforma
  15. Changes
  16. TODO
  17. References

Introduction

Short Message Peer to Peer (SMPP) protocol [SMPP 5.0] is an open, industry standard designed to provide a flexible data communications interface for the transfer of short message data between External Short Message Entities (ESME), Routing Entities (RE) and Short Message Service Centers (SMSC).

A great amount of the effort required for the development of any of the above mention entities is generally spent on a partial implementation of the SMPP protocol. It is rare to find an implementation that covers every aspect of the SMPP protocol as OSERL does.

OSERL (open SMPP erlang library) comprises the entire specification of the recently released SMPP version 5.0 (February 20th, 2003). Moreover, every forward and backward compatibility guidelines were adopted at the design stage, what makes the resulting library easy to maintain and update to any future or previous version of the protocol.

Install OSERL

You need to have erlang installed before you attempt to use OSERL.

Erlang is a concurrent functional programming language designed for programming large industrial real-time systems. Erlang is dynamically typed and has a pattern matching syntax. Functions are defined using recursion equations. Erlang provides explicit concurrency, has asynchronous message passing and is relatively free from side effects.

Distributed Erlang programs can run transparently on cross-platform multi-vendor systems. The language has primitives for detecting run-time errors and for dynamic code replacement (i.e. changes to code can be made in a running real-time system, without stopping system).

Install common_lib package:

  tar -zxvf common_lib-1.0.tar.gz
  cd common_lib-1.0
  make

Install oserl package:

  tar -zxvf oserl-1.1.tar.gz
  cd oserl-1.1
  make

Put the ebin directory of both packages in your code path. You may add these lines to your ~/.erlang

code:add_path("<path-to-common_lib>/ebin").
code:add_path("<path-to-oserl>/ebin").

To compile an erlang program using oserl, don't forget to add a reference to the include directory:

   -I <path-to-oserl>/include -pz <path-to-oserl>/ebin

SMPP protocol implementation overview

The SMPP protocol defines a set of operations, each one taking the form of a request and response PDU (Protocol Data Unit).

The first challenge to face on the implementation was to develop packing and unpacking functions for the command PDUs. The approach was to translate the entire SMPP protocol specification to erlang terms using the syntax notation described next. Then, we only needed to create the encoding and decoding functions for the primitive types used on our notation, and the whole problem of the SMPP PDU packing/unpacking was solved.

Notice that whenever a new version of the SMPP protocol gets released, we'll just need to update our "static" translation of the protocol to get support for new parameters and PDUs. Packing and unpacking functions won't need to be modified.

Besides the PDU format, encoding (decoding) mechanisms and associated error codes, the SMPP protocol specification defines how a well behaved SMPP based application should be implemented. Even the behaviour of every ESME (RE or MC) is predefined by the protocol specification in many senses, nowadays every developer must program these dynamic aspects of the protocol on its own. OSERL provides a generic ESME implementation that transparently handles just about every feature of the SMPP protocol that leaves room for automation, and there are many of them, as we will see later on.

SMPP version 5.0 declares 31 PDUs for 19 operations. All SMPP PDUs comprise of organized set of parameters. Those parameters are defined by means of a primitive type: Integer, C-Octet String and Octet String. As well as the primitive type, every parameter has a name, a domain (set of permitted values of the primitive type) and optionally an associated error code. Furthermore, there is a kind of parameters called TLVs (Tagged Length Value), whose definition also involves a tag (identifier), a set of reserved values (of the primitive type) and sometimes, a default value.

We divided the syntax notation into three layers; base syntax, param syntax and PDU syntax layer. The first one is used to define the erlang data structures for the representation of the SMPP primitive types, domains and reserved values. The parameter syntax provides an erlang notation to complete the field definitions (we'll use field and parameter interchangeable), adding to the primitive types of the underlying layer: parameter names, error codes and default values. On top of the parameter syntax, a PDU syntax was built, this upper layer defines the structure for a PDU descriptor.

Each syntax layer has its own encoding and decoding functions. The PDU packing function relies on the parameter encoder to translate to binary format every individual field of a PDU. In the same way, the parameter syntax layer doesn't know how encode a primitive value and again, must rely on the base syntax encoder to do the work. The same process applies for the unpacking mechanism.

operation
PDU datatypes PDU pack/unpack
Param datatypes Param encoding/decoding
Base datatypes Base encoding/decoding
 
PDU syntax
Param syntax
Base syntax

On this table we find the SMPP operation module on top of the PDU syntax. This module hides the type descriptors of the SMPP specification to higher layers.

Developing SMS based applications with OSERL

This sections shows how to implement SMS based applications with the Open SMPP Erlang Library.

Even if you don't have any prior experience with it, I strongly recommend you considering erlang as the programming language for the development of your ESME/SMSC or RE.

If you are reluctant to experience with a new (better ;-) development environment, you may want to consider using OSERL anyway. Erlang has mechanisms to talk to other languages such us C or java.

Examples below only apply to erlang development.

How to implement an ESME

This is a long read. Hope the examples I've include here complement the rest of the documentation (I realize there was a lack of code samples on it).

I assume you have OSERL installed and you are already familiar with OTP behaviors. Should you encounter any problems installing OSERL, please let me know.

To implement an ESME I recommend using the gen_esme behavior. This is a custom behavior (provided within OSERL), which automatically handles many of the cumbersome details associated to an ESME implementation. If you use this behavior you won't need to care about SMPP timers, PDU sequencing, link enquires, PDU encoding/decoding, error checking, error responses and so forth. The gen_esme behavior takes care of these things for you.

Let us see how the whole thing works by example. First, we'll compare erlang and java implementations by the submit_esme.erl example. Next we'll find out how to implement a simple echo ESME.

ESME skeleton

OSERL ships an ESME skeleton which might be a good starting point for your ESME development.

Find the esme_skel module under doc/examples/esme_skel.erl.

Submit SM sample (java vs. erlang)

If you are a SMPP developer, you will be familiar with java or C SMPP APIs, and wondering how does an equivalent erlang code to a given java example, looks like. Such a comparison is shown in doc/examples/submit_esme.erl, where a submit_sm java code is directly translated into erlang, using OSERL.

The original java sample had too many I/O instructions, since these kind of instructions are pretty much the same in any language, you'll find both implementations very similar. I believe this submit_sm example doesn't show the real strength of a SMPP erlang implementation. To find out the advantages of OSERL, I encourage you to implement the echo ESME example in a language such us java, and compare the result with the erlang implementations given below (I think you'll find erlang implementation much more readable and compact).

java submit_sm erlang submit_sm
public String  submit(String mobileNumber, String userMessage) {
    SubmitSM request = new SubmitSM();
    SubmitSMResp response;
  
    // input values
    serviceType = getParam("Service type", serviceType);
    sourceAddress = getAddress("Source",sourceAddress);
    destAddress = getAddress("Destination",new Address(mobileNumber));
    replaceIfPresentFlag = getParam("Replace if present flag", 
                                     replaceIfPresentFlag);
    shortMessage = userMessage;
    scheduleDeliveryTime = getParam("Schedule delivery time", 
                                    scheduleDeliveryTime);
    validityPeriod = getParam("Validity period", validityPeriod);
    esmClass = getParam("Esm class", esmClass);
    protocolId = 0;
    priorityFlag = getParam("Priority flag", priorityFlag);
    registeredDelivery = getParam("Registered delivery", 
                                   registeredDelivery);
    dataCoding = 0x10; // For Flash Message
    smDefaultMsgId = getParam("Sm default msg id", smDefaultMsgId);

    // set values
    request.setServiceType(serviceType);
    request.setSourceAddr(sourceAddress);
    request.setDestAddr(destAddress);
    request.setReplaceIfPresentFlag(replaceIfPresentFlag);
    request.setShortMessage(shortMessage);
    request.setScheduleDeliveryTime(scheduleDeliveryTime);
    request.setValidityPeriod(validityPeriod);
    request.setEsmClass(esmClass);
    request.setProtocolId(protocolId);
    request.setPriorityFlag(priorityFlag);
    request.setRegisteredDelivery(registeredDelivery);
    request.setDataCoding(dataCoding);
    request.setSmDefaultMsgId(smDefaultMsgId);

    // send the request
    int count = 1;
    System.out.println();
    count = getParam("How many times to submit this message (load test)",
                     count);
    for (int i = 0; i<count; i++) {
        request.assignSequenceNumber(true);
        //System.out.print("#"+i+"  ");
        System.out.println("Submit request " + request.debugString());
        if (asynchronous) {
            //session.submit(request);  
            response = session.submit(request);
            messageId = response.getMessageId();
            //System.out.println();
            return messageId;
        } else {
            response = session.submit(request);                    
            messageId = response.getMessageId();
            System.out.println("Submit response " + 
                               response.debugString() + 
                               " MESSAGE ID : " + 
                               messageId);
            return messageId;
        }
    }   
    return messageId;            
}
submit_sm(MobileNumber, UserMessage) ->



    % input values
    ServiceType = read_string("Service type> "),
    SourceAddress = read_string("Source> "),

    ReplaceIfPresentFlag = read_decimal("Replace if present flag> "),


    ScheduleDeliveryTime = read_string("Schedule delivery time> "),

    ValidityPeriod = read_string("Validity period> "),
    EsmClass = read_decimal("Esm class> "),
    ProtocolId = 0,
    PriorityFlag = read_decimal("Priority flag> "),
    RegisteredDelivery = read_decimal("Registered delivery> "),

    DataCoding = 2#10,  % For Flash Message
    SmDefaultMsgId = read_decimal("Sm default msg id> "),

    % set values (create the ParamList)
    ParamList = [{service_type, ServiceType},
                 {source_addr, SourceAddress},
                 {destination_addr, MobileNumber},
                 {replace_if_present_flag, ReplaceIfPresentFlag},
                 {short_message, UserMessage},
                 {schedule_delivery_time, ScheduleDeliveryTime},
                 {validity_period, ValidityPeriod},
                 {esm_class, EsmClass},
                 {protocol_id, ProtocolId},
                 {priority_flag, PriorityFlag},
                 {registered_delivery, RegisteredDelivery},
                 {data_coding, DataCoding},
                 {sm_default_msg_id, SmDefaultMsgId}],

    % send the request
    case read_decimal("How many times to submit this message (load test)") of
        Count when integer(Count) -> submit_sm_async_iter(ParamList, Count);
        _Error                    -> submit_sm_async_iter(ParamList, 1)
    end.


submit_sm_async_iter(ParamList, 0) -> 
    ok;
submit_sm_async_iter(ParamList, Count) ->
    spawn(fun() -> submit_sm(ParamList) end),
    submit_sm_async_iter(ParamList, Count - 1).


submit_sm(ParamList) -> 
    case gen_esme:submit_sm(?ESME_NAME, ParamList) of
        {ok, Response} -> 
            % See how to get a parameter value
            MessageId = operation:get_param(message_id, Response),
            io:format("Message ID: ~p~n", [MessageId]);
        {error, Error} ->
            io:format("Submit operation failed with ~p~n", [Error])
    end.

submit_esme is not just an erlang submit_sm function, but a complete ESME implementation. This ESME binds as a transmitter, behaves asynchronously, controls congestion and connection failures, and transparently handles SMPP timers and other aspects defined in the SMPP protocol.

Get into doc/examples directory and compile the submit_esme example by

    $ erlc -W -I ../../include -pz ../../ebin submit_esme.erl

Now, open an erlang shell and test it:

    $ erl
    Erlang (BEAM) emulator version 5.2 [source] [hipe]

    Eshell V5.2  (abort with ^G)
    1> executing user profile in HOME/.erlang
    .erlang rc finished

Assuming you've untared the common_lib and oserl in the same subdirectory.

    1> code:add_path("../../../common_lib/ebin").
    true
    2> code:add_path("../../ebin").
    true

and finally do

    3> submit_esme:start().
    {ok,<0.21.0>}
    4>  submit_esme:submit_sm("123456789", "hello there").
    "Service type> "CMT
    "Source> "1959
    "Replace if present flag> "0
    "Schedule delivery time> "040207105523000+
    "Validity period> "040208105523000+
    "Esm class> "0
    "Priority flag> "0
    "Registered delivery> "0
    "Sm default msg id> "0
    "How many times to submit this message (load test)"5
    ok
    Message ID: "34800629"
    Message ID: "24512469"
    Message ID: "49646623"
    Message ID: "34236909"
    Message ID: "64973652"
    4> submit_esme:stop().
    ok

To test your applications you may use the SMPPSim SMSC simulator by Martin Woolley. You can download SMPPSim from http://www.mobilelandscape.co.uk.

Since version 1.1 OSERL includes a minimal test SMSC. You can use this SMSC for your tests also, please read below for more details on this test SMSC.

Sample echo ESME

echo_esme offers a very simple (and useless) service to subscribers. Whenever a client sends a Short Message to this service, the ESME responds sending back the exact same Short Message.

You may find this example under doc/examples/echo_esme.erl.

Get into doc/examples directory compile and test this new example:

    $ erlc -W -I ../../include -pz ../../ebin echo_esme.erl

    $ erl
    Erlang (BEAM) emulator version 5.2 [source] [hipe]

    Eshell V5.2  (abort with ^G)
    1> executing user profile in HOME/.erlang
    .erlang rc finished
    1> code:add_path("../../../common_lib/ebin").
    true
    2> code:add_path("../../ebin").
    true
    3> echo_esme:start().
    Bound as transmitter to "SMPPSim"
    {ok,<0.41.0>}
    Bound as receiver to "SMPPSim"

In the test SMSC section below, this echo example is tested using the test_smsc.erl shipped with OSERL.

You may rather want to test this example using the SMSC simulator by Martin Woolley, if so, you can use this HTML form to try sending short messages to your test ESMEs.

Notice the address of the echo service is 1948 by default. If you decide to start the ESME in another address, you will need to change the destination address. Don't forget to point the SMSC IP address, in the HTML form, to where your SMPPSim SMSC simulator resides.

Type the message "hello" in the HTML form and click the button <send>. You won't get the echo response back in your browser phone, or whatever it looks like :-). SMPPSim SMSC simulator doesn't support that, this tool just listens on a HTTP port, but cannot send SM responses back. Anyway, you should see the following message in your browser:

    DELIVER_SM invoked OK

This means the message was successfully forwarded to your ESME. We can confirm that looking on the erlang shell, where the following message should be displayed:

    Echoing SM: {short_message,"hello"}

Let's give it another try (send another message with text "bye" and stop the ESME).

    Echoing SM: {short_message,"bye"}
    4> echo_esme:stop().
    Unbound as receiver.
    Unbound as transmitter.
    ok

During this session, the log of the SMSC simulator shows something like:

2004.02.07 11:25:43 668 INFO    CH8: BIND_RECEIVER:
2004.02.07 11:25:43 668 INFO    Hex dump (39) bytes:
2004.02.07 11:25:43 668 INFO    00000027:00000001:00000000:00000001:
2004.02.07 11:25:43 668 INFO    62616E61:6E610073:65637265:74000050:
2004.02.07 11:25:43 669 INFO    01013139:343800
2004.02.07 11:25:43 669 INFO
2004.02.07 11:25:43 670 INFO    SMPPReceiver: setting address range to 1948
2004.02.07 11:25:43 670 INFO    CH8: BIND_RECEIVER_RESP:
2004.02.07 11:25:43 671 INFO    Hex dump (24) bytes:
2004.02.07 11:25:43 671 INFO    00000018:80000001:00000000:00000001:
2004.02.07 11:25:43 671 INFO    534D5050:53696D00:
2004.02.07 11:25:43 671 INFO
2004.02.07 11:25:43 671 INFO    1 receivers connected and bound
2004.02.07 11:25:43 674 INFO    ConnectionHandler CH9 accepted a connection
2004.02.07 11:25:43 677 INFO    CH9: BIND_TRANSMITTER:
2004.02.07 11:25:43 677 INFO    Hex dump (39) bytes:
2004.02.07 11:25:43 677 INFO    00000027:00000002:00000000:00000001:
2004.02.07 11:25:43 677 INFO    62616E61:6E610073:65637265:74000050:
2004.02.07 11:25:43 677 INFO    01013139:343800
2004.02.07 11:25:43 677 INFO
2004.02.07 11:25:43 678 INFO    CH9: BIND_TRANSMITTER_RESP:
2004.02.07 11:25:43 678 INFO    Hex dump (24) bytes:
2004.02.07 11:25:43 678 INFO    00000018:80000002:00000000:00000001:
2004.02.07 11:25:43 678 INFO    534D5050:53696D00:
2004.02.07 11:25:43 678 INFO
2004.02.07 11:25:49 022 INFO    HC0 accepted connection
2004.02.07 11:25:49 023 INFO    HC0: DELIVER_SM
2004.02.07 11:25:49 024 INFO    DELIVER_SM:
2004.02.07 11:25:49 024 INFO    Hex dump (51) bytes:
2004.02.07 11:25:49 024 INFO    00000033:00000005:00000000:00000006:
2004.02.07 11:25:49 024 INFO    00000031:32333435:36373839:00000031:
2004.02.07 11:25:49 024 INFO    39343800:00000000:00000000:00056865:
2004.02.07 11:25:49 024 INFO    6C6C6F
2004.02.07 11:25:49 025 INFO
2004.02.07 11:25:49 025 INFO    HTTPController HC0 waiting for connection
2004.02.07 11:25:49 030 INFO    CH8: DELIVER_SM_RESP:
2004.02.07 11:25:49 030 INFO    Hex dump (27) bytes:
2004.02.07 11:25:49 030 INFO    0000001B:80000005:00000000:00000006:
2004.02.07 11:25:49 030 INFO    00001D00:01000428:000100
2004.02.07 11:25:49 030 INFO
2004.02.07 11:25:49 035 INFO    CH9: SUBMIT_SM:
2004.02.07 11:25:49 035 INFO    Hex dump (117) bytes:
2004.02.07 11:25:49 035 INFO    00000075:00000004:00000000:00000002:
2004.02.07 11:25:49 035 INFO    00010131:39343800:00003132:33343536:
2004.02.07 11:25:49 035 INFO    37383900:00000000:00000003:00056865:
2004.02.07 11:25:49 035 INFO    6C6C6F00:05000100:00070001:01000600:
2004.02.07 11:25:49 035 INFO    01010008:00020000:04240000:00190001:
2004.02.07 11:25:49 035 INFO    00020F00:0101020E:00010104:21000101:
2004.02.07 11:25:49 035 INFO    000D0001:00000F00:0101000E:00010100:
2004.02.07 11:25:49 035 INFO    10000200:00
2004.02.07 11:25:49 035 INFO
2004.02.07 11:25:49 036 INFO    CH9:SUBMIT_SM_RESP:
2004.02.07 11:25:49 036 INFO    Hex dump (25) bytes:
2004.02.07 11:25:49 036 INFO    00000019:80000004:00000000:00000002:
2004.02.07 11:25:49 037 INFO    37353333:33373232:00
2004.02.07 11:25:49 037 INFO
2004.02.07 11:25:56 460 INFO    HC0 accepted connection
2004.02.07 11:25:56 461 INFO    HC0: DELIVER_SM
2004.02.07 11:25:56 463 INFO    DELIVER_SM:
2004.02.07 11:25:56 464 INFO    Hex dump (49) bytes:
2004.02.07 11:25:56 464 INFO    00000031:00000005:00000000:00000007:
2004.02.07 11:25:56 464 INFO    00000031:32333435:36373839:00000031:
2004.02.07 11:25:56 464 INFO    39343800:00000000:00000000:00036279:
2004.02.07 11:25:56 464 INFO    65
2004.02.07 11:25:56 464 INFO
2004.02.07 11:25:56 464 INFO    HTTPController HC0 waiting for connection
2004.02.07 11:25:56 468 INFO    CH8: DELIVER_SM_RESP:
2004.02.07 11:25:56 468 INFO    Hex dump (27) bytes:
2004.02.07 11:25:56 468 INFO    0000001B:80000005:00000000:00000007:
2004.02.07 11:25:56 468 INFO    00001D00:01000428:000100
2004.02.07 11:25:56 468 INFO
2004.02.07 11:25:56 469 INFO    CH9: SUBMIT_SM:
2004.02.07 11:25:56 469 INFO    Hex dump (115) bytes:
2004.02.07 11:25:56 469 INFO    00000073:00000004:00000000:00000003:
2004.02.07 11:25:56 469 INFO    00010131:39343800:00003132:33343536:
2004.02.07 11:25:56 469 INFO    37383900:00000000:00000003:00036279:
2004.02.07 11:25:56 469 INFO    65000500:01000007:00010100:06000101:
2004.02.07 11:25:56 470 INFO    00080002:00000424:00000019:00010002:
2004.02.07 11:25:56 470 INFO    0F000101:020E0001:01042100:0101000D:
2004.02.07 11:25:56 470 INFO    00010000:0F000101:000E0001:01001000:
2004.02.07 11:25:56 470 INFO    020000
2004.02.07 11:25:56 470 INFO
2004.02.07 11:25:56 471 INFO    CH9:SUBMIT_SM_RESP:
2004.02.07 11:25:56 471 INFO    Hex dump (25) bytes:
2004.02.07 11:25:56 471 INFO    00000019:80000004:00000000:00000003:
2004.02.07 11:25:56 471 INFO    36363137:36343538:00
2004.02.07 11:25:56 471 INFO

One comment here. Notice there is one minute of inactivity since last submit, while in the demonstration above, I've intentionally waited 1 minute before stoping the ESME, see how the gen_esme behavior transparently handles inactivity timers (look at the enquire links in both sessions).

2004.02.07 11:26:56 916 INFO    CH8: ENQUIRE_LINK:
2004.02.07 11:26:56 916 INFO    Hex dump (16) bytes:
2004.02.07 11:26:56 916 INFO    00000010:00000015:00000000:00000008:
2004.02.07 11:26:56 916 INFO
2004.02.07 11:26:56 917 INFO
2004.02.07 11:26:56 917 INFO    CH8: ENQUIRE_LINK_RESP:
2004.02.07 11:26:56 917 INFO    Hex dump (16) bytes:
2004.02.07 11:26:56 917 INFO    00000010:80000015:00000000:00000008:
2004.02.07 11:26:56 917 INFO
2004.02.07 11:26:56 917 INFO
2004.02.07 11:26:56 917 INFO    CH9: ENQUIRE_LINK:
2004.02.07 11:26:56 917 INFO    Hex dump (16) bytes:
2004.02.07 11:26:56 917 INFO    00000010:00000015:00000000:00000004:
2004.02.07 11:26:56 917 INFO
2004.02.07 11:26:56 917 INFO
2004.02.07 11:26:56 918 INFO    CH9: ENQUIRE_LINK_RESP:
2004.02.07 11:26:56 918 INFO    Hex dump (16) bytes:
2004.02.07 11:26:56 918 INFO    00000010:80000015:00000000:00000004:
2004.02.07 11:26:56 918 INFO
2004.02.07 11:27:07 273 INFO    CH8: UNBIND
2004.02.07 11:27:07 273 INFO    Hex dump (16) bytes:
2004.02.07 11:27:07 273 INFO    00000010:00000006:00000000:00000009:
2004.02.07 11:27:07 273 INFO
2004.02.07 11:27:07 273 INFO
2004.02.07 11:27:07 275 INFO    0 receivers connected and bound
2004.02.07 11:27:07 277 INFO    CH8: UNBIND_RESP
2004.02.07 11:27:07 277 INFO    Hex dump (16) bytes:
2004.02.07 11:27:07 277 INFO    00000010:80000006:00000000:00000009:
2004.02.07 11:27:07 277 INFO
2004.02.07 11:27:07 277 INFO
2004.02.07 11:27:07 277 INFO    CH8 closing connection
2004.02.07 11:27:07 277 INFO    ConnectionHandler CH8 waiting for connection
2004.02.07 11:27:07 280 INFO    CH9: UNBIND
2004.02.07 11:27:07 280 INFO    Hex dump (16) bytes:
2004.02.07 11:27:07 281 INFO    00000010:00000006:00000000:00000005:
2004.02.07 11:27:07 281 INFO
2004.02.07 11:27:07 281 INFO
2004.02.07 11:27:07 281 INFO    CH9: UNBIND_RESP
2004.02.07 11:27:07 281 INFO    Hex dump (16) bytes:
2004.02.07 11:27:07 281 INFO    00000010:80000006:00000000:00000005:
2004.02.07 11:27:07 281 INFO
2004.02.07 11:27:07 281 INFO
2004.02.07 11:27:07 281 INFO    CH9 closing connection
2004.02.07 11:27:07 281 INFO    ConnectionHandler CH9 waiting for connection

Now that we have our ESME stopped, and unbound from the SMSC simulator, if you try to send another message through your browser, you'll get:

    No receiver available to deliver messageHTTP/1.1 200

    DELIVER_SM invoked OK

Let's dig into the details of the echo ESME implementation.

If you look at the documentation for the gen_esme behavior, you will find all the exported functions and callbacks available on this behavior. Notice there is an exported function for every SMPP operation an ESME may issue. Among others, these functions are exported:

As we mention, there are some other functions, most of them need to be exported to implement the gen_server and gen_esme_session behaviors, upon which gen_esme was implemented. Let us ignore the rest of the exported functions at this moment.

Recall gen_esme:submit_sm/2 function and how it was used in our echo_esme example.

handle_operation({deliver_sm, _Session, Pdu}, From, S) ->
    Mesg = sm:message_user_data(Pdu),   % gets incoming short message
    Dest = sm:reply_address(Pdu),       % source address as response address
    io:format("Echoing SM: ~p~n", [Mesg]),
    spawn(fun() -> gen_esme:submit_sm(S#state.trx_session, [Mesg|Dest]) end),
    {reply, {ok, []}, S};

See how to submit a short message from our ESME. Just need to call gen_esme:submit_sm/2. This function is declared as follows:

submit_sm(Session::Session, ParamList::ParamList) -> Result

The first argument is the pid() of the session. The second one (ParamList) holds the parameter values we are going to send along in our PDU. This argument is an assoc list; the items on this list are pairs in the format {ParamName, ParamValue}.

Let me rewrite the code above without using the sm module, so you can see all the details:

handle_operation({deliver_sm, _Session, Pdu}, From, S) ->
    % First of all, lets get the source address.

    SourceAddrTon = operation:get_param(source_addr_ton, Pdu),
    SourceAddrNpi = operation:get_param(source_addr_npi, Pdu),
    SourceAddr    = operation:get_param(source_addr,     Pdu),

    % Get the message payload from the incoming PDU. 
    % Since we don't know if the SMSC sends the short message using the
    % short_message or the message_payload parameter, we have to look which
    % one is used.
    %
    %  We'd also want to respond using the same parameter the SMSC uses.

    MessageUserData = 
        case operation:get_param(short_message, Pdu) of
            ShortMessage when ShortMessage == ""; ShortMessage == undefined ->
                {message_payload, operation:get_param(message_payload, Pdu)};
            ShortMessage ->
                {short_message, ShortMessage}
        end,

    % Now we're ready to create the parameter list for our response

    ResponseAddr = [{dest_addr_ton,    SourceAddrTon},
                    {dest_addr_npi,    SourceAddrNpi},
                    {destination_addr, SourceAddr}],

    ParamList = [MessageUserData|ResponseAddr],

    io:format("Echoing SM: ~p~n", [MessageUserData]),
    spawn(fun() -> gen_esme:submit_sm(S#state.trx_session, [Mesg|Dest]) end),
    {reply, {ok, []}, S};

Since getting the message_payload and creating the response address from the source address are common operations, both have been implemented in sm.erl module by the functions sm:message_user_data/1 and sm:reply_address/1 respectively.

gen_esme:submit_sm/2 creates a submit_sm PDU using the values in the parameter list. For those parameters not defined in the parameter list, the default value is used. Default values for every parameter are defined in smpp_param.hrl. Values in ParamList override defaults.

Parameter names and types are those in the SMPP specification, should you find any problems assigning values to an specific parameter, check their exact names and types in smpp_param.hrl.

SMPP composite parameters were defined as records. For example, values for unsuccess_sme parameters must be defined by means of the unsuccess_sme record (defined in smpp_base.hrl).

%%%
% %@spec {unsuccess_sme,
%         DestAddrTon,
%         DestAddrNpi,
%         DestinationAddr,
%         ErrorStatusCode}
%    DestAddrTon     = int()
%    DestAddrNpi     = int()
%    DestinationAddr = string()
%    ErrorStatusCode = int()
%
% %@doc unsuccess_sme composite record definition.
%
% <p>The macro ?UNSUCCESS_SME_DATATYPE defines the type specifier for this
% field.</p>
%
% <dl>
%   <dt>DestAddrTon: </dt><dd>Indicates Type of Number for destination.
%     Integer, 1 octet (default is ?TON_INTERNATIONAL).
%   </dd>
%   <dt>DestAddrNpi: </dt><dd>Numbering Plan Indicator for destination.
%     Integer, 1 octet  (default is ?NPI_ISDN).
%   </dd>
%   <dt>DestinationAddr: </dt><dd>Destination address of this short message.
%     For mobile terminated messages, this is the directory number of the
%     recipient MS.  C-Octet String, Var. max 21 octets.
%   </dd>
%   <dt>ErrorStatusCode: </dt><dd>Indicates the success or failure of the
%     submit_multi request to this SME address.  Check command_status
%     macros for a complete list of SMPP Error codes.  Integer, 4 octets.
%   </dd>
% </dl>
% %@end
%%
-record(unsuccess_sme,
        {dest_addr_ton     = ?TON_INTERNATIONAL,
         dest_addr_npi     = ?NPI_ISDN,
         destination_addr,
         error_status_code}).

The rest of SMPP operation functions have a ParamList argument also, and work much like gen_esme:submit_sm/2 does.

Look at the callbacks exported by the gen_esme behavior, to see how much you can customize your particular ESME implementation. Leaving a callback undefined crashes the entire ESME server whenever that function is called.

How to implement a SMSC

Since OSERL version 1.1 a gen_smsc behavior has been included to simplify SMSC implementations.

SMSC skeleton

OSERL ships a SMSC skeleton which might be a good starting point for your SMSC development.

Find the smsc_skel module under doc/examples/smsc_skel.erl.

Test SMSC

There's a simple test SMSC example under doc/examples/test_smsc.erl.

Let's compile this SMSC as usual and test our sample ESME.

    $ erlc -W -I ../../include -pz ../../ebin test_smsc.erl

    $ erl
    Erlang (BEAM) emulator version 5.2 [source] [hipe]

    Eshell V5.2  (abort with ^G)
    1> executing user profile in HOME/.erlang
    .erlang rc finished
    1> code:add_path("../../../common_lib/ebin").
    true
    2> code:add_path("../../ebin").
    true
    3> test_smsc:start_link().
    true

Open another erlang shell and start the echo_esme .

    $ erl
    Erlang (BEAM) emulator version 5.2 [source] [hipe]

    Eshell V5.2  (abort with ^G)
    1> executing user profile in HOME/.erlang
    .erlang rc finished
    1> code:add_path("../../../common_lib/ebin").
    true
    2> code:add_path("../../ebin").
    true
    3> echo_esme:start_link().
    {ok,<0.39.0>}

Good, now go back to the SMSC shell and deliver a SM to see what happens.

    bound_trx: <0.42.0>
    4> test_smsc:deliver_sm("123456789", "hello there").
    ok
    submit_sm: "123456789" - "hello there"

Check the output on the ESME shell:

    Echoing SM: {short_message,"hello there"}

Go ahead and start the submit_esme, you may start it within the same shell than the echo_esme.

    4> submit_esme:start_link().
    {ok,<0.48.0>}
    5> submit_esme:submit_sm("987654321", "hola").
    "Service type> "CMT
    "Source> "1950
    "Replace if present flag> "0
    "Schedule delivery time> "040207105523000+
    "Validity period> "040207105523000+
    "Esm class> "0
    "Priority flag> "0
    "Registered delivery> "0
    "Sm default msg id> "0
    "How many times to submit this message (load test)"5
    ok
    Message ID: "1"
    Message ID: "2"
    Message ID: "3"
    Message ID: "4"
    Message ID: "5"
    6> echo_esme:stop().
    ok
    7> halt().

During that sequence, the output on the SMSC shell should be something like the following.

    bound_tx: <0.49.0>
    submit_sm: "987654321" - "hola"
    submit_sm: "987654321" - "hola"
    submit_sm: "987654321" - "hola"
    submit_sm: "987654321" - "hola"
    submit_sm: "987654321" - "hola"
    unbind: <0.42.0>
    trx_session closed: <0.42.0>
    tx_session failure: <0.49.0> - {recv_error,{error,closed}}
    4> test_smsc:stop().
    ok

Notice the tx_session failure, recall we halted without stopping the submit_esme.

Fine tune OSERL's SMPP implementation

In order to fit OSERL to your particular needs, you may want to fine tune some parameters and/or PDU definitions of the SMPP protocol.

Redefining SMPP PDUs and parameters in OSERL is simple, since OSERL was designed with that principle in mind.

There are three header files you should consider, if you want to customize the SMPP protocol definitions:

smpp_pdu.hrl
PDUs type descriptor.
smpp_param.hrl
Standard and TLV parameters descriptor.
smpp_base.hrl
Parameters base type.
Important Note: Don't forget to recompile the module operation.erl for changes on any of these files to take effect.

smpp_pdu.hrl

This header defines the SMPP PDU Type Declarations.

Macros declaring SMPP PDU definitions and their default values. Refer to operation.erl to find out how these macros are used.

Every PDU declaration consists of a command_id and a couple of lists.

Standard body parameters declaration:
A list with the declaration of every standard parameter of the PDU body. Header parameters are *not* included on the PDU declaration. The standard parameters are packed/unpacked in the order specified on this list, thus the order must be the one declared on [SMPP 5.0].
TLV parameters declaration:
A list with the declaration of every TLV parameter of the PDU.

Programmers may want to comment out, uncomment or even add any desired TLV in order to fit the needs of a particular implementation. Notice that having declared unused TLVs doesn't do any harm, comment them out or remove'em from the TLV list on the PDU declaration only for efficiency sake.

Given the following declaration:

-define(BIND_TRANSMITTER_RESP,
        ?PDU(?COMMAND_ID_BIND_TRANSMITTER_RESP,
             [?SYSTEM_ID],
             [?CONGESTION_STATE,
              ?SC_INTERFACE_VERSION])).

A dictionary representing this PDU must have the elements:

[{command_id, ?COMMAND_ID_BIND_TRANSMITTER_RESP},
 {command_status, CommandStatus},
 {sequence_number, SequenceNumber},
 {system_id, SystemId}]

The pair {system_id, SystemId} might be ignored if command_status is not 0. Following pairs are also optional:

[{congestion_state, CongestionState}, 
 {sc_interface_version, ScInterfaceVersion}]

Notice that a command_length element is not included in the dictionary.

smpp_param.hrl

SMPP Standard and TLV Parameters descriptor. These descriptors are defined by means of the base types declared on smpp_base, and using the parameter syntax defined in param_syntax.hrl.

A parameter descriptor defines the parameter name, base type, default value and error code associated to it.

Feel free to adjust parameter descriptors to your particular needs. You may want to change the default value or the mandatory status of some TLVs.

smpp_base.hrl

Base definitions for the SMPP protocol specification. These definitions use the base syntax defined in base_syntax.hrl.

In this header file, parameter base datatypes, domains and reserved values are declared.

PDU data storage

PDU data is internally stored as a dictionary. Dictionaries are defined on erlang's stdlib. Functions operation:get_param/2 and operation:set_param/3 may be used to get/set parameters values by name.

The names of the parameters (keys of the PDU dictionary) are those on the SMPP protocol specification.

A PDU is completely defined by the content (the dictionary with the parameter values) and a type descriptor (terms described in the following section). Type descriptors declare the PDU structure, and determines the way it must be encoded/decoded.

To successfully encode/decode PDU contents into binary terms, every individual parameter value must match the type declared on the PDU descriptor. Users of this library won't need to care about type descriptors though. The operation module hides the details to higher layers and transparently handles the type descriptors.

Translating the SMPP specification

Only those developers interested in the implementation details of the library should read this section. Those who only want to use the API, and don't care much about how things were done, may safely skip this section.

The erlang terms used to represent the SMPP specification are intended to adopt the conventions used on [SMPP 5.0] as much as posible.

The best way to understand the syntax used in OSERL to represent PDU type descriptors, and how everything works, is by a complete example.

On page 74, section 4.2.3.2 of the SMPP specification [SMPP 5.0], the submit_multi_resp operation PDU is defined as follows:

submit_multi_resp Syntax
Field Name Size Octets Type
command_length 4 Integer
command_id 4 Integer
command_status 4 Integer
sequence_number 4 Integer
message_id Var. max 65 C-Octet String
no_unsuccess 1 Integer
unsuccess_sme Var. max 27 Composite
dest_addr_ton
dest_addr_npi
destination_addr
error_status_code
1
1
Var. max 21
4
Integer
Integer
C-Octet String
Integer
Message Submission Response TLVs Var TLV

unsuccess_sme is a composite field containing an SME address (dest_addr_ton, dest_addr_npi and destination_addr) and an error code (error_status_code). Additionally the field can be encoded multiple times according to the value specified in the no_unsuccess field.

Message Submission Response TLVs
TLV Name Tag Size Octets Type
additional_status_info_text 0x001D 1-256 C-Octet String
delivery_failure_reason 0x0425 1 Integer
dpf_result 0x0420 1 Integer
network_error_code 0x0423 3 Octet String

This PDU type is internaly represented by the following descriptor:

-define(SUBMIT_MULTI_RESP,
        ?PDU(?COMMAND_ID_SUBMIT_MULTI_RESP,
             [?MESSAGE_ID, 
              ?UNSUCCESS_SME],
             [?ADDITIONAL_STATUS_INFO_TEXT,
              ?CONGESTION_STATE,
              ?DELIVERY_FAILURE_REASON,
              ?DPF_RESULT,
              ?NETWORK_ERROR_CODE])).

This macro definition can be found in the header file smpp_pdu.hrl

Since the format of the header is common to every PDU type, it doesn't need to be declared.

Recalling the internal representation of the PDU, every individual field is defined in smpp_param.hrl as:

-define(MESSAGE_ID, 
        ?STANDARD(message_id, 
                  ?MESSAGE_ID_DOMAIN, 
                  ?NULL_C_OCTET_STRING,
                  ?ESME_RINVMSGID)).

-define(UNSUCCESS_SME, 
        ?STANDARD(unsuccess_sme, 
                  ?UNSUCCESS_SME_DOMAIN,
                  undefined, 
                  ?ESME_RINVDSTADR)).

-define(ADDITIONAL_STATUS_INFO_TEXT, 
        ?OPTIONAL_TLV(additional_status_info_text,
                      16#001D, 
                      ?ADDITIONAL_STATUS_INFO_TEXT_DOMAIN, 
                      ?ADDITIONAL_STATUS_INFO_TEXT_RESERVED, 
                      undefined, 
                      undefined)).

-define(CONGESTION_STATE, 
        ?OPTIONAL_TLV(congestion_state,
                      16#0428, 
                      ?CONGESTION_STATE_DOMAIN, 
                      ?CONGESTION_STATE_RESERVED, 
                      undefined, 
                      undefined)).

-define(DELIVERY_FAILURE_REASON, 
        ?OPTIONAL_TLV(delivery_failure_reason,
                      16#0425, 
                      ?DELIVERY_FAILURE_REASON_DOMAIN, 
                      ?DELIVERY_FAILURE_REASON_RESERVED, 
                      undefined, 
                      undefined)).

-define(DPF_RESULT, 
        ?OPTIONAL_TLV(dpf_result,
                      16#0420, 
                      ?DPF_RESULT_DOMAIN, 
                      ?DPF_RESULT_RESERVED, 
                      ?DPF_RESULT_NOT_SET, 
                      undefined)).

-define(NETWORK_ERROR_CODE, 
        ?OPTIONAL_TLV(network_error_code,
                      16#0423, 
                      ?NETWORK_ERROR_CODE_DOMAIN, 
                      ?NETWORK_ERROR_CODE_RESERVED, 
                      undefined, 
                      undefined)).

These parameter descriptors were defined by means of some macros declared in the parameter syntax.

Domains and reserved values are defined in smpp_base.hrl.

%%%
% message_id
%%
-define(MESSAGE_ID_DOMAIN, ?VAR_C_OCTET_STRING(65)).

%%%
% unsuccess_sme
%%
-define(UNSUCCESS_SME_ITEM_DOMAIN,
        ?COMPOSITE(unsuccess_sme,
                   {?TON_DOMAIN,
                    ?NPI_DOMAIN,
                    ?ADDR_21_DOMAIN,
                    ?SMPP_ERROR_DOMAIN})).

-define(UNSUCCESS_SME_DOMAIN, ?LIST(?UNSUCCESS_SME_ITEM_DOMAIN)).

%%%
% additional_status_info_text
%%
-define(ADDITIONAL_STATUS_INFO_TEXT_DOMAIN,   ?VAR_C_OCTET_STRING(256)).
-define(ADDITIONAL_STATUS_INFO_TEXT_RESERVED, ?EMPTY).

%%%
% congestion_state
%%
-define(CONGESTION_STATE_DOMAIN,   ?BOUND_INTEGER(1, 99)).
-define(CONGESTION_STATE_RESERVED, ?RANGE_INTEGER(1, 100, 255)).

%%%
% delivery_failure_reason
%%
-define(DELIVERY_FAILURE_REASON_DOMAIN,   ?BOUND_INTEGER(1, 3)).
-define(DELIVERY_FAILURE_REASON_RESERVED, ?RANGE_INTEGER(1, 4, 255)).

%%%
% dpf_result
%%
-define(DPF_RESULT_DOMAIN,   ?BOUND_INTEGER(1, 1)).
-define(DPF_RESULT_RESERVED, ?RANGE_INTEGER(1, 2, 255)).

%%%
% network_error_code
%%
-define(NETWORK_ERROR_CODE_TYPE_DOMAIN,   ?RANGE_INTEGER(1, 1, 8)).
-define(NETWORK_ERROR_CODE_TYPE_RESERVED,   
        ?UNION([?CONSTANT(0), ?RANGE_INTEGER(1, 9, 255)])).

-define(NETWORK_ERROR_CODE_ERROR_DOMAIN,   ?INTEGER(2)).
-define(NETWORK_ERROR_CODE_ERROR_RESERVED, ?INTEGER(2)).

-define(NETWORK_ERROR_CODE_DOMAIN, 
        ?COMPOSITE(network_error_code,
                   {?NETWORK_ERROR_CODE_TYPE_DOMAIN,
                    ?NETWORK_ERROR_CODE_ERROR_DOMAIN})).
-define(NETWORK_ERROR_CODE_RESERVED, 
        ?COMPOSITE(network_error_code,
                   {?NETWORK_ERROR_CODE_TYPE_RESERVED,
                    ?NETWORK_ERROR_CODE_ERROR_RESERVED})).

Notice that UNSUCCESS_SME_DOMAIN embraces the definition of both fields; unsuccess_sme and no_unsuccess, since a list is encoded in a Length ++ List fashion, no_unsuccess doesn't need to be explicitly declared.

Additionally, two records must be defined in order to successfully decode named composites:

-record(unsuccess_sme, 
        {dest_addr_ton     = ?TON_INTERNATIONAL,
         dest_addr_npi     = ?NPI_ISDN,
         destination_addr,
         error_status_code}).

-record(network_error_code, 
        {type  = ?NETWORK_ERROR_CODE_TYPE_GSM,
         error}).

Record identifiers must match the names provided in the composite declaration for the decoding function to work propertly. These names are ignored by the encode function though.

Please read documentation, comments and examples on the source code for further details.

Base Syntax

The base syntax is an internal representation for the base types defined on the SMPP specification.

Some base types are explicitly defined on the protocol specification, but many of them didn't became necessary until the PDUs were examined in greater detail. Data-types like lists or composites are needed to support PDU fields that may appear multiple times, or fields that happen to be always grouped together inside a given PDU definition.

The following base types are defined as records on the base syntax:

  1. constant
  2. integer
  3. c_octet_string
  4. octet_string
  5. list
  6. composite
  7. union

constant

-record(constant, {value})

Constant declaration. Constants are defined in binary format and their value is packed as is.

Value:
Binary constant value.

integer

-record(integer, {size, min, max})

Integer data-type declaration.

Size:
Size in octets.
Min:
Lower limit (included).
Max:
Upper limit (included).

Refer to section 3.1 on [SMPP 5.0]

c_octet_string

-record(c_octet_string, {fixed, size, format})

C-Octet String data-type declaration. A C-Octet String must always be NULL terminated, thus the minimun length allowed is 1 octet.

Fixed:
String size may be of variable or fixed length. When fixed the length must be exactly Size octets long, if not fixed it may be from 1 up to Size octets long.
Size:
Size in octets.
Format:
If a format predicate is given, the string will be valid if the C-Octet String satisfies Format.

Refer to section 3.1 on [SMPP 5.0]

octet_string

-record(octet_string, {fixed, size, format})

Octet String data-type declaration. An Octet String is not necessary to be NULL terminated, thus the minimun length allowed is 0 octets.

Fixed:
String size may be of variable or fixed length. When fixed the length must be exactly Size octets long, if not fixed it may be from 0 up to Size octets long.
Size:
Size in octets.
Format:
If a format predicate is given, the string will be valid if the C-Octet String satisfies Format.

Refer to section 3.1 on [SMPP 5.0]

list

-record(list, {type, size})

List data-type declaration. Represents a list with elements of the same Type.

Notice that nested data-type definitions are allowed. Type could be:

Type:
Defines the type of the elements on the list. Any kind of data-type declaration is valid.
Size:
Maximum number of elements.

composite

-record(composite, {name, tuple})

Composite data-type declaration. If Name is undefined the composite is said to be anonymous and thus represented by a tuple. Named composites are represented by records of type Name. It is responsibility of the programmer to provide the appropriate record definitions.

Name is ignored when encoding the value into a binary stream. It is only considered to translate the binary representation of a composite into erlang records. It also helps to document the declaration of nested data structures. It'll be a good practice to give the composite (and the associated record) the same Name that the SMPP Protocol Specification uses [SMPP 5.0].

Whether named or not, Tuple is a tuple (never a record) defining the type structure of the composite. For example, in a composite with two fields; an integer and an octet_string, Tuple would be something like:

{?INTEGER_4, VAR_OCTET_STRING(21)}

Nested data-type definitions are allowed. An element on the composite may be of the type:

Name:
Identifier of the composite. Must be left undefined if the composite is anonymous.
Tuple:
Defines the structure of the composite. Any kind of data-type declaration is a valid term of this tuple.

union

-record(union, {types})

Union data-type declaration. Defines a new type which is the union of types. For example the destination address in a submit_multi command represents either a distribution list or an SME address (these addresses have different data-types).

{union, [SmeAddress, DlAddress]}

where SmeAddress and DlAddress are composite data-type definitions. Notice that nested data-type declarations are allowed. In SMPP the union declaration is mainly used in examples like the one above mentioned, where the union types are composites.

Types:
A list with the types of the union.

Base Syntax Macros

Macros based on record data-types definitions

-define(CONSTANT(Value), 
        #constant{value = Value}).
-define(INTEGER(Size), 
        #integer{size = Size, min = 0, max = math:pow(256, Size) - 1}).
-define(C_OCTET_STRING(Fixed, Size),
        #c_octet_string{fixed = Fixed, size = Size, format = free}).
-define(OCTET_STRING(Fixed, Size),
        #octet_string{fixed = Fixed, size = Size, format = free}).
-define(LIST(Type), 
        #list{type = Type, size = 255}).
-define(COMPOSITE(Name, Tuple), 
        #composite{name = Name, tuple = Tuple}).
-define(UNION(Types), 
        #union{types = Types}).

Simplified macros, some options are implicitly assigned. Most of the SMPP PDU fields definitions are more readable using these macros.

-define(BOUND_INTEGER(Size, Max), 
        #integer{size = Size, min = 0, max = Max}).
-define(RANGE_INTEGER(Size, Min, Max), 
        #integer{size = Size, min = Min, max = Max}).

-define(HEX_C_OCTET_STRING(Fixed, Size),
        #c_octet_string{
            fixed  = Fixed, 
            size   = Size, 
            format = fun(Str) -> my_string:is_hex(Str) end}).
-define(HEX_OCTET_STRING(Fixed, Size),
        #octet_string{
            fixed  = Fixed, 
            size   = Size, 
            format = fun(Str) -> my_string:is_hex(Str) end}).

-define(DEC_C_OCTET_STRING(Fixed, Size),
        #c_octet_string{
            fixed  = Fixed, 
            size   = Size, 
            format = fun(Str) -> my_string:is_dec(Str) end}).
-define(DEC_OCTET_STRING(Fixed, Size),
        #octet_string{
            fixed  = Fixed, 
            size   = Size, 
            format = fun(Str) -> my_string:is_dec(Str) end}).

-define(ANONYMOUS_COMPOSITE(Tuple), #composite{tuple = Tuple}).

-define(VAR_C_OCTET_STRING(Size),   ?C_OCTET_STRING(false, Size)).
-define(VAR_OCTET_STRING(Size),     ?OCTET_STRING(false, Size)).
-define(FIXED_C_OCTET_STRING(Size), ?C_OCTET_STRING(true, Size)).
-define(FIXED_OCTET_STRING(Size),   ?OCTET_STRING(true, Size)).

-define(VAR_HEX_C_OCTET_STRING(Size),   ?HEX_C_OCTET_STRING(false, Size)).
-define(VAR_HEX_OCTET_STRING(Size),     ?HEX_OCTET_STRING(false, Size)).
-define(FIXED_HEX_C_OCTET_STRING(Size), ?HEX_C_OCTET_STRING(true, Size)).
-define(FIXED_HEX_OCTET_STRING(Size),   ?HEX_OCTET_STRING(true, Size)).

-define(VAR_DEC_C_OCTET_STRING(Size),   ?DEC_C_OCTET_STRING(false, Size)).
-define(VAR_DEC_OCTET_STRING(Size),     ?DEC_OCTET_STRING(false, Size)).
-define(FIXED_DEC_C_OCTET_STRING(Size), ?DEC_C_OCTET_STRING(true, Size)).
-define(FIXED_DEC_OCTET_STRING(Size),   ?DEC_OCTET_STRING(true, Size)).

-define(SIZED_LIST(Type, Size), 
        #list{type = Type, size = Size}).

Time strings

-define(ATIME_C_OCTET_STRING,
        #c_octet_string{
            fixed  = true, 
            size   = 17,
            format = fun(Str) -> my_string:is_atime(Str) end}).
-define(RTIME_C_OCTET_STRING,
        #c_octet_string{
            fixed  = true, 
            size   = 17,
            format = fun(Str) -> my_string:is_rtime(Str) end}).

Sets

-define(EMPTY,     ?UNION([])).
-define(SET(List), ?UNION(lists:map(fun(C) -> ?CONSTANT(C) end, List))).

Param Syntax

Standard and TLV parameter syntax definition. This layer complements the base syntax defined in the file base_syntax.hrl, giving to the parameter definitions the possibility to specify an associated error code, a default value and a tag (on TLVs only). A complete list of the SMPP parameters specification is defined in smpp_param.hrl, based on macros defined below and the syntax declarations included in smpp_base.hrl.

Even this syntax is considered implementation-specific, the definitions herein included try to reflect the conventions used on [SMPP 5.0].

The following types are defined as records on the param syntax:

  1. standard
  2. tlv

standard

-record(standard, {name, domain, default, error})

Standard Parameter declaration.

Name:
Name of the parameter.
Domain:
Domain of the parameter. Defined by means of the base syntax (base_syntax.hrl).
Default:
Default value for the parameter. Used by encoding functions whenever the parameter is left undefined.
Error:
Error code associated to the parameter.

tlv

-record(tlv, {name, tag, domain, reserved, mandatory, multiple, default, error})

Tagged Length Value Parameter declaration.

Name:
Name of the parameter.
Tag:
Identifier of the TLV.
Domain:
Domain of the parameter. Defined by means of the base syntax (base_syntax.hrl).
Reserved:
Set of reserved values for the parameter. Defined by means of the base syntax (base_syntax.hrl).
Mandatory:
Boolean. true if the TLV is mandatory.
Multiple:
Boolean. true if the TLV can be encoded multiple times.
Default:
Default value for the parameter. If the encoding (or decoding) operation fails the default value should be used (if provided).
Error:
Error code associated to the parameter.

Param Syntax Macros

Macros based on record param-types definitions.

-define(STANDARD(Name, Domain, Default, Error), 
        #standard{
            name    = Name, 
            domain  = Domain, 
            default = Default, 
            error   = Error}).
-define(TLV(Name, Tag, Domain, Reserved, Mandatory, Multiple, Default, Error), 
        #tlv{
            name      = Name,
            tag       = Tag, 
            domain    = Domain,
            reserved  = Reserved, 
            mandatory = Mandatory,
            multiple  = Multiple,
            default   = Default, 
            error     = Error}).

Simplified TLV Macros for Readability, some options are implicitly assigned here. SMPP PDU TLV definitions are more readable using these macros.

-define(MANDATORY_TLV(Name, Tag, Domain, Reserved, Default, Error), 
        ?TLV(Name, Tag, Domain, Reserved, true, false, Default, Error)).
-define(OPTIONAL_TLV(Name, Tag, Domain, Reserved, Default, Error), 
        ?TLV(Name, Tag, Domain, Reserved, false, false, Default, Error)).
-define(MULTIPLE_MANDATORY_TLV(Name, Tag, Domain, Reserved, Default, Error), 
        ?TLV(Name, Tag, Domain, Reserved, true, true, Default, Error)).
-define(MULTIPLE_OPTIONAL_TLV(Name, Tag, Domain, Reserved, Default, Error), 
        ?TLV(Name, Tag, Domain, Reserved, false, true, Default, Error)).

PDU Syntax

This syntax extends the param syntax and gives support to PDU defintions.

The following types are defined as records on the pdu syntax:

  1. pdu

pdu

-record(pdu, {command_id, stds_types, tlvs_types})

PDU definitions.

CommandId:
SMPP command_id.
StdsTypes:
List with the types of the standard parameters. There is one descriptor per standard parameter on the PDU. This list must have the same exact order than the one defined on the SMPP protocol specification.
TlvsTypes:
List with the types of every permitted TLV of the PDU. The SMPP protocol specification does not establish any kind of ordering for the TLV parameters. The order on this list might only be relevant from an efficiency point of view, refer to the packing/ unpacking functions implementation on pdu_syntax.erl.

PDU Syntax Macros

Macros based on PDU record definition.

-define(PDU(CommandId, StdsTypes, TlvsTypes),
        #pdu{command_id = CommandId,
             stds_types = StdsTypes, 
             tlvs_types = TlvsTypes}).

Behaviours

Generic ESME (External Short Message Entity) and SMSC (Short Message Service Center) behaviours are implemented in OSERL. Behaviours are formalizations of "design patters" which can be used to program certain common problems, an ESME or SMSC in this particular case. The generic ESME behaviour was built on top of a generic ESME session behaviour, while generic SMSC behaviour has an underlying generic SMSC session behaviour. Both (ESME and SMSC behaviours) are a sort of an extended gen_server behaviour.

behaviours

The schema depicted on the previous figure shows how the ESME/SMSC behaviours act as the callback modules for their corresponding session behaviours.

The session receives binary data from a TCP/IP connection and unpacks SMPP PDUs therein included. Some operation PDUs are automatically responded and managed by session behaviours but others, like an outbind or submit_sm PDU, must be forwarded to the upper ESME/SMSC behaviour to let it decide what to do.

SMS Developers don't need to care about sessions or connections, they will just need to implement the logic of their applications using the set of callbacks exported by the ESME/SMSC behaviours.

Leaving a callback unimplemented crashes the entire ESME/SMSC server whenever that particular function gets called.

Every callback is guaranteed to be correct, never a malformed or unexpected PDU could trigger a callback.

Due to these behaviours, OSERL handles transparently many aspects of the SMPP protocol, saving lots of work to developers by

PICS Proforma

Find OSERL's PICS proforma in openoffice format here.

Changes

See changes log here.

TODO

References

[SMPP 5.0]
Short Message Peer-to-Peer Protocol Specification. Version 5.0. SMS Forum.