Raspberry Pi, Python, and 3G Dongles - oh my!


This is a bit of a brain dump / diary of what I've discovered about using 3G dongles to send SMS using Python on the Raspberry Pi.

Here is how to use Python to send an SMS from the Raspberry Pi via a 3G USB dongle.

In order to talk to the dongle, we need to install pyserial

wget http://pypi.python.org/packages/source/p/pyserial/pyserial-2.6.tar.gz
gunzip pyserial-2.6.tar.gz
tar -xvf pyserial-2.6.tar
cd pyserial-2.6
sudo python setup.py install

Save this file as sms.py - make sure you change the phone number and message!

# This is pyserial which is needed to communicate with the dongle
import serial

# Set up the connection to the dongle
dongle = serial.Serial(port="/dev/ttyUSB0",baudrate=115200,timeout=0,rtscts=0,xonxoff=0)

# This sends the command to the dongle
def sendatcmd(cmd):
    dongle.write('AT'+cmd+'r')

# put the dongle into text mode
sendatcmd('+CMGF=1')

# Set the telephone number we want to send to
sendatcmd('+CMGS="+447700900123"')

# Set the message we want to send
dongle.write('Sending from python')

# Pass the CTRL+Z character to let the dongle know we're done
dongle.write(chr(26))

# Close the connection
dongle.close()

Run the file by typing

python sms.py

An SMS should be sent!

SMSC

The SMSC is the the message centre through which an SMS is sent. This is how to discover the SMSC of your SIM

AT+CSCA?

Should generate this as the output.

+CSCA: "+447802002606",145

SMS Modes

To see which modes your mobile supports, you can use the "AT+CMGF=?" command.
You will get a response with the supported SMS formats
0: PDU mode, 1: Text mode

Putting It All Together

My previous program calculated the PDU which needed to be sent to the dongle.

This program asks the user for the destination phone number, message, and whether they want to send a flash SMS or regular SMS.
It gets the SMSC from the dongle, calculates the PDU values, then sends them to the dongle.

Then - hopefully! - an SMS will be sent 🙂

The source is on GitHub.

# This Python file uses the following encoding: utf-8
"""
© 2012 Terence Eden

Adapted from http://rednaxela.net/pdu.php Version 1.5 r9aja
Original JavaScript (c) BPS & co, 2003. Written by Swen-Peter Ekkebus, edited by Ing. Milan Chudik, fixes and functionality by Andrew Alexander.
Original licence http://rednaxela.net/pdu.php "Feel free to use this code as you wish."

Python version © 2012 Terence Eden - released as MIT License
***
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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

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.
***

Note - this is my first Python program - I am quite happy to be corrected on True Pythonic Style etc. :-)
"""

"""
This program allows the user to craft a PDU when sending an SMS.
The user enters the destination number, the message, the class, and the SMSC.
The program generates the commands needed to instruct a modem to deliver the SMS.
"""

# This is pyserial which is needed to communicate with the 3G USB Dongle http://pyserial.sourceforge.net/
import serial


# Array with the default 7 bit alphabet
# @ = 0 = 0b00000000, a = 97 = 0b1100001, etc
# Alignment is purely an attempt at readability
SEVEN_BIT_ALPHABET_ARRAY = (
    '@', '£', '$', '¥', 'è', 'é', 'ù', 'ì', 'ò', 'Ç', 'n', 'Ø', 'ø', 'r','Å', 'å',
    'u0394', '_', 'u03a6', 'u0393', 'u039b', 'u03a9', 'u03a0','u03a8', 'u03a3', 'u0398', 'u039e',
    '€', 'Æ', 'æ', 'ß', 'É', ' ', '!', '"', '#', '¤', '%', '&', ''', '(', ')','*', '+', ',', '-', '.', '/',
    '0', '1', '2', '3', '4', '5', '6', '7','8', '9',
    ':', ';', '< ', '=', '>', '?', '¡',
    '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',
    'Ä', 'Ö',
                                                                     'Ñ', 'Ü', '§', '¿',
    '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',
    'ä', 'ö',
                                                                     'ñ', 'ü',
    'à')


def semi_octet_to_string(input) :
    """ Takes an octet and returns a string
"""
    out = ""
    i=0
    for i in range(0,len(input),2) : # from 0 - length, incrementing by 2
        out = out + input[i+1:i+2] + input[i:i+1]
    return out


def convert_character_to_seven_bit(character) :
    """ Takes a single character.
Looks it up in the SEVEN_BIT_ALPHABET_ARRAY.
Returns the position in the array.
"""
    for i in range(0,len(SEVEN_BIT_ALPHABET_ARRAY)) :
        if SEVEN_BIT_ALPHABET_ARRAY[i] == character:
            return i
    return 36 # If the character cannot be found, return a ¤ to indicate the missing character


def send_AT_command(cmd) :
    """ Send a command to the dongle
"""
    dongle.write(AT_COMMAND+cmd+'r')


def get_SMSC_from_dongle() :
    """ Interogate the dongle and get the SMSC number
"""

    print "Asking the SIM for the SMSC"
    # find the SMSC
    send_AT_command('+CSCA?')

    # read the output, print it to screen. Stop when "OK" is seen
    while True:
        output = dongle.readline()
        print output

        # find the response about the SMSC
        if output.startswith("+CSCA:") :
            first_quote = output.find('"') + 1 # zero based index, first quote
            last_quote = output.rfind('"') # last quote
            SMSC_number = output[first_quote:last_quote] # Extract the string between the "
            print "The SMSC number is " + SMSC_number
            return SMSC_number

        if output.startswith("OK"):
            break
        if output.startswith("ERROR"):
            break



# Set the initial variables
FIRST_OCTET = "0100" # MAGIC
PROTO_ID = "00" # MORE MAGIC
data_encoding = "1" # EVEN MORE MAGIC
message_class = "" # Message Class. 0 for FLASH, 1 for normal
SMSC_number = "" # The message centre through which the SMS is sent
SMSC = "" # How the SMSC is represented once encoded
SMSC_info_length = 0
SMSC_length = 0
SMSC_number_format = "81" # by default, assume that it's in national format - e.g. 077...
destination_phone_number = "" # Where the SMS is being sent
destination_phone_number_format = "81" # by default, assume that it's in national format - e.g. 077...
message_text = "" # The message to be sent
encoded_message_binary_string = "" # The message, as encoded into binary
encoded_message_octet = "" # individual octets of the message
AT_COMMAND = "AT" # Commands sent to dongle should start with this
AT_SET_PDU = "+CMGF=0" # Command to set the dongle into PDU mode
SEND_CHARACTER = chr(26)

# Set up the connection to the dongle
dongle = serial.Serial(port="/dev/ttyUSB0",baudrate=115200,timeout=0,rtscts=0,xonxoff=0)

# Get the user inputs. No error checking in this version :-)
get_destination_phone_number = raw_input("Which phone number do you want to send an SMS to? (e.g. +447700900123) : ")
get_message_text = raw_input("What message do you want to send? : ")
get_message_class = raw_input("For FLASH SMS, type 0. For regular SMS, type 1 : ")

# TODO Error check & sanitize input
destination_phone_number = get_destination_phone_number
message_text = get_message_text
message_class = int(get_message_class)
SMSC_number = get_SMSC_from_dongle() #get_SMSC_number

# Set data encoding
data_encoding = data_encoding + str(message_class)

# Get the SMSC number format
if SMSC_number[:1] == '+' : # if the SMSC starts with a + then it is an international number
    SMSC_number_format = "91"; # international
    SMSC_number = SMSC_number[1:len(SMSC_number)] # Strip off the +

# Odd numbers need to be padded with an "F"
if len(SMSC_number)%2 != 0 :
    SMSC_number = SMSC_number + "F"

# Encode the SMSC number
SMSC = semi_octet_to_string(SMSC_number)

# Calculate the SMSC values
SMSC_info_length = (len(SMSC_number_format + "" + SMSC))/2
SMSC_length = SMSC_info_length;

# Is the number we're sending to in international format?
if destination_phone_number[:1] == '+' : # if it starts with a + then it is an international number
    destination_phone_number_format = "91"; # international
    destination_phone_number = destination_phone_number[1:len(destination_phone_number)] # Strip off the +

# Calculate the destination values in hex (so remove 0x, make upper case, pad with zeros if needed)
destination_phone_number_length = hex(len(destination_phone_number))[2:3].upper().zfill(2)

if len(destination_phone_number)%2 != 0 : # Odd numbers need to be padded
    destination_phone_number = destination_phone_number + "F"

destination = semi_octet_to_string(destination_phone_number)

# Size of the message to be delivered in hex (so remove 0x, make upper case, pad with zeros if needed)
message_data_size = str(hex(len(message_text)))[2:len(message_text)].upper().zfill(2)

# Go through the message text, encoding each character
for i in range(0,len(message_text)) :
    character = message_text[i:i+1] # get the current character
    current = bin(convert_character_to_seven_bit(character)) # translate into the 7bit alphabet
    character_string = str(current) # Make a string of the binary number. eg "0b1110100
    character_binary_string = character_string[2:len(str(character_string))] # Strip off the 0b
    character_padded_7_bit = character_binary_string.zfill(7) # all text must contain 7 bits
    # Concatenate the bits
    # Note, they are added to the START of the string
    encoded_message_binary_string = character_padded_7_bit + encoded_message_binary_string


# Reverse the string to make it easier to count
encoded_message_binary_string_reversed = encoded_message_binary_string[::-1]

# Get each octet into hex
for i in range(0,len(encoded_message_binary_string_reversed),8) : # from 0 - length, incrementing by 8
    # Get the 8 bits, reverse them back to normal, if less than 8, pad them with 0
    encoded_octet = encoded_message_binary_string_reversed[i:i+8][::-1].zfill(8)
    encoded_octet_hex = hex(int(encoded_octet,2)) # Convert to hex

    # Strip the 0x at the start, make uppercase, pad with a leading 0 if needed
    encoded_octet_hex_string = str(encoded_octet_hex)[2:len(encoded_octet_hex)].upper().zfill(2)

    # Concatenate the octet to the message
    encoded_message_octet = encoded_message_octet + encoded_octet_hex_string

# Generate the PDU
PDU = str(SMSC_info_length).zfill(2)
        + str(SMSC_number_format)
        + SMSC
        + FIRST_OCTET
        + str(destination_phone_number_length)
        + destination_phone_number_format
        + destination
        + PROTO_ID
        + data_encoding
        + str(message_data_size)
        + encoded_message_octet

# Generate the AT Commands
AT_CMGS = "+CMGS=" + str((len(PDU)/2) - SMSC_length - 1)

# Show the commands
print AT_COMMAND + AT_SET_PDU
print AT_COMMAND + AT_CMGS
print PDU

# Send the commands to the dongle
send_AT_command("") # Send an initial AT
send_AT_command(AT_SET_PDU) # Send the command to place the dongle in PDU mode
send_AT_command(AT_CMGS) # Send the command showing the length of the upcoming PDU, should prompt for input ">"
dongle.write(PDU) # Send the PDU
dongle.write(SEND_CHARACTER) # Submit the PDU
dongle.close() # Close the connection

The source is on GitHub.


Share this post on…

27 thoughts on “Raspberry Pi, Python, and 3G Dongles - oh my!”

  1. tony says:

    Very interesting and I will use this information. In my application I will also need to answer a call and reply with a mp3 or wav file. I was think of using gammu but I'm concerned about playing wav to the device. Any idea how to make this ?

    Reply
  2. Uthpala says:

    I have tried this with a pc running fedora.dongle is huwawei e153.but when i run the program, terminal will go to blocked state. after some time i pressed Ctrl+C. result was :

    Traceback (most recent call last):
    File "sms.py", line 11, in
    sendatcmd('+CMGF=1')
    File "sms.py", line 8, in sendatcmd
    def sendatcmd(cmd):dongle.write('AT'+cmd+'r')
    File "/home/uthpala/Downloads/pith/pyserial-2.6/serial/serialposix.py", line 475, in write
    n = os.write(self.fd, d)
    KeyboardInterrupt

    What can i do for this?
    This thing is helpful for my project.help me to get through. Thank You so much.

    Reply
  3. Dave says:

    Does the sending of the SMS generate a confirmation code that the message was delivered, the same way as your mobile does? If so how can the Pi read the message and then trigger a response to the user that the message has been sent?

    Reply
  4. I'm using Sakis and a Huawei E173. Is there some configuration that will allow me to do both 3G internet and SMS? I currently seem to only be able to do one or the other as they both want to use ttyUSB0. SMS will not work on ttyUSB1 and I've tried having Sakis3G use ttyUSB1 to no avail. Anyone run into this?

    Cheers!

    Reply
  5. David says:

    Tony why not try asterisk exactly RASPBX to answer and play a mp3 file.

    Reply
  6. Andrew says:

    I know this sounds naive but why did you have to write the program in the putting it all together section? And all the stuff about getting the devises smsc why isn't that in the first program (SMS.py)? Will the SMS.py program work on its own, like it is written in the first section?
    I'm a bit confused 🙁

    Thanks!

    Reply
    1. The first program is if you just want to send a basic text message. It should work by itself.

      The second program gives you more control and allows you to send Class 0 (Flash) SMS.

      Reply
  7. Andrew says:

    Hi again! On this website:
    http://dtuunicef.wordpress.com/2013/05/19/using-the-raspberry-pi-as-a-sms-gateway-by-a-huawei-hilink-3g-dongle/
    I read how to set up my dongle because of a problem with it. Once I have done that would I then have to follow your instructions on your previous page called "3G internet on raspberry pi" which you used to connect to the mobile network and then to send an SMS Do I then use pyserial and the code at the top of this page?? Also, is there anyway to send an SMS without having to connect to the network because I don't want to use any data, only send messages?

    I really appreciate your help.

    Btw. I haven't got my dongle yet so that's why I can't just try this stuff out.

    Thanks!

    Reply
    1. Sorry George - it won't work with USSD. I've not found any way to send and receive USSD messages via the serial port. Sorry!

      Reply
  8. henke says:

    Hi! When I send AT commands to my 3g donge, tp-link ma180, I don't get any response! Only empty responses. Any idea about what I'm doing wrong?

    Reply
  9. Great post!

    I've got my pi connected to a small 3G router via Ethernet - could you point me in the direction for sending SMS on my setup?

    Reply
  10. Ata Fatahi says:

    hi , i use huawei e303. but in my /dev folder i dont have any file named ttyUSB% but they are tty% and ttyS%. when i command dmesg | grep tty i see tty0 enabled.
    and when i try your code this error release :
    File "fsms.py", line 110, in
    SMSC_number = get_SMSC_from_dongle() #get_SMSC_number
    File "fsms.py", line 61, in get_SMSC_from_dongle
    output = dongle.readline()
    File "/usr/local/lib/python2.7/dist-packages/pyserial-2.7-py2.7.egg/serial/serialposix.py", line 475, in read
    raise SerialException('device reports readiness to read but returned no data (device disconnected or multiple access on port?)')
    serial.serialutil.SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)
    can any one help me ?
    thank a lot.

    Reply
  11. Andrew says:

    Hi! Everything is working according to your first instructions so thanks for that!
    However, when I try to send my own AT commands to the dongle to find out the signal strength ( it should be AT+CSQ ) using the first program which you wrote by adding the commands:

    sendatcmd('+CSQ')
    sigStren = dongle.readline()
    print sigStren

    I get no response. Do you know what I'm doing wrong?
    Thanks!

    Reply
    1. Andrew says:

      I've figured it out now. I needed to read more than one line and I was using a usb powered hub so even when the raspberry pi was switched off, the dongle remained on and so it was never power cycled after I took the sim out earlier! Sorry for wasting time 🙂

      Reply
  12. chathura says:

    Hey first of all thanks for sharing. I had try this code( first part/ first method) but i did not work due Errno 16. it says Device or resource busy.
    i want to send sms alert as a part of my school project. Also in my project i use the same dongle to upload some data to a website using sakis3g script.
    cann't i send the sms and use sakis 3g script at the same time?
    thanks in advance

    Reply
  13. youness says:

    Hello, this was very useful and the pdu sms sending succeded. Now we'd like that the raspberry pi recieves the pdu sms.
    Can you help please!

    Reply
  14. anthrax says:

    How do I read the lines?
    I tried doing
    sendatcmd('+CMGL="ALL"')
    output = dongle.readlines()
    print output

    but all I get for the output is [] although I have more than 5 messages.

    Reply
  15. Nivethitha says:

    can i able to send output of other program as SMS?? help me... Thanks in advance.

    Reply
    1. Terence Eden says:

      Yes you can. It is hard to help you without seeing your code. I suggest Github.com or stackoverflow as great resources for learning.
      Good luck!

      Reply
      1. Nivethitha says:

        import serial
        port = "/dev/ttyAMA0" # Raspberry Pi 3

        def parseGPS(data):
        # print "raw:", data
        if data[0:6] == "$GPGGA":
        s = data.split(",")
        if s[7] == '0':
        print "no satellite data available"
        return
        #time = s[1][0:2] + ":" + s[1][2:4] + ":" + s[1][4:6]
        lat = decode(s[2])
        dirLat = s[3]
        lon = decode(s[4])
        dirLon = s[5]
        #alt = s[9] + " m"
        #sat = s[7]
        print ("Latitude: %s %s -- Longitude:%s %s" %(lat, dirLat, lon, dirLon))

        def decode(coord):
        # DDDMM.MMMMM -> DD deg MM.MMMMM min
        v = coord.split(".")
        head = v[0]
        tail = v[1]
        deg = head[0:-2]
        min = head[-1:]
        return deg + "." + min + tail

        ser = serial.Serial(port, baudrate = 9600, timeout = 0.5)
        while True:
        data = ser.readline()
        parseGPS(data)
        This is my code. I am getting location information from GPS connected in serial port. How can i send this information as SMS.Thanks in advance

        Reply
        1. Terence Eden says:

          ...and what happens when you send the output of your code to SMS? What errors do you see?

          Reply

What links here from around this blog?

What are your reckons?

All comments are moderated and may not be published immediately. Your email address will not be published.Allowed HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <p> <pre> <br> <img src="" alt="" title="" srcset="">