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].
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.
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
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.
|
|
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.
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.
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.
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.
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.
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.
Since OSERL version 1.1 a gen_smsc behavior has been included to simplify SMSC implementations.
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.
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.
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:
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.
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.
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.
In this header file, parameter base datatypes, domains and reserved values are declared.
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.
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:
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 | ||||||||||||
|
|
|
||||||||||||
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.
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.
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:
-record(constant, {value})
Constant declaration. Constants are defined in binary format and their value is packed as is.
-record(integer, {size, min, max})
Integer data-type declaration.
Refer to section 3.1 on [SMPP 5.0]
-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.
Refer to section 3.1 on [SMPP 5.0]
-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.
Refer to section 3.1 on [SMPP 5.0]
-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:
-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:
-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.
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))). |
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:
-record(standard, {name, domain, default, error})
Standard Parameter declaration.
-record(tlv, {name, tag, domain, reserved, mandatory, multiple, default, error})
Tagged Length Value Parameter declaration.
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)). |
This syntax extends the param syntax and gives support to PDU defintions.
The following types are defined as records on the pdu syntax:
-record(pdu, {command_id, stds_types, tlvs_types})
PDU definitions.
Macros based on PDU record definition.
-define(PDU(CommandId, StdsTypes, TlvsTypes), #pdu{command_id = CommandId, stds_types = StdsTypes, tlvs_types = TlvsTypes}). |
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.
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
Upon failure the appropriate response is automatically issued. Even errors associated to individual parameters of the PDUs are detected, and reported using the corresponding error code.
Every error code in the protocol specification was implemented.
See changes log here.
Implement a gen_session to hold code common to gen_esme_session and gen_smsc_session.
Implement a gen_peer to hold code common to gen_esme and gen_smsc.