Get Started with Super SIM SMS Commands and the Raspberry Pi Pico

The Raspberry Pi Pico would be a great Internet of Things device but for one thing: it has no cellular Internet connectivity. Fortunately, we can fix that with a Super SIM and an add-on cellular module such as Waveshare's Pico SIM7080.

These three components provide an excellent development platform for IoT. And with the Pico's RP2040 microcontroller available in commercial quantities, the development work you do can readily form the basis for production devices later.

The Pico has another advantage: it's highly programmable. Though it's intended for traditional embedded applications, and therefore supports C/C++ development, it can also be used with MicroPython, a version of the popular language that's been tailored for use with microcontrollers rather than full PCs. This means that anyone who's used Python can quickly begin prototyping their own IoT devices with the Pico and Super SIM.

This guide will show you how to build just such a sample IoT device. You can control it remotely, and you can receive data and status messages back. We'll focus on the basic functionality, and use a single Super SIM API, SMS Commands, but it'll nonetheless give you a firm base from which to explore other Super SIM APIs and more sophisticated device-cloud communications.

This guide requires a Twilio account. Sign up here now if you don't have one. It also requires a configured and active Super SIM. If you haven't set up your Super SIM in the Console, please do so now. Our Super SIM First Steps guide has extra help if you need it.


1. Gather your components

This tutorial involves some soldering work. Please make sure you have the right equipment and you're confident to continue.

To complete this tutorial, you will need:

  • A Raspberry Pi Pico

    • These are widely available from many online electronics suppliers.

  • A Waveshare Pico SIM7080 cellular modem

  • An HT16K33-based four-digit, seven-segment LED

  • An MCP9808 temperature sensor

  • A micro USB cable to power the hardware and connect it to your computer.

  • Some jumper wires, and either a large solderless breadboards or two smaller boards clipped together.

You will also need to install and configure Twilio's CLI tool for your platform.


2. Assemble the hardware

The components that require soldering are the Pico, the Waveshare board, the four-digit LED, and the sensor. All you'll need to do is fix connector pins to each device and, in the case of the LED, fit the LED itself to the supplied circuit board.

For the Waveshare Pico SIM7080, we recommend fitting the bundled female header. They're fitted with the Simcom module facing upward as shown in the picture below. You'll need to take a little extra care with the unit's GND pins because the way they're wired means they require extra heat to get the solder to flow around each pin into and into the board. If your solder is sticking to the pin but not the board, you know you haven't got the join hot enough. Place the soldering iron's tip against each of the board's GND holes for a little longer, to get it up to the right temperature.

The Pico itself takes two rows of male header pins so it will slot onto the Waveshare board. You can use the header that came with the module and carefully break it into two even sections. Solder each section to one side of the Pico:

Solder the supplied header pins to the MCP9808 board. For the LED, fit and solder the LED block to the board first, clip the LED's pins, and then solder in the header pins. Make sure you orient the LED correctly: insert it into board so that the four decimal point indicators are adjacent to the 0.56" 7-segment… text on the board. Adafruit has a great guide to show you how to do this if you need more detail.

When you're done soldering, turn the Waveshare board over and fit your Super SIM into the spring-loaded slot. Push out the smallest SIM card from the Super SIM's large plastic mount. It goes in logo face up:

Now turn it back over and fit the Pico. Make sure the Pico's micro USB connector is at the opposite end to the Simcom SIM7080 module:

Fit the combined unit to a breadboard so you have space either side for wiring. Screw one of the thin whip cables that came with the Waveshare board into the large antenna and then clip the other end of the cable to the module's LTE connector. Take care as the U.FL antenna connector is very fragile:

For the first part of the guide, you're going to use the four-digit LED only, so plug it into a free area of the breadboard and wire it up to the Pico using the wires like this:


3. Install MicroPython on the Pico

  1. Visit the MicroPython site and download the latest stable build for the Pico. You can find it here . The file will be named something like rp2-pico-20220618-v1.19.1.uf2 .

  2. While holding down the button on the Pico marked BOOTSEL , connect the Pico to your computer with the micro USB cable. Now release the button.

  3. Depending on your OS, you may see a disk called RPI-RP2 mounted on your desktop, or it might appear elsewhere in your computer's file system. Copy the .uf2 file you downloaded in step 1 to the drive, either by using a command-line copy program like cp, or by dragging and dropping.

    • Windows 10 includes drivers for UF2 devices, but earlier versions will require additional drivers to be installed. This tutorial only covers Windows 10.

  4. If the drive doesn't eject automatically, eject it now. Unplug it and reconnect it, this time without holding down BOOTSEL.


4. Talk to the Pico

Select your computer's operating system from among the tabs below and follow the instructions.

  1. Open your distribution's terminal app.

  2. Get the Pico's device file. It should be /dev/ttyACM0 . You may need to ensure you have access to the serial port: on most distributions this can be done by adding your user account to the dialout user group.

  3. You'll use a command-line serial console tool called Minicom to communicate with the Pico. Install Minicom using your operating system's package manager, such as apt , rpm , dpkg , or similar, e.g., sudo apt install minicom .

  4. Enter minicom -o -D /dev/ttyACM0 to open a connection to the module. If Minicom posts an error indicating that the device is inaccessible, check that you've connected the Pico to your computer and that you copied its device file name correctly.


5. Pause for breath

You've done a lot of work to get this far, and there is plenty more to come. So take five and consider what you'll do in the remaining steps. MicroPython doesn't support the Simcom SIM7078G module directly, so you will need to write code that tells the module what you want it to do: to apply certain settings, and to transfer some information, for example.

The Pico and the modem connect using four data pins, two of which are a serial link that you'll program to communicate with the module shortly. The other two are used to wake the module and to tell it to power down.

These are the pins that connect the Pico and the Waveshare board. Black lines are GND; red PWR. This tutorial's code makes use of the RX, TX and PWR_EN pins.

With the communication link between MicroPython and module established, your code will send commands — called 'AT commands' — to the module and process the responses. We won't go into detail about AT commands here, but there's a good introduction available if you want to learn more. If you already know about AT commands, you can find the Simcom SIM7080's instruction set documented here. These are handy resources for further study, but you don't need to read them to complete this tutorial.


6. Send the Pico some Python code

In Minicom (or PuTTY if you're using Windows 10), hit Ctrl-C to break to the Python REPL. You won't use the REPL directly, but it provides a way to enter large blocks of MicroPython code easily. Now hit Ctrl-E. This makes MicroPython ready to accept a full Python program pasted in. Click on the button at the top right of the code listing below to copy it and then paste it into Minicom. The copy button will appear when you mouse over the code. When you've pasted the code, hit Ctrl-D to tell MicroPython to run the code.

You can find the a complete listing of the code, including all subsequent additions, at our public GitHub repo.

To save scrolling, click here to jump to the rest of the tutorial.

from machine import UART, Pin, I2C
from utime import ticks_ms, sleep

'''
Send an AT command - return True if we got an expected
response ('back'), otherwise False
'''
def send_at(cmd, back="OK", timeout=1000):
    # Send the command and get the response (until timeout)
    buffer = send_at_get_resp(cmd, timeout)
    if len(buffer) > 0: return (back in buffer)
    return False

'''
Send an AT command - just return the response
'''
def send_at_get_resp(cmd, timeout=1000):
    # Send the AT command
    modem.write((cmd + "\r\n").encode())

    # Read and return the response (until timeout)
    return read_buffer(timeout)

'''
Read in the buffer by sampling the UART until timeout
'''
def read_buffer(timeout):
    buffer = bytes()
    now = ticks_ms()
    while (ticks_ms() - now) < timeout and len(buffer) < 1025:
        if modem.any():
            buffer += modem.read(1)
    return buffer.decode()

'''
Module startup detection
Send a command to see if the modem is powered up
'''
def boot_modem():
    state = False
    count = 0
    while count < 20:
        if send_at("ATE1"):
            print("The modem is ready")
            return True
        if not state:
            print("Powering the modem")
            module_power()
            state = True
        sleep(4)
        count += 1
    return False

'''
Power the module on/off
'''
def module_power():
    pwr_key = Pin(14, Pin.OUT)
    pwr_key.value(1)
    sleep(1.5)
    pwr_key.value(0)

'''
Check we are attached
'''
def check_network():
    is_connected = False
    response = send_at_get_resp("AT+COPS?")
    line = split_msg(response, 1)
    if "+COPS:" in line:
        is_connected = (line.find(",") != -1)
        if is_connected: print("Network information:", line)
    return is_connected

'''
Attach to the network
'''
def configure_modem():
    # AT commands can be sent together, not just one at a time.
    # Set the error reporting level, set SMS text mode, delete left-over SMS
    # select LTE-only mode, select Cat-M only mode, set the APN to 'super' for Super SIM
    send_at("AT+CMEE=2;+CMGF=1;+CMGD=,4;+CNMP=38;+CMNB=1;+CGDCONT=1,\"IP\",\"super\"")
    print("Modem configured for Cat-M and Super SIM")

'''
Flash the Pico LED
'''
def led_blink(blinks):
    for i in range(0, blinks):
        led_off()
        sleep(0.25)
        led_on()
        sleep(0.25)

def led_on():
    led.value(1)

def led_off():
    led.value(0)

'''
Split a response from the modem into separate lines,
removing empty lines and returning all that's left or,
if 'want_line' has a non-default value, return that one line
'''
def split_msg(msg, want_line=99922222122222222):
    lines = msg.split("\r\n")
    results = []
    for i in range(0, len(lines)):
        if i == want_line:
            return lines[i]
        if len(lines[i]) > 0:
            results.append(lines[i])
    return results

# Set up the modem UART
modem = UART(0, 115200)

# Set the LED and turn it off
led = Pin(25, Pin.OUT)
led_off()

# Start the modem
if boot_modem():
    configure_modem()

    # Check we're attached
    state = True
    while not check_network():
        if state:
            led_on()
        else:
            led_off()
        state = not state

    # Light the LED
    led_on()
else:
    # Error! Blink LED 5 times
    led_blink(5)
    led_off()

This is the basis of the code you'll work on through the remainder of the guide. What does it do? It defines some useful functions to turn on the modem, configure the modem, and send AT commands and check the responses. It turns the Pico's LED on if it succeeds — otherwise the LED will flash five times as a signal that something went wrong: consult what's being reported in Minicom.

When the code runs, it turns off the Pico's built-in LED. The LED will flash rapidly five times if there was a problem booting the modem.

The LED is turned on when the device is attached to the network. If the LED is flashing slowly, that means it has not yet attached. Please be patient; it will attach shortly, though this can take many minutes in certain circumstances.


7. Listen for SMS Commands

The device code isn't doing much, so let's extend it to listen out for incoming text messages, check them for useful commands, and action any that are received.

First, though, paste the code you copied into a text editor file. You'll make quite a few changes to this as we go, and it'll be easier to do so on a text editor. You'll also be able to grab the code at each stage and paste the latest version into Minicom and over to the Pico.

With the code in your editor, paste the following right after the final function definition, def split_msg(msg, want_line=99):

'''
Extract the SMS index from a modem response line
'''
def get_sms_number(line):
    p = line.split(",")
    if len(p) > 0:
        return p[1]
    return 0

'''
Blink the LED n times after extracting n from the command string
'''
def process_command_led(msg):
    blinks = msg[4:]
    print("Blinking LED",blinks,"time(s)")
    try:
        led_blink(int(blinks))
    except:
        print("BAD COMMAND:",blinks)

'''
Listen for incoming SMS Commands
'''
def listen():
    print("Listening for Commands...")
    while True:
        # Did we receive a Unsolicited Response Code (URC)?
        buffer = read_buffer(5000)
        if len(buffer) > 0:
            lines = split_msg(buffer)
            for line in lines:
                if "+CMTI:" in line:
                    # We received an SMS, so get it...
                    num = get_sms_number(line)
                    msg = send_at_get_resp("AT+CMGR=" + num, 2000)

                    # ...and process it for commands
                    cmd = split_msg(msg, 2)
                    if cmd.startswith("LED="):
                        process_command_led(cmd)
                    else:
                        print("UNKNOWN COMMAND:",cmd)
                    # Delete all SMS now we're done with them
                    send_at("AT+CMGD=,4")

Now add this line right after #Light the LED and led_on() in the section headed # Start the modem:

# Begin listening for commands
listen()

Copy all the code. Go back to Minicom and you should see the >>> prompt. Hit Ctrl-E, paste in the new Python, and then hit Ctrl-D to run it.

This time, you won't see >>> in Minicom but rather the line Listening for Commands.... The Pico is waiting for instructions — let's send it something.

If you get tired of all this cutting and pasting, grab the MicroPython utility Pyboard. Save it to your computer and make the file executable. You can then call it as follows:

Linux/macOS

python pyboard.py -d /dev/ttyACM0 -f cp my_code.py :main.py

Windows 10

python pyboard.py -d COM3 -b 9600 -f cp my_code.py :main.py

where my_code.py is the saved version of the code you're assembling in your text editor. Mac users wlll need to change the device file name. Windows 10 users shoud confirm their device COM port and quit PuTTY if it's still running.

Pyboard will transfer the code to the Pico and present the program's output. It can also be used to transfer files to the Pico's filesystem, but we're not going to need that functionality here. You can read more about Pyboard's features at the MicroPython docs website.


8. Send an SMS Command to the Pico

Open up a new terminal window or tab (or open Command Prompt in Windows) and paste in the following command:

twilio api:supersim:v1:sms-commands:create \
  --sim "<YOUR_SIM_NAME_OR_SID>" \
  --payload "LED=10"

Windows 10 folk, when you copy the example above — and others later on — make sure you either remove each `` so all the elements are on the same line, or replace them with a ^ and a space. The space is important: Windows won't recognize the command as a multi-line entry without it.

You'll need to replace the sections in angle brackets (< and >) with your own information. Your SIM's SID — or friendly name if you've set one — and the specified account credentials are all accessible from the Twilio Console.

This command uses the Super SIM API's SMS Commands feature to send a machine-to-machine message to the Pico by way of the cellular module and its Super SIM. The key part is the --payload "LED=10" part. The payload parameter tells Twilio to send the portion in the double-quotes as the body of the message. In this case, that's LED=10.

The listen() function in your Python code keeps an ear open for incoming SMS messages, which are signaled by the module transmitting a string that includes the characters +CMTI. If it appears, the code sends a new AT command to the modem to get the message (AT+CMGR) and then awaits a response. When the response comes, the code processes it and extracts the LED=10 — which tells the device to flash its LED ten times.


9. Display numeric data

Flashing the LED is a good start, but you can't use it to display readily readable informations. Let's make use of the four-digit display so show some values in bold color.

The display needs code to drive it. Rather than include it all here, just grab the code you need from this public GitHub repo. You'll need the contents of two files — click the links below to view the raw code in your browser, then copy and paste each one into your text editor, right below the import statements at the top.

With the second file, make sure you only copy the class — don't include the first two lines above it (starting # Import the base class...)

You also need to add the following functions further down, and make some changes to listen(). So just copy the following code and use it to replace all of your existing listen() function:

'''
Display the decimal value n after extracting n from the command string
'''
def process_command_num(msg):
    value = msg[4:]
    print("Setting",value,"on the LED")
    try:
        # Extract the decimal value (string) from 'msg' and convert
        # to a hex integer for easy presentation of decimal digits
        hex_value = int(value, 16)
        display.set_number((hex_value & 0xF000) >> 12, 0)
        display.set_number((hex_value & 0x0F00) >>  8, 1)
        display.set_number((hex_value & 0x00F0) >>  4, 2)
        display.set_number((hex_value & 0x000F), 3).update()
    except:
        print("BAD COMMAND:",value)

'''
Listen for incoming SMS Commands
'''
def listen():
    print("Listening for Commands...")
    while True:
        # Did we receive a Unsolicited Response Code (URC)?
        buffer = read_buffer(5000)
        if len(buffer) > 0:
            lines = split_msg(buffer)
            for line in lines:
                if "+CMTI:" in line:
                    # We received an SMS, so get it...
                    num = get_sms_number(line)
                    msg = send_at_get_resp("AT+CMGR=" + num, 2000)

                    # ...and process it for commands
                    cmd = split_msg(msg, 2)
                    if cmd.startswith("LED="):
                        process_command_led(cmd)
                    elif cmd.startswith("NUM="):
                        process_command_num(cmd)
                    else:
                        print("UNKNOWN COMMAND:",cmd)
                    # Delete all SMS now we're done with them
                    _ = send_at("AT+CMGD=,4")

Finally, add the following lines after the # Setup the modem UART block:

# Set up I2C and the display
i2c = I2C(1, scl=Pin(3), sda=Pin(2))
display = HT16K33Segment(i2c)
display.set_brightness(2)
display.clear().draw()

Copy all the new code from your text editor and paste it to the Pico in the usual way.

Now send a slightly different command via SMS:

twilio api:supersim:v1:sms-commands:create \
  --sim "<YOUR_SIM_NAME_OR_SID>" \
  --payload "NUM=2021"

Again, you'll need to do this in a separate terminal tab or window, and replace the <...> sections with your own data.

After a moment or two you should see 2021 appear on the LED. If it doesn't appear after a short time, check you have the LED wired to the Pico correctly — take a look at the diagram above. If you see Setting 2021 on the LED in Minicom, you know the command was received. If not, did you enter the command above correctly? Perhaps you mis-typed the payload value. You'll see an error message in Minicom in this case.

It should be clear what's happening: the code dissects the incoming text message for a command and a value. Before, only the LED command was supported; now so is NUM. For the latter, the value is written to the display.

Now it's time to make the conversation two way.


10. Send SMS Commands from the device

Slot the MCP9808 temperature sensor into the breadboard and wire it up as follows:

We've shown an expanded layout to make clear where the connections go, but you can adjust it as you prefer, especially if you're working with a smaller breadboard.

Now for the code. Add the sensor's driver code — paste all of the following code after the import statements at the top:

class MCP9808:
    """
    A simple driver for the I2C-connected MCP9808 temperature sensor.
    This release supports MicroPython.
    """

    # *********** PRIVATE PROPERTIES **********

    i2c = None
    address = 0x18

    # *********** CONSTRUCTOR **********

    def __init__(self, i2c, i2c_address=0x18):
        assert 0x00 <= i2c_address < 0x80, "ERROR - Invalid I2C address in MCP9808()"
        self.i2c = i2c
        self.address = i2c_address

    # *********** PUBLIC METHODS **********

    def read_temp(self):
        # Read sensor and return its value in degrees celsius.
        temp_bytes = self.i2c.readfrom_mem(self.address, 0x05, 2)
        # Scale and convert to signed value.
        temp_raw = (temp_bytes[0] << 8) | temp_bytes[1]
        temp_cel = (temp_raw & 0x0FFF) / 16.0
        if temp_raw & 0x1000: temp_cel -= 256.0
        return temp_cel

Add the following function below the def process_command_num(msg): function definition:

'''
Get a temperature reading and send it back as an SMS
'''
def process_command_tmp():
    print("Sending a temperature reading")
    celsius_temp = "{:.2f}".format(sensor.read_temp())
    if send_at("AT+CMGS=\"000\"", ">"):
        # '>' is the prompt sent by the modem to signal that
        # it's waiting to receive the message text.
        # 'chr(26)' is the code for ctrl-z, which the modem
        # uses as an end-of-message marker
        r = send_at_get_resp(celsius_temp + chr(26))
'''
Get a temperature reading and send it back as an SMS
'''
def process_command_tmp():
    print("Sending a temperature reading")
    celsius_temp = "{:.2f}".format(sensor.read_temp())
    if send_at("AT+CMGS=\"000\"", ">"):
        # '>' is the prompt sent by the modem to signal that
        # it's waiting to receive the message text.
        # 'chr(26)' is the code for ctrl-z, which the modem
        # uses as an end-of-message marker
        r = send_at_get_resp(celsius_temp + chr(26))

Add these lines to the command checking sequence within the listen() function:

elif cmd.startswith("TMP"):
    process_command_tmp()

Finally, you need to instantiate the sensor for use, so add these lines below the # Set up I2C and the display block:

# Set up the MCP9808 sensor
sensor = MCP9808(i2c=i2c)

Once again, copy all the source code from your editor and paste it over to the Pico. Assuming you made no text entry or pasting errors, the code will run, and you'll shortly see Listening for Commands… as usual.

Your device is now ready to receive TMP commands, which will cause it to send an SMS Command containing the current temperature of the air around it. But you're not quite ready to send the command yet: you need to configure your Super SIM's fleet to relay device-originated messages to your server.

The SMS Commands API provides a specific phone number, 000, to which all SMS messages, from every device, are sent. Twilio relays the ones it has received from your SIMs on to you. How does it know exactly where to send them? You specify a webhook address.

Super SIMs are organized into Fleets: groups of SIMs that have common settings, such as which networks they are able to connect to and whether they can make use of cellular data services. Fleets are represented in the Super SIM API by Fleet resources, and SMS Commands adds properties to each Fleet resource in which you can store your SMS Commands webhook address and, optionally, the HTTP method it uses.

So before you can send a message from the device, you need to set up a webhook target URL and add it to your Super SIM's Fleet.

Beeceptor is a handy service for testing webhooks. It's designed for developing and testing APIs of your own, and offers free mock servers — virtual endpoints that can receive webhook calls. Let's set one up to receive messages from the device.

  1. In a web browser tab, go to Beeceptor .

  2. Enter an endpoint name in the large text field and click Create Endpoint:

  3. On the screen that appears next, click on the upper of the two clipboard icons to copy the endpoint URL:

  4. Keep the tab open.

Now you update your Super SIM's Fleet to add an SMS Commands webhook. You can do this with the API, but we'll use the Console for this demo.

  1. Open a second web browser tab and log into the Console .

  2. Go to IoT > Super SIM > Fleets and select the Fleet containing the Super SIM you are using.

  3. Scroll down to SMS Commands Callback URL and paste in your webhook URL from the previous section. By default, webhooks are triggered with a POST request. Leave it unchanged for now, but if you need to use another method for your own application, you can change it later:

  4. Click Save .

  5. Close the tab if you like.

In a new terminal tab or window, send this API command:

twilio api:supersim:v1:sms-commands:create \
--sim "<YOUR_SIM_NAME_OR_SID>" \
--payload "TMP"

Again, you'll need to replace the <...> sections with your own settings.

11. Read the received temperature

  1. Jump back to the Beeceptor tab in the browser. You should see — or will shortly see — the endpoint has received a POST request. Click on it to see the request body, then on the JSON icon, {:} , to view the data more clearly.

  2. Look for the line beginning Payload= — right after it is the temperature as sent by your device:

  3. Read the temperature measured by the device's sensor.


Next steps

Well done! You've come a long way, but you now have a Raspberry Pi Pico that can communicate with an attached Simcom SIM7080 modem. It can receive and parse commands relayed using Super SIM's SMS Commands array to activate its LED and a connected display. It can retrieve readings from a connected temperature sensor peripheral, and send them back to base — again via SMS Commands. In short, you have an IoT device with two-way communication with your server-side application.

That's not the end of the process. For a start, you have to build that very server-side application. You might also want to produce a mobile or web app to allow your end-users to control elements of your IoT product themselves.

That's in the future, though. For now, you may want to consider ways to expand the testbed you've built today. Try adding some other sensors, output devices, or actuators to allow the unit to affect the physical world.

The code doesn't check connection state. Tell the modem to send you cellular registration notifications and update the listen() function to watch form the. Our guide Four Best Practices for Cellular Module Registration has guidance to help you.

The current command processor is basic — why not try to extend it to support commands, data, and other information, such as timestamps, via a JSON package? Hint: use an encoding technique like base 64 to convert your JSON strings into characters within the 7-bit SMS character set. Third-party libraries can help you with this.

You might also want to allow the device to communicate with Internet-hosted resources directly, to GET or POST data. We're going to cover just that in an upcoming tutorial based on the device you assembled and got running today.

Wherever you take your Raspberry Pi Pico-based IoT device next, we can't wait to see what you build!

Last updated