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.
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.
This guide requires a Twilio account. Sign up here now if you don't have one. It also requires a configured 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.
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.
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.
You can find the a complete listing of the code, including all subsequent additions, at our public GitHub repo.
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.
_608from machine import UART, Pin, I2C_608from utime import ticks_ms, sleep_608import json_608_608class 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_608class 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_608class 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'''_608Send an AT command - return True if we got an expected_608response ('back'), otherwise False_608'''_608def 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'''_608Send an AT command - just return the response_608'''_608def 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'''_608Read in the buffer by sampling the UART until timeout_608'''_608def 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'''_608Module startup detection_608Send a command to see if the modem is powered up_608'''_608def 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'''_608Power the module on/off_608'''_608def 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'''_608Check we are attached_608'''_608def 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'''_608Configure the modem_608'''_608def 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'''_608Open/close a data connection to the server_608'''_608def 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_608def close_data_conn():_608 # Just close the connection down_608 send_at("AT+CNACT=0,0")_608 print("Data connection inactive")_608_608'''_608Start/end an HTTP session_608'''_608def 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_608def end_session():_608 # Break the link to the server_608 send_at("AT+SHDISC")_608 print("HTTP session closed")_608_608'''_608Set a standard request header_608'''_608def 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'''_608Make a request to the specified server_608'''_608def 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'''_608Flash the Pico LED_608'''_608def 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_608def led_on():_608 led.value(1)_608_608def led_off():_608 led.value(0)_608_608'''_608Split a response from the modem into separate lines,_608removing empty lines and returning all that's left or,_608if 'want_line' has a non-default value, return that one line_608'''_608def 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'''_608Extract the SMS index from a modem response line_608'''_608def get_sms_number(line):_608 return get_field_value(line, 1)_608_608'''_608Extract a comma-separated field value from a line_608'''_608def 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'''_608Blink the LED n times after extracting n from the command string_608'''_608def 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'''_608Display the decimal value n after extracting n from the command string_608'''_608def 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'''_608Get a temperature reading and send it back as an SMS_608'''_608def 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'''_608Make a request to a sample server_608'''_608def 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'''_608Listen for incoming SMS Commands_608'''_608def 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_608req_head_set = False_608_608# Set up the modem UART_608modem = UART(0, 115200)_608_608# Set up I2C and the display_608i2c = I2C(1, scl=Pin(3), sda=Pin(2))_608display = HT16K33Segment(i2c)_608display.set_brightness(2)_608display.clear().draw()_608_608# Set up the MCP9808 sensor_608sensor = MCP9808(i2c=i2c)_608_608# Set the LED and turn it off_608led = Pin(25, Pin.OUT)_608led_off()_608_608# Start the modem_608if 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()_608else:_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.
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.
Before you can run the code, you need to set up the data that will be retrieved. You're going to use Beeceptor 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.
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:
process_command_get()
function in the Python code. Paste the endpoint URL you got from step 3, in place of
YOUR_BEECEPTOR_URL
.
api/v1/status
right after the
/
that's already there.
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] }
The panel should look like this:
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:
_10twilio 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.
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:
You should also see 1234 displayed on the LED — one part of the data received from the API you connected to!
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'''_10Output raw data_10'''_10def 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()
:
_10debug_output(buffer.decode())
The output will now look like this:
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'''_14Set request body_14'''_14def set_request_body(body):_14 send_at("AT+SHCPARA;+SHPARA=\"data\",\"" + body + "\"")_14_14'''_14Make a GET, POST requests to the specified server_14'''_14def get_data(server, path):_14 return issue_request(server, path, None, "GET")_14_14def send_data(server, path, data):_14 return issue_request(server, path, data, "POST")
Replace the existing issue_request()
function with this code:
_48def 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'''_37Make a request to a sample server_37'''_37def 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_37def 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_37def 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:
_10elif 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.
api/v1/logs
right after the
/
that's already there.
Under Response Body, paste the following JSON:
_10{ "status":4567 }
The panel should look like this:
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:
_10twilio 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:
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:
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!
When this was written, of course: the Pico W has been release since then. Back to top