SMS PDU in Python for Raspberry Pi

Cracking on with my Raspberry Pi, I've written my first program in Python.

The aim - to be able to send an SMS via a 3G USB dongle.
The problem - the way SMS needs to be encoded is hideously complicated.

For example, suppose you want to send "This is a very simple message :-)" to the phone number +447700900123.

This is the command that you need to send to your dongle:

AT+CMGS=42
079144872000626001000C9144770009103200112154747A0E4ACF416190BD2CCF83E6E9369C5D06B5CBF379F85C06E95A29

WHAT? THE? JUDDERING? FUCK?!

I found an excellent JavaScript PDU tool - which I have adapted. Also of great help was Lars Pettersson's PDU explanation and Jeron's discussion of the SMS PDU. I would have been totally lost without them.

Python is already pre-installed on the Raspberry Pi - which is handy. So here is a tool I whipped up which will generate the above gibberish.

Running the program should show this:

Which phone number do you want to send an SMS to? (e.g. +447700900123) :
What message do you want to send? : 
For FLASH SMS, type 0. For regular SMS, type 1 : 
Which SMSC will you use? (e.g. +447802002606) : 

And will output this:

AT+CMGS=60
079144872000626001000C91447700091032001035493328FFAE83D0617B19442E8FDFE432194447A7E72C50FE5D07A1C3F632E8FE7683C2A0B3BC1CA683E0F2B4BE1C02

The source is on GitHub - or please find in below for your edification and delight.

This is my first outing in Python, so I'm sure I've made a few syntactic and stylistic mistakes. Any corrections gratefully received.

  1. # This Python file uses the following encoding: utf-8
  2. """
  3. © 2012 Terence Eden
  4.  
  5. Adapted from http://rednaxela.net/pdu.php Version 1.5 r9aja
  6. Original JavaScript (c) BPS & co, 2003. Written by Swen-Peter Ekkebus, edited by Ing. Milan Chudik, fixes and functionality by Andrew Alexander.
  7. Original licence http://rednaxela.net/pdu.php "Feel free to use this code as you wish."
  8.  
  9. Python version © 2012 Terence Eden - released as MIT License
  10. ***
  11. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  12.  
  13. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  14.  
  15. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  16. ***
  17.  
  18. Note - this is my first Python program - I am quite happy to be corrected on True Pythonic Style etc. :-)
  19. """
  20.  
  21. """
  22. This program allows the user to craft a PDU when sending an SMS.
  23. The user enters the destination number, the message, the class, and the SMSC.
  24. The program generates the commands needed to instruct a modem to deliver the SMS.
  25. """
  26.  
  27. # Array with the default 7 bit alphabet
  28. # @ = 0 = 0b00000000, a = 97 = 0b1100001, etc
  29. # Alignment is purely an attempt at readability
  30. SEVEN_BIT_ALPHABET_ARRAY = (
  31.     '@', '£', '$', '¥', 'è', 'é', 'ù', 'ì', 'ò', 'Ç', '\n', 'Ø', 'ø', '\r','Å', 'å',
  32.     '\u0394', '_', '\u03a6', '\u0393', '\u039b', '\u03a9', '\u03a0','\u03a8', '\u03a3', '\u0398', '\u039e',
  33.     '€', 'Æ', 'æ', 'ß', 'É', ' ', '!', '"', '#', '¤', '%', '&', '\'', '(', ')','*', '+', ',', '-', '.', '/',
  34.     '0', '1', '2', '3', '4', '5', '6', '7','8', '9',
  35.     ':', ';', '< ', '=', '>', '?', '¡',
  36.     'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
  37.     'Ä',                                                                  'Ö', 'Ñ',                     'Ü', '§', '¿',
  38.     'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
  39.     'ä',                                                                  'ö', 'ñ',                     'ü',
  40.     'à')
  41.  
  42.  
  43. def semi_octet_to_string(input) :
  44.     """ Takes an octet and returns a string
  45.    """
  46.     out = ""
  47.     i=0
  48.     for i in range(0,len(input),2) : # from 0 - length, incrementing by 2
  49.         out = out + input[i+1:i+2] + input[i:i+1]
  50.     return out
  51.  
  52.  
  53. def convert_character_to_seven_bit(character) :
  54.     """ Takes a single character.
  55.    Looks it up in the SEVEN_BIT_ALPHABET_ARRAY.
  56.    Returns the position in the array.
  57.    """
  58.     for i in range(0,len(SEVEN_BIT_ALPHABET_ARRAY)) :
  59.         if SEVEN_BIT_ALPHABET_ARRAY[i] == character:
  60.             return i
  61.     return 36 # If the character cannot be found, return a ¤ to indicate the missing character
  62.  
  63. # Set the initial variables
  64. FIRST_OCTET = "0100" # MAGIC
  65. PROTO_ID = "00" # MORE MAGIC
  66. data_encoding = "1" # EVEN MORE MAGIC
  67. message_class = "" # Message Class. 0 for FLASH, 1 for normal
  68. SMSC_number = "" # The message centre through which the SMS is sent
  69. SMSC = "" # How the SMSC is represented once encoded
  70. SMSC_info_length = 0
  71. SMSC_length = 0
  72. SMSC_number_format = "81" # by default, assume that it's in national format - e.g. 077...
  73. destination_phone_number = "" # Where the SMS is being sent
  74. destination_phone_number_format = "81" # by default, assume that it's in national format - e.g. 077...
  75. message_text = "" # The message to be sent
  76. encoded_message_binary_string = "" # The message, as encoded into binary
  77. encoded_message_octet = "" # individual octets of the message
  78.  
  79. # Get the user inputs. No error checking in this version :-)
  80. get_destination_phone_number = raw_input("Which phone number do you want to send an SMS to? (e.g. +447700900123) : ")
  81. get_message_text = raw_input("What message do you want to send? : ")
  82. get_message_class = raw_input("For FLASH SMS, type 0. For regular SMS, type 1 : ")
  83. get_SMSC_number = raw_input("Which SMSC will you use? (e.g. +447802002606) : ")
  84.  
  85. # TODO Error check & sanitize input
  86. destination_phone_number = get_destination_phone_number
  87. message_text = get_message_text
  88. message_class = int(get_message_class)
  89. SMSC_number = get_SMSC_number
  90.  
  91. # Set data encoding
  92. data_encoding = data_encoding + str(message_class)
  93.  
  94. # Get the SMSC number format
  95. if SMSC_number[:1] == '+' : # if the SMSC starts with a + then it is an international number
  96.     SMSC_number_format = "91"; # international
  97.     SMSC_number = SMSC_number[1:len(SMSC_number)] # Strip off the +
  98.  
  99. # Odd numbers need to be padded with an "F"
  100. if len(SMSC_number)%2 != 0 :
  101.     SMSC_number = SMSC_number + "F"
  102.  
  103. # Encode the SMSC number
  104. SMSC = semi_octet_to_string(SMSC_number)
  105.  
  106. # Calculate the SMSC values
  107. SMSC_info_length = (len(SMSC_number_format + "" + SMSC))/2
  108. SMSC_length = SMSC_info_length;
  109.  
  110. # Is the number we're sending to in international format?
  111. if destination_phone_number[:1] == '+' : # if it starts with a + then it is an international number
  112.     destination_phone_number_format = "91"; # international
  113.     destination_phone_number = destination_phone_number[1:len(destination_phone_number)] # Strip off the +
  114.  
  115. # Calculate the destination values in hex (so remove 0x, make upper case, pad with zeros if needed)
  116. destination_phone_number_length = hex(len(destination_phone_number))[2:3].upper().zfill(2)
  117.  
  118. if len(destination_phone_number)%2 != 0 : # Odd numbers need to be padded
  119.     destination_phone_number = destination_phone_number + "F"
  120.  
  121. destination = semi_octet_to_string(destination_phone_number)
  122.  
  123. # Size of the message to be delivered in hex (so remove 0x, make upper case, pad with zeros if needed)
  124. message_data_size = str(hex(len(message_text)))[2:len(message_text)].upper().zfill(2)
  125.  
  126. # Go through the message text, encoding each character
  127. for i in range(0,len(message_text)) :
  128.     character = message_text[i:i+1] # get the current character
  129.     current = bin(convert_character_to_seven_bit(character)) # translate into the 7bit alphabet
  130.     character_string = str(current) # Make a string of the binary number. eg "0b1110100
  131.     character_binary_string = character_string[2:len(str(character_string))] # Strip off the 0b
  132.     character_padded_7_bit =  character_binary_string.zfill(7) # all text must contain 7 bits
  133.     # Concatenate the bits
  134.     # Note, they are added to the START of the string
  135.     encoded_message_binary_string = character_padded_7_bit + encoded_message_binary_string
  136.  
  137.  
  138. # Reverse the string to make it easier to count
  139. encoded_message_binary_string_reversed = encoded_message_binary_string[::-1]
  140.  
  141. # Get each octet into hex
  142. for i in range(0,len(encoded_message_binary_string_reversed),8) : # from 0 - length, incrementing by 8
  143.     # Get the 8 bits, reverse them back to normal, if less than 8, pad them with 0
  144.     encoded_octet = encoded_message_binary_string_reversed[i:i+8][::-1].zfill(8)
  145.     encoded_octet_hex = hex(int(encoded_octet,2)) # Convert to hex
  146.    
  147.     # Strip the 0x at the start, make uppercase, pad with a leading 0 if needed
  148.     encoded_octet_hex_string = str(encoded_octet_hex)[2:len(encoded_octet_hex)].upper().zfill(2)
  149.    
  150.     # Concatenate the octet to the message
  151.     encoded_message_octet = encoded_message_octet + encoded_octet_hex_string
  152.  
  153. # Generate the PDU
  154. PDU = str(SMSC_info_length).zfill(2) \
  155.         + str(SMSC_number_format) \
  156.         + SMSC \
  157.         + FIRST_OCTET \
  158.         + str(destination_phone_number_length) \
  159.         + destination_phone_number_format \
  160.         + destination \
  161.         + PROTO_ID \
  162.         + data_encoding \
  163.         + str(message_data_size) \
  164.         + encoded_message_octet
  165.  
  166. # Generate the AT Commands
  167. AT_CMGS = (len(PDU)/2) - SMSC_length - 1
  168. AT_COMMAND = "AT+CMGS=" + str(AT_CMGS)
  169.  
  170. # Show the commands
  171. print AT_COMMAND
  172. print PDU

The source is on GitHub.

In order to actually send the SMS, you will need to put the modem into PDU mode, this is done by the command

AT+CMGF=0

So, as per my earlier post on sending SMS, the complete sequence is

AT+CMGF=0
AT+CMGS=60
07914487...

Then press CTRL+Z to send.


One Response to “SMS PDU in Python for Raspberry Pi”

What Do You Reckon?