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.
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:
You will also need to install and configure Twilio's CLI tool for your platform.
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:
rp2-pico-20220618-v1.19.1.uf2
.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.
Select your computer's operating system from among the tabs below and follow the instructions.
/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.apt
, rpm
, dpkg
, or similar, e.g., sudo apt install minicom
.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.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.
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. 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.
1from machine import UART, Pin, I2C2from utime import ticks_ms, sleep34'''5Send an AT command - return True if we got an expected6response ('back'), otherwise False7'''8def send_at(cmd, back="OK", timeout=1000):9# Send the command and get the response (until timeout)10buffer = send_at_get_resp(cmd, timeout)11if len(buffer) > 0: return (back in buffer)12return False1314'''15Send an AT command - just return the response16'''17def send_at_get_resp(cmd, timeout=1000):18# Send the AT command19modem.write((cmd + "\r\n").encode())2021# Read and return the response (until timeout)22return read_buffer(timeout)2324'''25Read in the buffer by sampling the UART until timeout26'''27def read_buffer(timeout):28buffer = bytes()29now = ticks_ms()30while (ticks_ms() - now) < timeout and len(buffer) < 1025:31if modem.any():32buffer += modem.read(1)33return buffer.decode()3435'''36Module startup detection37Send a command to see if the modem is powered up38'''39def boot_modem():40state = False41count = 042while count < 20:43if send_at("ATE1"):44print("The modem is ready")45return True46if not state:47print("Powering the modem")48module_power()49state = True50sleep(4)51count += 152return False5354'''55Power the module on/off56'''57def module_power():58pwr_key = Pin(14, Pin.OUT)59pwr_key.value(1)60sleep(1.5)61pwr_key.value(0)6263'''64Check we are attached65'''66def check_network():67is_connected = False68response = send_at_get_resp("AT+COPS?")69line = split_msg(response, 1)70if "+COPS:" in line:71is_connected = (line.find(",") != -1)72if is_connected: print("Network information:", line)73return is_connected7475'''76Attach to the network77'''78def configure_modem():79# AT commands can be sent together, not just one at a time.80# Set the error reporting level, set SMS text mode, delete left-over SMS81# select LTE-only mode, select Cat-M only mode, set the APN to 'super' for Super SIM82send_at("AT+CMEE=2;+CMGF=1;+CMGD=,4;+CNMP=38;+CMNB=1;+CGDCONT=1,\"IP\",\"super\"")83print("Modem configured for Cat-M and Super SIM")8485'''86Flash the Pico LED87'''88def led_blink(blinks):89for i in range(0, blinks):90led_off()91sleep(0.25)92led_on()93sleep(0.25)9495def led_on():96led.value(1)9798def led_off():99led.value(0)100101'''102Split a response from the modem into separate lines,103removing empty lines and returning all that's left or,104if 'want_line' has a non-default value, return that one line105'''106def split_msg(msg, want_line=99922222122222222):107lines = msg.split("\r\n")108results = []109for i in range(0, len(lines)):110if i == want_line:111return lines[i]112if len(lines[i]) > 0:113results.append(lines[i])114return results115116# Set up the modem UART117modem = UART(0, 115200)118119# Set the LED and turn it off120led = Pin(25, Pin.OUT)121led_off()122123# Start the modem124if boot_modem():125configure_modem()126127# Check we're attached128state = True129while not check_network():130if state:131led_on()132else:133led_off()134state = not state135136# Light the LED137led_on()138else:139# Error! Blink LED 5 times140led_blink(5)141led_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.
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):
1'''2Extract the SMS index from a modem response line3'''4def get_sms_number(line):5p = line.split(",")6if len(p) > 0:7return p[1]8return 0910'''11Blink the LED n times after extracting n from the command string12'''13def process_command_led(msg):14blinks = msg[4:]15print("Blinking LED",blinks,"time(s)")16try:17led_blink(int(blinks))18except:19print("BAD COMMAND:",blinks)2021'''22Listen for incoming SMS Commands23'''24def listen():25print("Listening for Commands...")26while True:27# Did we receive a Unsolicited Response Code (URC)?28buffer = read_buffer(5000)29if len(buffer) > 0:30lines = split_msg(buffer)31for line in lines:32if "+CMTI:" in line:33# We received an SMS, so get it...34num = get_sms_number(line)35msg = send_at_get_resp("AT+CMGR=" + num, 2000)3637# ...and process it for commands38cmd = split_msg(msg, 2)39if cmd.startswith("LED="):40process_command_led(cmd)41else:42print("UNKNOWN COMMAND:",cmd)43# Delete all SMS now we're done with them44send_at("AT+CMGD=,4")
Now add this line right after #Light the LED
and led_on()
in the section headed # Start the modem
:
1# Begin listening for commands2listen()
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.
Open up a new terminal window or tab (or open Command Prompt in Windows) and paste in the following command:
1twilio api:supersim:v1:sms-commands:create \2--sim "<YOUR_SIM_NAME_OR_SID>" \3--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.
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:
1'''2Display the decimal value n after extracting n from the command string3'''4def process_command_num(msg):5value = msg[4:]6print("Setting",value,"on the LED")7try:8# Extract the decimal value (string) from 'msg' and convert9# to a hex integer for easy presentation of decimal digits10hex_value = int(value, 16)11display.set_number((hex_value & 0xF000) >> 12, 0)12display.set_number((hex_value & 0x0F00) >> 8, 1)13display.set_number((hex_value & 0x00F0) >> 4, 2)14display.set_number((hex_value & 0x000F), 3).update()15except:16print("BAD COMMAND:",value)1718'''19Listen for incoming SMS Commands20'''21def listen():22print("Listening for Commands...")23while True:24# Did we receive a Unsolicited Response Code (URC)?25buffer = read_buffer(5000)26if len(buffer) > 0:27lines = split_msg(buffer)28for line in lines:29if "+CMTI:" in line:30# We received an SMS, so get it...31num = get_sms_number(line)32msg = send_at_get_resp("AT+CMGR=" + num, 2000)3334# ...and process it for commands35cmd = split_msg(msg, 2)36if cmd.startswith("LED="):37process_command_led(cmd)38elif cmd.startswith("NUM="):39process_command_num(cmd)40else:41print("UNKNOWN COMMAND:",cmd)42# Delete all SMS now we're done with them43_ = send_at("AT+CMGD=,4")
Finally, add the following lines after the # Setup the modem UART
block:
1# Set up I2C and the display2i2c = I2C(1, scl=Pin(3), sda=Pin(2))3display = HT16K33Segment(i2c)4display.set_brightness(2)5display.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:
1twilio api:supersim:v1:sms-commands:create \2--sim "<YOUR_SIM_NAME_OR_SID>" \3--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.
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:
1class MCP9808:2"""3A simple driver for the I2C-connected MCP9808 temperature sensor.4This release supports MicroPython.5"""67# *********** PRIVATE PROPERTIES **********89i2c = None10address = 0x181112# *********** CONSTRUCTOR **********1314def __init__(self, i2c, i2c_address=0x18):15assert 0x00 <= i2c_address < 0x80, "ERROR - Invalid I2C address in MCP9808()"16self.i2c = i2c17self.address = i2c_address1819# *********** PUBLIC METHODS **********2021def read_temp(self):22# Read sensor and return its value in degrees celsius.23temp_bytes = self.i2c.readfrom_mem(self.address, 0x05, 2)24# Scale and convert to signed value.25temp_raw = (temp_bytes[0] << 8) | temp_bytes[1]26temp_cel = (temp_raw & 0x0FFF) / 16.027if temp_raw & 0x1000: temp_cel -= 256.028return temp_cel
Add the following function below the def process_command_num(msg):
function definition:
1'''2Get a temperature reading and send it back as an SMS3'''4def process_command_tmp():5print("Sending a temperature reading")6celsius_temp = "{:.2f}".format(sensor.read_temp())7if send_at("AT+CMGS=\"000\"", ">"):8# '>' is the prompt sent by the modem to signal that9# it's waiting to receive the message text.10# 'chr(26)' is the code for ctrl-z, which the modem11# uses as an end-of-message marker12r = send_at_get_resp(celsius_temp + chr(26))
Add these lines to the command checking sequence within the listen()
function:
1elif cmd.startswith("TMP"):2process_command_tmp()
Finally, you need to instantiate the sensor for use, so add these lines below the # Set up I2C and the display
block:
1# Set up the MCP9808 sensor2sensor = 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.
In a web browser tab, go to Beeceptor.
Enter an endpoint name in the large text field and click Create Endpoint:
On the screen that appears next, click on the upper of the two clipboard icons to copy the endpoint URL:
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.
Open a second web browser tab and log into the Console.
Go to IoT > Super SIM > Fleets and select the Fleet containing the Super SIM you are using.
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:
Click Save.
Close the tab if you like.
In a new terminal tab or window, send this API command:
1twilio api:supersim:v1:sms-commands:create \2--sim "<YOUR_SIM_NAME_OR_SID>" \3--payload "TMP"
Again, you'll need to replace the <...>
sections with your own settings.
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.
Look for the line beginning Payload=
— right after it is the temperature as sent by your device:
Read the temperature measured by the device's sensor.
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!
Check out our follow-up tutorial: Get Started with Super SIM: Raspberry Pi Pico Data Comms.