Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Get Started with Data Comms and the Raspberry Pi Pico


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

Raspberry Pi Pico and Waveshare Pico SIM7080.

In our tutorial Get Started with SMS Commands and the Raspberry Pi Pico, we combined the Pico, the Waveshare Pico SIM7080 cellular module board, an MCP9808 temperature sensor, and a four-digit, seven-segment LED display into a prototype Internet of Things (IoT) development device.

This device uses Super SIM's SMS Commands API to receive commands over-the-air and, when instructed, to send back information. This works very well for device-to-user communications routed through your cloud, but what if you want the device to be able to reach out to other Internet resources? For that you need a data connection and the ability to make HTTP requests — GET, POST, PUT, etc. — and parse the remote server's response.

This tutorial will take you through the process of adding exactly this functionality to your IoT application.

(warning)

Warning


1. Set up the hardware and software

1-set-up-the-hardware-and-software page anchor

If you have already completed Get Started with SMS Commands and the Raspberry Pi Pico, you're ready to jump straight to Step 2, below. If not, run through the SMS Commands tutorial's first four steps, which cover the crucial hardware and software setup that you will need to undertake in order to complete this tutorial.

Head there now and then come back here when you've completed Step 4.


2. Prepare the initial Python code

2-prepare-the-initial-python-code page anchor

Throughout this tutorial, you'll be pasting code from this page into a text editor, first the code below and then additional functions as you progress through the guide. At each stage, you'll copy the current code from your editor and paste it across to the Pico. The code included here entirely replaces hat from the previous tutorial in the series.

At this point, you should have a Pico with MicroPython installed. It should be fitted to the Waveshare board and connected to your computer by USB cable. You should have fired up Minicom (Mac/Linux) or PuTTY (Windows) and have the MicroPython REPL prompt, >>>. Hit Ctrl-C to exit the running program, if you don't see the prompt.

As a reminder, hit Ctrl-E to enter MicroPython's 'paste mode', paste in code copied from your text editor, and then hit Ctrl-D to start running it.

Alternatively, if you're a Mac or Linux user, you can use the pyboard.py tool to beam it over for you and relay the output to your terminal — details here.

Here's the base code listing. Copy it — click on the copy icon in the top right corner of the listing; it'll appear as you mouse over the code — and paste it into your text editor.

(information)

Info

You can find the a complete listing of the code, including all subsequent additions, at our public GitHub repo(link takes you to an external page).

Don't send it over to the Pico just yet — you'll need to complete Step 3 first.

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


_608
from machine import UART, Pin, I2C
_608
from utime import ticks_ms, sleep
_608
import json
_608
_608
class MCP9808:
_608
"""
_608
A simple driver for the I2C-connected MCP9808 temperature sensor.
_608
This release supports MicroPython.
_608
"""
_608
_608
# *********** PRIVATE PROPERTIES **********
_608
_608
i2c = None
_608
address = 0x18
_608
_608
# *********** CONSTRUCTOR **********
_608
_608
def __init__(self, i2c, i2c_address=0x18):
_608
assert 0x00 <= i2c_address < 0x80, "ERROR - Invalid I2C address in MCP9808()"
_608
self.i2c = i2c
_608
self.address = i2c_address
_608
_608
# *********** PUBLIC METHODS **********
_608
_608
def read_temp(self):
_608
# Read sensor and return its value in degrees celsius.
_608
temp_bytes = self.i2c.readfrom_mem(self.address, 0x05, 2)
_608
# Scale and convert to signed value.
_608
temp_raw = (temp_bytes[0] << 8) | temp_bytes[1]
_608
temp_cel = (temp_raw & 0x0FFF) / 16.0
_608
if temp_raw & 0x1000: temp_cel -= 256.0
_608
return temp_cel
_608
_608
class HT16K33:
_608
"""
_608
A simple, generic driver for the I2C-connected Holtek HT16K33 controller chip.
_608
This release supports MicroPython and CircuitPython
_608
_608
Version: 3.0.2
_608
Bus: I2C
_608
Author: Tony Smith (@smittytone)
_608
License: MIT
_608
Copyright: 2020
_608
"""
_608
_608
# *********** CONSTANTS **********
_608
_608
HT16K33_GENERIC_DISPLAY_ON = 0x81
_608
HT16K33_GENERIC_DISPLAY_OFF = 0x80
_608
HT16K33_GENERIC_SYSTEM_ON = 0x21
_608
HT16K33_GENERIC_SYSTEM_OFF = 0x20
_608
HT16K33_GENERIC_DISPLAY_ADDRESS = 0x00
_608
HT16K33_GENERIC_CMD_BRIGHTNESS = 0xE0
_608
HT16K33_GENERIC_CMD_BLINK = 0x81
_608
_608
# *********** PRIVATE PROPERTIES **********
_608
_608
i2c = None
_608
address = 0
_608
brightness = 15
_608
flash_rate = 0
_608
_608
# *********** CONSTRUCTOR **********
_608
_608
def __init__(self, i2c, i2c_address):
_608
assert 0x00 <= i2c_address < 0x80, "ERROR - Invalid I2C address in HT16K33()"
_608
self.i2c = i2c
_608
self.address = i2c_address
_608
self.power_on()
_608
_608
# *********** PUBLIC METHODS **********
_608
_608
def set_blink_rate(self, rate=0):
_608
"""
_608
Set the display's flash rate.
_608
"""
_608
assert rate in (0, 0.5, 1, 2), "ERROR - Invalid blink rate set in set_blink_rate()"
_608
self.blink_rate = rate & 0x03
_608
self._write_cmd(self.HT16K33_GENERIC_CMD_BLINK | rate << 1)
_608
_608
def set_brightness(self, brightness=15):
_608
"""
_608
Set the display's brightness (ie. duty cycle).
_608
"""
_608
if brightness < 0 or brightness > 15: brightness = 15
_608
self.brightness = brightness
_608
self._write_cmd(self.HT16K33_GENERIC_CMD_BRIGHTNESS | brightness)
_608
_608
def draw(self):
_608
"""
_608
Writes the current display buffer to the display itself.
_608
"""
_608
self._render()
_608
_608
def update(self):
_608
"""
_608
Alternative for draw() for backwards compatibility
_608
"""
_608
self._render()
_608
_608
def clear(self):
_608
"""
_608
Clear the buffer.
_608
"""
_608
for i in range(0, len(self.buffer)): self.buffer[i] = 0x00
_608
return self
_608
_608
def power_on(self):
_608
"""
_608
Power on the controller and display.
_608
"""
_608
self._write_cmd(self.HT16K33_GENERIC_SYSTEM_ON)
_608
self._write_cmd(self.HT16K33_GENERIC_DISPLAY_ON)
_608
_608
def power_off(self):
_608
"""
_608
Power on the controller and display.
_608
"""
_608
self._write_cmd(self.HT16K33_GENERIC_DISPLAY_OFF)
_608
self._write_cmd(self.HT16K33_GENERIC_SYSTEM_OFF)
_608
_608
# ********** PRIVATE METHODS **********
_608
_608
def _render(self):
_608
"""
_608
Write the display buffer out to I2C
_608
"""
_608
buffer = bytearray(len(self.buffer) + 1)
_608
buffer[1:] = self.buffer
_608
buffer[0] = 0x00
_608
self.i2c.writeto(self.address, bytes(buffer))
_608
_608
def _write_cmd(self, byte):
_608
"""
_608
Writes a single command to the HT16K33. A private method.
_608
"""
_608
self.i2c.writeto(self.address, bytes([byte]))
_608
_608
class HT16K33Segment(HT16K33):
_608
"""
_608
Micro/Circuit Python class for the Adafruit 0.56-in 4-digit,
_608
7-segment LED matrix backpack and equivalent Featherwing.
_608
_608
Version: 3.0.2
_608
Bus: I2C
_608
Author: Tony Smith (@smittytone)
_608
License: MIT
_608
Copyright: 2020
_608
"""
_608
_608
# *********** CONSTANTS **********
_608
_608
HT16K33_SEGMENT_COLON_ROW = 0x04
_608
HT16K33_SEGMENT_MINUS_CHAR = 0x10
_608
HT16K33_SEGMENT_DEGREE_CHAR = 0x11
_608
HT16K33_SEGMENT_SPACE_CHAR = 0x00
_608
_608
# The positions of the segments within the buffer
_608
POS = (0, 2, 6, 8)
_608
_608
# Bytearray of the key alphanumeric characters we can show:
_608
# 0-9, A-F, minus, degree
_608
CHARSET = b'\x3F\x06\x5B\x4F\x66\x6D\x7D\x07\x7F\x6F\x5F\x7C\x58\x5E\x7B\x71\x40\x63'
_608
_608
# *********** CONSTRUCTOR **********
_608
_608
def __init__(self, i2c, i2c_address=0x70):
_608
self.buffer = bytearray(16)
_608
super(HT16K33Segment, self).__init__(i2c, i2c_address)
_608
_608
# *********** PUBLIC METHODS **********
_608
_608
def set_colon(self, is_set=True):
_608
"""
_608
Set or unset the display's central colon symbol.
_608
"""
_608
self.buffer[self.HT16K33_SEGMENT_COLON_ROW] = 0x02 if is_set is True else 0x00
_608
return self
_608
_608
def set_glyph(self, glyph, digit=0, has_dot=False):
_608
"""
_608
Present a user-defined character glyph at the specified digit.
_608
"""
_608
assert 0 <= digit < 4, "ERROR - Invalid digit (0-3) set in set_glyph()"
_608
assert 0 <= glyph < 0xFF, "ERROR - Invalid glyph (0x00-0xFF) set in set_glyph()"
_608
self.buffer[self.POS[digit]] = glyph
_608
if has_dot is True: self.buffer[self.POS[digit]] |= 0x80
_608
return self
_608
_608
def set_number(self, number, digit=0, has_dot=False):
_608
"""
_608
Present single decimal value (0-9) at the specified digit.
_608
"""
_608
assert 0 <= digit < 4, "ERROR - Invalid digit (0-3) set in set_number()"
_608
assert 0 <= number < 10, "ERROR - Invalid value (0-9) set in set_number()"
_608
return self.set_character(str(number), digit, has_dot)
_608
_608
def set_character(self, char, digit=0, has_dot=False):
_608
"""
_608
Present single alphanumeric character at the specified digit.
_608
"""
_608
assert 0 <= digit < 4, "ERROR - Invalid digit set in set_character()"
_608
char = char.lower()
_608
char_val = 0xFF
_608
if char == "deg":
_608
char_val = HT16K33_SEGMENT_DEGREE_CHAR
_608
elif char == '-':
_608
char_val = self.HT16K33_SEGMENT_MINUS_CHAR
_608
elif char == ' ':
_608
char_val = self.HT16K33_SEGMENT_SPACE_CHAR
_608
elif char in 'abcdef':
_608
char_val = ord(char) - 87
_608
elif char in '0123456789':
_608
char_val = ord(char) - 48
_608
assert char_val != 0xFF, "ERROR - Invalid char string set in set_character()"
_608
self.buffer[self.POS[digit]] = self.CHARSET[char_val]
_608
if has_dot is True: self.buffer[self.POS[digit]] |= 0x80
_608
return self
_608
_608
'''
_608
Send an AT command - return True if we got an expected
_608
response ('back'), otherwise False
_608
'''
_608
def send_at(cmd, back="OK", timeout=1000):
_608
# Send the command and get the response (until timeout)
_608
buffer = send_at_get_resp(cmd, timeout)
_608
if len(buffer) > 0: return (back in buffer)
_608
return False
_608
_608
'''
_608
Send an AT command - just return the response
_608
'''
_608
def send_at_get_resp(cmd, timeout=1000):
_608
# Send the AT command
_608
modem.write((cmd + "\r\n").encode())
_608
_608
# Read and return the response (until timeout)
_608
return read_buffer(timeout)
_608
_608
'''
_608
Read in the buffer by sampling the UART until timeout
_608
'''
_608
def read_buffer(timeout):
_608
buffer = bytes()
_608
now = ticks_ms()
_608
while (ticks_ms() - now) < timeout and len(buffer) < 1025:
_608
if modem.any():
_608
buffer += modem.read(1)
_608
return buffer.decode()
_608
_608
'''
_608
Module startup detection
_608
Send a command to see if the modem is powered up
_608
'''
_608
def boot_modem():
_608
state = False
_608
count = 0
_608
while count < 20:
_608
if send_at("ATE1"):
_608
print("The modem is ready")
_608
return True
_608
if not state:
_608
print("Powering the modem")
_608
module_power()
_608
state = True
_608
sleep(4)
_608
count += 1
_608
return False
_608
_608
'''
_608
Power the module on/off
_608
'''
_608
def module_power():
_608
pwr_key = Pin(14, Pin.OUT)
_608
pwr_key.value(1)
_608
sleep(1.5)
_608
pwr_key.value(0)
_608
_608
'''
_608
Check we are attached
_608
'''
_608
def check_network():
_608
is_connected = False
_608
response = send_at_get_resp("AT+COPS?")
_608
line = split_msg(response, 1)
_608
if "+COPS:" in line:
_608
is_connected = (line.find(",") != -1)
_608
if is_connected: print("Network information:", line)
_608
return is_connected
_608
_608
'''
_608
Configure the modem
_608
'''
_608
def configure_modem():
_608
# NOTE AT commands can be sent together, not one at a time.
_608
# Set the error reporting level, set SMS text mode, delete left-over SMS
_608
# select LTE-only mode, select Cat-M only mode, set the APN to 'super' for Super SIM
_608
send_at("AT+CMEE=2;+CMGF=1;+CMGD=,4;+CNMP=38;+CMNB=1;+CGDCONT=1,\"IP\",\"super\"")
_608
# Set SSL version, SSL no verify, set HTTPS request parameters
_608
send_at("AT+CSSLCFG=\"sslversion\",1,3;+SHSSL=1,\"\";+SHCONF=\"BODYLEN\",1024;+SHCONF=\"HEADERLEN\",350")
_608
print("Modem configured for Cat-M and Super SIM")
_608
_608
'''
_608
Open/close a data connection to the server
_608
'''
_608
def open_data_conn():
_608
# Activate a data connection using PDP 0,
_608
# but first check it's not already open
_608
response = send_at_get_resp("AT+CNACT?")
_608
line = split_msg(response, 1)
_608
status = get_field_value(line, 1)
_608
_608
if status == "0":
_608
# Inactive data connection so start one up
_608
success = send_at("AT+CNACT=0,1", "ACTIVE", 2000)
_608
elif status in ("1", "2"):
_608
# Active or operating data connection
_608
success = True
_608
_608
print("Data connection", "active" if success else "inactive")
_608
return success
_608
_608
def close_data_conn():
_608
# Just close the connection down
_608
send_at("AT+CNACT=0,0")
_608
print("Data connection inactive")
_608
_608
'''
_608
Start/end an HTTP session
_608
'''
_608
def start_session(server):
_608
# Deal with an existing session if there is one
_608
if send_at("AT+SHSTATE?", "1"):
_608
print("Closing existing HTTP session")
_608
send_at("AT+SHDISC")
_608
_608
# Configure a session with the server...
_608
send_at("AT+SHCONF=\"URL\",\"" + server + "\"")
_608
_608
# ...and open it
_608
resp = send_at_get_resp("AT+SHCONN", 2000)
_608
# The above command may take a while to return, so
_608
# continue to check the UART until we have a response,
_608
# or 90s passes (timeout)
_608
now = ticks_ms()
_608
while ((ticks_ms() - now) < 90000):
_608
#if len(resp) > 0: print(resp)
_608
if "OK" in resp: return True
_608
if "ERROR" in resp: return False
_608
resp = read_buffer(1000)
_608
return False
_608
_608
def end_session():
_608
# Break the link to the server
_608
send_at("AT+SHDISC")
_608
print("HTTP session closed")
_608
_608
'''
_608
Set a standard request header
_608
'''
_608
def set_request_header():
_608
global req_head_set
_608
_608
# Check state variable to see if we need to
_608
# set the standard request header
_608
if not req_head_set:
_608
send_at("AT+SHCHEAD")
_608
send_at("AT+SHAHEAD=\"Content-Type\",\"application/x-www-form-urlencoded\";+SHAHEAD=\"User-Agent\",\"twilio-pi-pico/1.0.0\"")
_608
send_at("AT+SHAHEAD=\"Cache-control\",\"no-cache\";+SHAHEAD=\"Connection\",\"keep-alive\";+SHAHEAD=\"Accept\",\"*/*\"")
_608
req_head_set = True
_608
_608
'''
_608
Make a request to the specified server
_608
'''
_608
def issue_request(server, path, body, verb):
_608
result = ""
_608
_608
# Check the request verb
_608
code = 0
_608
verbs = ["GET", "PUT", "POST", "PATCH", "HEAD"]
_608
if verb.upper() in verbs:
_608
code = verbs.index(verb) + 1
_608
else:
_608
print("ERROR -- Unknown request verb specified")
_608
return ""
_608
_608
# Attempt to open a data session
_608
if start_session(server):
_608
print("HTTP session open")
_608
# Issue the request...
_608
set_request_header()
_608
print("HTTP request verb code:",code)
_608
if body != None: set_request_body(body)
_608
response = send_at_get_resp("AT+SHREQ=\"" + path + "\"," + str(code))
_608
start = ticks_ms()
_608
while ((ticks_ms() - start) < 90000):
_608
if "+SHREQ:" in response: break
_608
response = read_buffer(1000)
_608
_608
# ...and process the response
_608
lines = split_msg(response)
_608
for line in lines:
_608
if len(line) == 0: continue
_608
if "+SHREQ:" in line:
_608
status_code = get_field_value(line, 1)
_608
if int(status_code) > 299:
_608
print("ERROR -- HTTP status code",status_code)
_608
break
_608
_608
# Get the data from the modem
_608
data_length = get_field_value(line, 2)
_608
if data_length == "0": break
_608
response = send_at_get_resp("AT+SHREAD=0," + data_length)
_608
_608
# The JSON data may be multi-line so store everything in the
_608
# response that comes after (and including) the first '{'
_608
pos = response.find("{")
_608
if pos != -1: result = response[pos:]
_608
end_session()
_608
else:
_608
print("ERROR -- Could not connect to server")
_608
return result
_608
_608
'''
_608
Flash the Pico LED
_608
'''
_608
def led_blink(blinks):
_608
for i in range(0, blinks):
_608
led_off()
_608
sleep(0.25)
_608
led_on()
_608
sleep(0.25)
_608
_608
def led_on():
_608
led.value(1)
_608
_608
def led_off():
_608
led.value(0)
_608
_608
'''
_608
Split a response from the modem into separate lines,
_608
removing empty lines and returning all that's left or,
_608
if 'want_line' has a non-default value, return that one line
_608
'''
_608
def split_msg(msg, want_line=99):
_608
lines = msg.split("\r\n")
_608
results = []
_608
for i in range(0, len(lines)):
_608
if i == want_line:
_608
return lines[i]
_608
if len(lines[i]) > 0:
_608
results.append(lines[i])
_608
return results
_608
_608
'''
_608
Extract the SMS index from a modem response line
_608
'''
_608
def get_sms_number(line):
_608
return get_field_value(line, 1)
_608
_608
'''
_608
Extract a comma-separated field value from a line
_608
'''
_608
def get_field_value(line, field_num):
_608
parts = line.split(",")
_608
if len(parts) > field_num:
_608
return parts[field_num]
_608
return ""
_608
_608
'''
_608
Blink the LED n times after extracting n from the command string
_608
'''
_608
def process_command_led(msg):
_608
blinks = msg[4:]
_608
print("Blinking LED",blinks,"time(s)")
_608
try:
_608
led_blink(int(blinks))
_608
except:
_608
print("BAD COMMAND:",blinks)
_608
_608
'''
_608
Display the decimal value n after extracting n from the command string
_608
'''
_608
def process_command_num(msg):
_608
value = msg[4:]
_608
print("Setting",value,"on the LED")
_608
try:
_608
# Extract the decimal value (string) from 'msg' and convert
_608
# to a hex integer for easy presentation of decimal digits
_608
hex_value = int(value, 16)
_608
display.set_number((hex_value & 0xF000) >> 12, 0)
_608
display.set_number((hex_value & 0x0F00) >> 8, 1)
_608
display.set_number((hex_value & 0x00F0) >> 4, 2)
_608
display.set_number((hex_value & 0x000F), 3).update()
_608
except:
_608
print("BAD COMMAND:",value)
_608
_608
'''
_608
Get a temperature reading and send it back as an SMS
_608
'''
_608
def process_command_tmp():
_608
print("Sending a temperature reading")
_608
celsius_temp = "{:.2f}".format(sensor.read_temp())
_608
if send_at("AT+CMGS=\"000\"", ">"):
_608
# '>' is the prompt sent by the modem to signal that
_608
# it's waiting to receive the message text.
_608
# 'chr(26)' is the code for ctrl-z, which the modem
_608
# uses as an end-of-message marker
_608
r = send_at_get_resp(celsius_temp + chr(26))
_608
_608
'''
_608
Make a request to a sample server
_608
'''
_608
def process_command_get():
_608
print("Requesting data...")
_608
server = "YOUR_BEECEPTOR_URL"
_608
endpoint_path = "/api/v1/status"
_608
_608
# Attempt to open a data connection
_608
if open_data_conn():
_608
result = issue_request(server, endpoint_path, None, "GET")
_608
if len(result) > 0:
_608
# Decode the received JSON
_608
try:
_608
response = json.loads(result)
_608
# Extract an integer value and show it on the display
_608
if "status" in response:
_608
process_command_num("NUM=" + str(response["status"]))
_608
except:
_608
print("ERROR -- No JSON data received. Raw:\n",result)
_608
else:
_608
print("ERROR -- No JSON data received")
_608
_608
# Close the open connection
_608
close_data_conn()
_608
_608
'''
_608
Listen for incoming SMS Commands
_608
'''
_608
def listen():
_608
print("Listening for Commands...")
_608
while True:
_608
# Did we receive a Unsolicited Response Code (URC)?
_608
buffer = read_buffer(5000)
_608
if len(buffer) > 0:
_608
lines = split_msg(buffer)
_608
for line in lines:
_608
if "+CMTI:" in line:
_608
# We received an SMS, so get it...
_608
num = get_sms_number(line)
_608
msg = send_at_get_resp("AT+CMGR=" + num, 2000)
_608
_608
# ...and process it for commands
_608
cmd = split_msg(msg, 2).upper()
_608
if cmd.startswith("LED="):
_608
process_command_led(cmd)
_608
elif cmd.startswith("NUM="):
_608
process_command_num(cmd)
_608
elif cmd.startswith("TMP"):
_608
process_command_tmp()
_608
elif cmd.startswith("GET"):
_608
process_command_get()
_608
else:
_608
print("UNKNOWN COMMAND:",cmd)
_608
# Delete all SMS now we're done with them
_608
send_at("AT+CMGD=,4")
_608
_608
# Globals
_608
req_head_set = False
_608
_608
# Set up the modem UART
_608
modem = UART(0, 115200)
_608
_608
# Set up I2C and the display
_608
i2c = I2C(1, scl=Pin(3), sda=Pin(2))
_608
display = HT16K33Segment(i2c)
_608
display.set_brightness(2)
_608
display.clear().draw()
_608
_608
# Set up the MCP9808 sensor
_608
sensor = MCP9808(i2c=i2c)
_608
_608
# Set the LED and turn it off
_608
led = Pin(25, Pin.OUT)
_608
led_off()
_608
_608
# Start the modem
_608
if boot_modem():
_608
configure_modem()
_608
_608
# Check we're attached
_608
state = True
_608
while not check_network():
_608
if state:
_608
led_on()
_608
else:
_608
led_off()
_608
state = not state
_608
_608
# Light the LED
_608
led_on()
_608
_608
# Begin listening for commands
_608
listen()
_608
else:
_608
# Error! Blink LED 5 times
_608
led_blink(5)
_608
led_off()

This is the basis of the code you'll work on through the remainder of the guide. What does it do? Much of it is the code you worked on last time, so let's focus on the additions.

To communicate with Internet resources, the modem needs to establish a data connection through the cellular network, and then an HTTP connection to the target server. Lastly, it creates and sends an HTTP request to that server, and reads back the response.

The function open_data_conn() handles the first part, by sending the AT command CNACT=0,1. The 0 is the 'Packet Data Protocol (PDP) context', essentially one of a number of IP channels the modem provides. The 1 is the instruction to enable the data connection.

When the data connection is up, the code calls start_session() to open an HTTP connection to a specific server, which is passed in as an argument. The server is set using AT+SHCONF="URL","<SERVER_DOMAIN>" and the connection then opened with AT+SHCONN.

Requests are made through the function issue_request(). It calls start_session() and then sets up the request: we build the header on the modem (and keep it for future use) and then send AT+SHREQ= with the path to a resource and the value 1 as parameters — the 1 indicates it is a GET request.

The response returned by the modem contains information about the data returned by the server, which is stored on the modem. The code uses this information to get the HTTP status code — to check the request was successful — and the response's length. If the latter is non-zero, the code sends AT+SHREAD= to retrieve that many bytes from the modem's cache. issue_request() extracts any JSON in the response and returns it.

All this is triggered by the receipt of an SMS command, GET, which causes the function process_command_get() to be called. This function calls open_data_conn() and then issue_request(). It parses the received data as JSON and displays the value of a certain field on the LED display using code you worked on in the previous tutorial.

(information)

Info

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.


3. Set up a data source

3-set-up-a-data-source page anchor

Before you can run the code, you need to set up the data that will be retrieved. You're going to use Beeceptor(link takes you to an external page) as a proxy for the Internet resource your IoT device will be communicating with. In a real-world application, you would sign up to use a specific service and access that, but Beeceptor makes a very handy stand-in. Let's set it up to receive HTTP GET requests from the device.

  1. In a web browser tab, go to Beeceptor(link takes you to an external page) .
  2. Enter an endpoint name in the large text field and click Create Endpoint:

    Enter an endpoint name in the large text field.
  3. On the screen that appears next, click on the upper of the two clipboard icons to copy the endpoint URL:

    Copy the endpoint URL.
  4. Keep the tab open.
  5. Jump back to your text editor and locate the process_command_get() function in the Python code. Paste the endpoint URL you got from step 3, in place of YOUR_BEECEPTOR_URL .
  6. Save the file and then transfer it over to the Pico.
  7. Hop back to Beeceptor and click on Mocking Rules (0) in the page shown above and then click Create New Rule .
  8. In the third field, add api/v1/status right after the / that's already there.
  9. Under Response Body, paste the following JSON, your test API's sample output:


    _10
    { "userId":10,"status":1234,"title":"delectus aut autem","completed":false,"datapoints":[1,2,3,4,5,6,7,8,9,0] }

  10. The panel should look like this:

    The Beeceptor mocking rules panel.
  11. Click Save Rule and then close the Mocking Rules panel by clicking the X in the top right corner.
  12. Again, keep the tab open.

Switch over to Minicom (or PuTTY). When you see the Listening for commands... message, open a separate terminal tab or window and enter the following command:


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

You'll need to replace the sections in angle brackets (< and >) with your own information, just as you did last time. Your SIM's SID — or friendly name if you've set one — and the specified account credentials are all accessible from the Twilio Console(link takes you to an external page).

This command uses the Super SIM API's SMS Commands API to send a machine-to-machine message to the Pico. The --payload parameter tells Twilio what the body of the SMS should be: it's whatever comes after the equals sign. In this case, that's GET, the command to which we want the Pico to respond.

The listen() function in your Python code keeps an ear open for incoming SMS messages, which are signalled 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 GET — which tells the device to make an HTTP request of that type.

You'll see all this in your terminal window:

The GET request's progress in the terminal.

You should also see 1234 displayed on the LED — one part of the data received from the API you connected to!

(information)

Info

The code listed above will output state messages, but if you want to see the full flow of AT command requests and their responses, you'll need to add a handful of extra lines. To do so, drop in this function:


_10
'''
_10
Output raw data
_10
'''
_10
def debug_output(msg):
_10
for line in split_msg(msg): print(">>> ",line)

and add this line right before the final return in the function read_buffer():


_10
debug_output(buffer.decode())

The output will now look like this:

Super SIM Pico AT command flow.

5. Post data from the device

5-post-data-from-the-device page anchor

Reaching out across the Internet and requesting information is only half of the story: you also want to push data out to the cloud. Our Pico-based IoT demo is well prepared to be a source of information: it includes a temperature sensor which you can read and transmit the result by SMS if you send the command TMP by text message.

Not all data receivers accept input by SMS, however. Most will accept POST requests, though, so let's add the code to the application to support that. There are a number of changes and additions to make.

Add the following code right below the def set_request_header(): function definition:


_14
'''
_14
Set request body
_14
'''
_14
def set_request_body(body):
_14
send_at("AT+SHCPARA;+SHPARA=\"data\",\"" + body + "\"")
_14
_14
'''
_14
Make a GET, POST requests to the specified server
_14
'''
_14
def get_data(server, path):
_14
return issue_request(server, path, None, "GET")
_14
_14
def send_data(server, path, data):
_14
return issue_request(server, path, data, "POST")

Replace the existing issue_request() function with this code:


_48
def issue_request(server, path, body, verb):
_48
result = ""
_48
_48
# Check the request verb
_48
code = 0
_48
verbs = ["GET", "PUT", "POST", "PATCH", "HEAD"]
_48
if verb.upper() in verbs:
_48
code = verbs.index(verb) + 1
_48
else:
_48
print("ERROR -- Unknown request verb specified")
_48
return ""
_48
_48
# Attempt to open a data session
_48
if start_session(server):
_48
print("HTTP session open")
_48
# Issue the request...
_48
set_request_header()
_48
print("HTTP request verb code:",code)
_48
if body != None: set_request_body(body)
_48
response = send_at_get_resp("AT+SHREQ=\"" + path + "\"," + str(code))
_48
start = ticks_ms()
_48
while ((ticks_ms() - start) < 90000):
_48
if "+SHREQ:" in response: break
_48
response = read_buffer(1000)
_48
_48
# ...and process the response
_48
lines = split_msg(response)
_48
for line in lines:
_48
if len(line) == 0: continue
_48
if "+SHREQ:" in line:
_48
status_code = get_field_value(line, 1)
_48
if int(status_code) > 299:
_48
print("ERROR -- HTTP status code",status_code)
_48
break
_48
_48
# Get the data from the modem
_48
data_length = get_field_value(line, 2)
_48
if data_length == "0": break
_48
response = send_at_get_resp("AT+SHREAD=0," + data_length)
_48
_48
# The JSON data may be multi-line so store everything in the
_48
# response that comes after (and including) the first '{'
_48
pos = response.find("{")
_48
if pos != -1: result = response[pos:]
_48
end_session()
_48
else:
_48
print("ERROR -- Could not connect to server")
_48
return result

Replace the process_command_get() function with all of the following code:


_37
'''
_37
Make a request to a sample server
_37
'''
_37
def process_command_get():
_37
print("Requesting data...")
_37
server = "YOUR_BEECEPTOR_URL"
_37
endpoint_path = "/api/v1/status"
_37
process_request(server, endpoint_path)
_37
_37
def process_command_post():
_37
print("Sending data...")
_37
server = "YOUR_BEECEPTOR_URL"
_37
endpoint_path = "/api/v1/logs"
_37
process_request(server, endpoint_path, "{:.2f}".format(sensor.read_temp()))
_37
_37
def process_request(server, path, data=None):
_37
# Attempt to open a data connection
_37
if open_data_conn():
_37
if data is not None:
_37
result = send_data(server, path, data)
_37
else:
_37
result = get_data(server, path)
_37
_37
if len(result) > 0:
_37
# Decode the received JSON
_37
try:
_37
response = json.loads(result)
_37
# Extract an integer value and show it on the display
_37
if "status" in response:
_37
process_command_num("NUM=" + str(response["status"]))
_37
except:
_37
print("ERROR -- No JSON data received. Raw:\n",result)
_37
else:
_37
print("ERROR -- No JSON data received")
_37
_37
# Close the open connection
_37
close_data_conn()

When you've done that, copy and paste your Beeceptor endpoint URL into the places marked YOUR_BEECEPTOR_URL.

Finally, add the following lines to the listen() function, right below the code that looks for a GET command:


_10
elif cmd.startswith("POST"):
_10
process_command_post()

Finally, transfer the updated program to the Pico.


You can't send data without somewhere to post it, so set that up now. Once more, Beeceptor comes to our assistance: you're going to add a mocking rule as a stand-in for an application server that will take in the data the device posts and return a status message.

  1. Switch to the web browser tab showing Beeceptor(link takes you to an external page) .
  2. Click on Mocking Rules (1) and then Create New Rule .
  3. Under Method , select POST .
  4. In the third field, add api/v1/logs right after the / that's already there.
  5. Under Response Body, paste the following JSON:


    _10
    { "status":4567 }

  6. The panel should look like this:

    The Beeceptor POST mocking rules.
  7. Click Save Rule and then close the Mocking Rules panel by clicking the X in the top right corner.
  8. Again, keep the tab open.

6. Try out the code — part deux

6-try-out-the-code--part-deux page anchor

Switch over to Minicom (or PuTTY). When you see the Listening for commands... message, open a separate terminal tab or window and enter the following command, filling in your details where necessary:


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

This time, you'll see all this in your terminal window:

The POST request's progress in the terminal.

You should also see 4567 displayed on the LED — one part of the data received bacl from the API. Speaking of the API, what did it see? Take a look at Beeceptor. It records the receipt of a POST request to /api/v1/logs, and if you click on the entry in the table, then the JSON icon ({:}) above the Request body panel, you'll see the celsius temperature as received data:

The POSTed data.

You now have a Raspberry Pi Pico-based IoT device that can send and receive data across the Internet. In this demo, you've used test APIs for getting and posting data, and triggered both by manually sending an SMS command. Why not adapt the code not only to make use of different APIs — perhaps one you might make use of in your production IoT device — but also to do so automatically, at a time appropriate to the application? For example, you might include a regular weather forecast update, or pull in the output from a Slack channel. You might post device status data to your own cloud.

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


Note

note page anchor

When this was written, of course: the Pico W(link takes you to an external page) has been release since then. Back to top


Rate this page: