Skip to contentSkip to navigationSkip to topbar
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.

1
from machine import UART, Pin, I2C
2
from utime import ticks_ms, sleep
3
import json
4
5
class MCP9808:
6
"""
7
A simple driver for the I2C-connected MCP9808 temperature sensor.
8
This release supports MicroPython.
9
"""
10
11
# *********** PRIVATE PROPERTIES **********
12
13
i2c = None
14
address = 0x18
15
16
# *********** CONSTRUCTOR **********
17
18
def __init__(self, i2c, i2c_address=0x18):
19
assert 0x00 <= i2c_address < 0x80, "ERROR - Invalid I2C address in MCP9808()"
20
self.i2c = i2c
21
self.address = i2c_address
22
23
# *********** PUBLIC METHODS **********
24
25
def read_temp(self):
26
# Read sensor and return its value in degrees celsius.
27
temp_bytes = self.i2c.readfrom_mem(self.address, 0x05, 2)
28
# Scale and convert to signed value.
29
temp_raw = (temp_bytes[0] << 8) | temp_bytes[1]
30
temp_cel = (temp_raw & 0x0FFF) / 16.0
31
if temp_raw & 0x1000: temp_cel -= 256.0
32
return temp_cel
33
34
class HT16K33:
35
"""
36
A simple, generic driver for the I2C-connected Holtek HT16K33 controller chip.
37
This release supports MicroPython and CircuitPython
38
39
Version: 3.0.2
40
Bus: I2C
41
Author: Tony Smith (@smittytone)
42
License: MIT
43
Copyright: 2020
44
"""
45
46
# *********** CONSTANTS **********
47
48
HT16K33_GENERIC_DISPLAY_ON = 0x81
49
HT16K33_GENERIC_DISPLAY_OFF = 0x80
50
HT16K33_GENERIC_SYSTEM_ON = 0x21
51
HT16K33_GENERIC_SYSTEM_OFF = 0x20
52
HT16K33_GENERIC_DISPLAY_ADDRESS = 0x00
53
HT16K33_GENERIC_CMD_BRIGHTNESS = 0xE0
54
HT16K33_GENERIC_CMD_BLINK = 0x81
55
56
# *********** PRIVATE PROPERTIES **********
57
58
i2c = None
59
address = 0
60
brightness = 15
61
flash_rate = 0
62
63
# *********** CONSTRUCTOR **********
64
65
def __init__(self, i2c, i2c_address):
66
assert 0x00 <= i2c_address < 0x80, "ERROR - Invalid I2C address in HT16K33()"
67
self.i2c = i2c
68
self.address = i2c_address
69
self.power_on()
70
71
# *********** PUBLIC METHODS **********
72
73
def set_blink_rate(self, rate=0):
74
"""
75
Set the display's flash rate.
76
"""
77
assert rate in (0, 0.5, 1, 2), "ERROR - Invalid blink rate set in set_blink_rate()"
78
self.blink_rate = rate & 0x03
79
self._write_cmd(self.HT16K33_GENERIC_CMD_BLINK | rate << 1)
80
81
def set_brightness(self, brightness=15):
82
"""
83
Set the display's brightness (ie. duty cycle).
84
"""
85
if brightness < 0 or brightness > 15: brightness = 15
86
self.brightness = brightness
87
self._write_cmd(self.HT16K33_GENERIC_CMD_BRIGHTNESS | brightness)
88
89
def draw(self):
90
"""
91
Writes the current display buffer to the display itself.
92
"""
93
self._render()
94
95
def update(self):
96
"""
97
Alternative for draw() for backwards compatibility
98
"""
99
self._render()
100
101
def clear(self):
102
"""
103
Clear the buffer.
104
"""
105
for i in range(0, len(self.buffer)): self.buffer[i] = 0x00
106
return self
107
108
def power_on(self):
109
"""
110
Power on the controller and display.
111
"""
112
self._write_cmd(self.HT16K33_GENERIC_SYSTEM_ON)
113
self._write_cmd(self.HT16K33_GENERIC_DISPLAY_ON)
114
115
def power_off(self):
116
"""
117
Power on the controller and display.
118
"""
119
self._write_cmd(self.HT16K33_GENERIC_DISPLAY_OFF)
120
self._write_cmd(self.HT16K33_GENERIC_SYSTEM_OFF)
121
122
# ********** PRIVATE METHODS **********
123
124
def _render(self):
125
"""
126
Write the display buffer out to I2C
127
"""
128
buffer = bytearray(len(self.buffer) + 1)
129
buffer[1:] = self.buffer
130
buffer[0] = 0x00
131
self.i2c.writeto(self.address, bytes(buffer))
132
133
def _write_cmd(self, byte):
134
"""
135
Writes a single command to the HT16K33. A private method.
136
"""
137
self.i2c.writeto(self.address, bytes([byte]))
138
139
class HT16K33Segment(HT16K33):
140
"""
141
Micro/Circuit Python class for the Adafruit 0.56-in 4-digit,
142
7-segment LED matrix backpack and equivalent Featherwing.
143
144
Version: 3.0.2
145
Bus: I2C
146
Author: Tony Smith (@smittytone)
147
License: MIT
148
Copyright: 2020
149
"""
150
151
# *********** CONSTANTS **********
152
153
HT16K33_SEGMENT_COLON_ROW = 0x04
154
HT16K33_SEGMENT_MINUS_CHAR = 0x10
155
HT16K33_SEGMENT_DEGREE_CHAR = 0x11
156
HT16K33_SEGMENT_SPACE_CHAR = 0x00
157
158
# The positions of the segments within the buffer
159
POS = (0, 2, 6, 8)
160
161
# Bytearray of the key alphanumeric characters we can show:
162
# 0-9, A-F, minus, degree
163
CHARSET = b'\x3F\x06\x5B\x4F\x66\x6D\x7D\x07\x7F\x6F\x5F\x7C\x58\x5E\x7B\x71\x40\x63'
164
165
# *********** CONSTRUCTOR **********
166
167
def __init__(self, i2c, i2c_address=0x70):
168
self.buffer = bytearray(16)
169
super(HT16K33Segment, self).__init__(i2c, i2c_address)
170
171
# *********** PUBLIC METHODS **********
172
173
def set_colon(self, is_set=True):
174
"""
175
Set or unset the display's central colon symbol.
176
"""
177
self.buffer[self.HT16K33_SEGMENT_COLON_ROW] = 0x02 if is_set is True else 0x00
178
return self
179
180
def set_glyph(self, glyph, digit=0, has_dot=False):
181
"""
182
Present a user-defined character glyph at the specified digit.
183
"""
184
assert 0 <= digit < 4, "ERROR - Invalid digit (0-3) set in set_glyph()"
185
assert 0 <= glyph < 0xFF, "ERROR - Invalid glyph (0x00-0xFF) set in set_glyph()"
186
self.buffer[self.POS[digit]] = glyph
187
if has_dot is True: self.buffer[self.POS[digit]] |= 0x80
188
return self
189
190
def set_number(self, number, digit=0, has_dot=False):
191
"""
192
Present single decimal value (0-9) at the specified digit.
193
"""
194
assert 0 <= digit < 4, "ERROR - Invalid digit (0-3) set in set_number()"
195
assert 0 <= number < 10, "ERROR - Invalid value (0-9) set in set_number()"
196
return self.set_character(str(number), digit, has_dot)
197
198
def set_character(self, char, digit=0, has_dot=False):
199
"""
200
Present single alphanumeric character at the specified digit.
201
"""
202
assert 0 <= digit < 4, "ERROR - Invalid digit set in set_character()"
203
char = char.lower()
204
char_val = 0xFF
205
if char == "deg":
206
char_val = HT16K33_SEGMENT_DEGREE_CHAR
207
elif char == '-':
208
char_val = self.HT16K33_SEGMENT_MINUS_CHAR
209
elif char == ' ':
210
char_val = self.HT16K33_SEGMENT_SPACE_CHAR
211
elif char in 'abcdef':
212
char_val = ord(char) - 87
213
elif char in '0123456789':
214
char_val = ord(char) - 48
215
assert char_val != 0xFF, "ERROR - Invalid char string set in set_character()"
216
self.buffer[self.POS[digit]] = self.CHARSET[char_val]
217
if has_dot is True: self.buffer[self.POS[digit]] |= 0x80
218
return self
219
220
'''
221
Send an AT command - return True if we got an expected
222
response ('back'), otherwise False
223
'''
224
def send_at(cmd, back="OK", timeout=1000):
225
# Send the command and get the response (until timeout)
226
buffer = send_at_get_resp(cmd, timeout)
227
if len(buffer) > 0: return (back in buffer)
228
return False
229
230
'''
231
Send an AT command - just return the response
232
'''
233
def send_at_get_resp(cmd, timeout=1000):
234
# Send the AT command
235
modem.write((cmd + "\r\n").encode())
236
237
# Read and return the response (until timeout)
238
return read_buffer(timeout)
239
240
'''
241
Read in the buffer by sampling the UART until timeout
242
'''
243
def read_buffer(timeout):
244
buffer = bytes()
245
now = ticks_ms()
246
while (ticks_ms() - now) < timeout and len(buffer) < 1025:
247
if modem.any():
248
buffer += modem.read(1)
249
return buffer.decode()
250
251
'''
252
Module startup detection
253
Send a command to see if the modem is powered up
254
'''
255
def boot_modem():
256
state = False
257
count = 0
258
while count < 20:
259
if send_at("ATE1"):
260
print("The modem is ready")
261
return True
262
if not state:
263
print("Powering the modem")
264
module_power()
265
state = True
266
sleep(4)
267
count += 1
268
return False
269
270
'''
271
Power the module on/off
272
'''
273
def module_power():
274
pwr_key = Pin(14, Pin.OUT)
275
pwr_key.value(1)
276
sleep(1.5)
277
pwr_key.value(0)
278
279
'''
280
Check we are attached
281
'''
282
def check_network():
283
is_connected = False
284
response = send_at_get_resp("AT+COPS?")
285
line = split_msg(response, 1)
286
if "+COPS:" in line:
287
is_connected = (line.find(",") != -1)
288
if is_connected: print("Network information:", line)
289
return is_connected
290
291
'''
292
Configure the modem
293
'''
294
def configure_modem():
295
# NOTE AT commands can be sent together, not one at a time.
296
# Set the error reporting level, set SMS text mode, delete left-over SMS
297
# select LTE-only mode, select Cat-M only mode, set the APN to 'super' for Super SIM
298
send_at("AT+CMEE=2;+CMGF=1;+CMGD=,4;+CNMP=38;+CMNB=1;+CGDCONT=1,\"IP\",\"super\"")
299
# Set SSL version, SSL no verify, set HTTPS request parameters
300
send_at("AT+CSSLCFG=\"sslversion\",1,3;+SHSSL=1,\"\";+SHCONF=\"BODYLEN\",1024;+SHCONF=\"HEADERLEN\",350")
301
print("Modem configured for Cat-M and Super SIM")
302
303
'''
304
Open/close a data connection to the server
305
'''
306
def open_data_conn():
307
# Activate a data connection using PDP 0,
308
# but first check it's not already open
309
response = send_at_get_resp("AT+CNACT?")
310
line = split_msg(response, 1)
311
status = get_field_value(line, 1)
312
313
if status == "0":
314
# Inactive data connection so start one up
315
success = send_at("AT+CNACT=0,1", "ACTIVE", 2000)
316
elif status in ("1", "2"):
317
# Active or operating data connection
318
success = True
319
320
print("Data connection", "active" if success else "inactive")
321
return success
322
323
def close_data_conn():
324
# Just close the connection down
325
send_at("AT+CNACT=0,0")
326
print("Data connection inactive")
327
328
'''
329
Start/end an HTTP session
330
'''
331
def start_session(server):
332
# Deal with an existing session if there is one
333
if send_at("AT+SHSTATE?", "1"):
334
print("Closing existing HTTP session")
335
send_at("AT+SHDISC")
336
337
# Configure a session with the server...
338
send_at("AT+SHCONF=\"URL\",\"" + server + "\"")
339
340
# ...and open it
341
resp = send_at_get_resp("AT+SHCONN", 2000)
342
# The above command may take a while to return, so
343
# continue to check the UART until we have a response,
344
# or 90s passes (timeout)
345
now = ticks_ms()
346
while ((ticks_ms() - now) < 90000):
347
#if len(resp) > 0: print(resp)
348
if "OK" in resp: return True
349
if "ERROR" in resp: return False
350
resp = read_buffer(1000)
351
return False
352
353
def end_session():
354
# Break the link to the server
355
send_at("AT+SHDISC")
356
print("HTTP session closed")
357
358
'''
359
Set a standard request header
360
'''
361
def set_request_header():
362
global req_head_set
363
364
# Check state variable to see if we need to
365
# set the standard request header
366
if not req_head_set:
367
send_at("AT+SHCHEAD")
368
send_at("AT+SHAHEAD=\"Content-Type\",\"application/x-www-form-urlencoded\";+SHAHEAD=\"User-Agent\",\"twilio-pi-pico/1.0.0\"")
369
send_at("AT+SHAHEAD=\"Cache-control\",\"no-cache\";+SHAHEAD=\"Connection\",\"keep-alive\";+SHAHEAD=\"Accept\",\"*/*\"")
370
req_head_set = True
371
372
'''
373
Make a request to the specified server
374
'''
375
def issue_request(server, path, body, verb):
376
result = ""
377
378
# Check the request verb
379
code = 0
380
verbs = ["GET", "PUT", "POST", "PATCH", "HEAD"]
381
if verb.upper() in verbs:
382
code = verbs.index(verb) + 1
383
else:
384
print("ERROR -- Unknown request verb specified")
385
return ""
386
387
# Attempt to open a data session
388
if start_session(server):
389
print("HTTP session open")
390
# Issue the request...
391
set_request_header()
392
print("HTTP request verb code:",code)
393
if body != None: set_request_body(body)
394
response = send_at_get_resp("AT+SHREQ=\"" + path + "\"," + str(code))
395
start = ticks_ms()
396
while ((ticks_ms() - start) < 90000):
397
if "+SHREQ:" in response: break
398
response = read_buffer(1000)
399
400
# ...and process the response
401
lines = split_msg(response)
402
for line in lines:
403
if len(line) == 0: continue
404
if "+SHREQ:" in line:
405
status_code = get_field_value(line, 1)
406
if int(status_code) > 299:
407
print("ERROR -- HTTP status code",status_code)
408
break
409
410
# Get the data from the modem
411
data_length = get_field_value(line, 2)
412
if data_length == "0": break
413
response = send_at_get_resp("AT+SHREAD=0," + data_length)
414
415
# The JSON data may be multi-line so store everything in the
416
# response that comes after (and including) the first '{'
417
pos = response.find("{")
418
if pos != -1: result = response[pos:]
419
end_session()
420
else:
421
print("ERROR -- Could not connect to server")
422
return result
423
424
'''
425
Flash the Pico LED
426
'''
427
def led_blink(blinks):
428
for i in range(0, blinks):
429
led_off()
430
sleep(0.25)
431
led_on()
432
sleep(0.25)
433
434
def led_on():
435
led.value(1)
436
437
def led_off():
438
led.value(0)
439
440
'''
441
Split a response from the modem into separate lines,
442
removing empty lines and returning all that's left or,
443
if 'want_line' has a non-default value, return that one line
444
'''
445
def split_msg(msg, want_line=99):
446
lines = msg.split("\r\n")
447
results = []
448
for i in range(0, len(lines)):
449
if i == want_line:
450
return lines[i]
451
if len(lines[i]) > 0:
452
results.append(lines[i])
453
return results
454
455
'''
456
Extract the SMS index from a modem response line
457
'''
458
def get_sms_number(line):
459
return get_field_value(line, 1)
460
461
'''
462
Extract a comma-separated field value from a line
463
'''
464
def get_field_value(line, field_num):
465
parts = line.split(",")
466
if len(parts) > field_num:
467
return parts[field_num]
468
return ""
469
470
'''
471
Blink the LED n times after extracting n from the command string
472
'''
473
def process_command_led(msg):
474
blinks = msg[4:]
475
print("Blinking LED",blinks,"time(s)")
476
try:
477
led_blink(int(blinks))
478
except:
479
print("BAD COMMAND:",blinks)
480
481
'''
482
Display the decimal value n after extracting n from the command string
483
'''
484
def process_command_num(msg):
485
value = msg[4:]
486
print("Setting",value,"on the LED")
487
try:
488
# Extract the decimal value (string) from 'msg' and convert
489
# to a hex integer for easy presentation of decimal digits
490
hex_value = int(value, 16)
491
display.set_number((hex_value & 0xF000) >> 12, 0)
492
display.set_number((hex_value & 0x0F00) >> 8, 1)
493
display.set_number((hex_value & 0x00F0) >> 4, 2)
494
display.set_number((hex_value & 0x000F), 3).update()
495
except:
496
print("BAD COMMAND:",value)
497
498
'''
499
Get a temperature reading and send it back as an SMS
500
'''
501
def process_command_tmp():
502
print("Sending a temperature reading")
503
celsius_temp = "{:.2f}".format(sensor.read_temp())
504
if send_at("AT+CMGS=\"000\"", ">"):
505
# '>' is the prompt sent by the modem to signal that
506
# it's waiting to receive the message text.
507
# 'chr(26)' is the code for ctrl-z, which the modem
508
# uses as an end-of-message marker
509
r = send_at_get_resp(celsius_temp + chr(26))
510
511
'''
512
Make a request to a sample server
513
'''
514
def process_command_get():
515
print("Requesting data...")
516
server = "YOUR_BEECEPTOR_URL"
517
endpoint_path = "/api/v1/status"
518
519
# Attempt to open a data connection
520
if open_data_conn():
521
result = issue_request(server, endpoint_path, None, "GET")
522
if len(result) > 0:
523
# Decode the received JSON
524
try:
525
response = json.loads(result)
526
# Extract an integer value and show it on the display
527
if "status" in response:
528
process_command_num("NUM=" + str(response["status"]))
529
except:
530
print("ERROR -- No JSON data received. Raw:\n",result)
531
else:
532
print("ERROR -- No JSON data received")
533
534
# Close the open connection
535
close_data_conn()
536
537
'''
538
Listen for incoming SMS Commands
539
'''
540
def listen():
541
print("Listening for Commands...")
542
while True:
543
# Did we receive a Unsolicited Response Code (URC)?
544
buffer = read_buffer(5000)
545
if len(buffer) > 0:
546
lines = split_msg(buffer)
547
for line in lines:
548
if "+CMTI:" in line:
549
# We received an SMS, so get it...
550
num = get_sms_number(line)
551
msg = send_at_get_resp("AT+CMGR=" + num, 2000)
552
553
# ...and process it for commands
554
cmd = split_msg(msg, 2).upper()
555
if cmd.startswith("LED="):
556
process_command_led(cmd)
557
elif cmd.startswith("NUM="):
558
process_command_num(cmd)
559
elif cmd.startswith("TMP"):
560
process_command_tmp()
561
elif cmd.startswith("GET"):
562
process_command_get()
563
else:
564
print("UNKNOWN COMMAND:",cmd)
565
# Delete all SMS now we're done with them
566
send_at("AT+CMGD=,4")
567
568
# Globals
569
req_head_set = False
570
571
# Set up the modem UART
572
modem = UART(0, 115200)
573
574
# Set up I2C and the display
575
i2c = I2C(1, scl=Pin(3), sda=Pin(2))
576
display = HT16K33Segment(i2c)
577
display.set_brightness(2)
578
display.clear().draw()
579
580
# Set up the MCP9808 sensor
581
sensor = MCP9808(i2c=i2c)
582
583
# Set the LED and turn it off
584
led = Pin(25, Pin.OUT)
585
led_off()
586
587
# Start the modem
588
if boot_modem():
589
configure_modem()
590
591
# Check we're attached
592
state = True
593
while not check_network():
594
if state:
595
led_on()
596
else:
597
led_off()
598
state = not state
599
600
# Light the LED
601
led_on()
602
603
# Begin listening for commands
604
listen()
605
else:
606
# Error! Blink LED 5 times
607
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:

    { "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:

1
twilio api:supersim:v1:sms-commands:create \
2
--sim "<YOUR_SIM_NAME_OR_SID>" \
3
--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:

1
'''
2
Output raw data
3
'''
4
def debug_output(msg):
5
for line in split_msg(msg): print(">>> ",line)

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

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:

1
'''
2
Set request body
3
'''
4
def set_request_body(body):
5
send_at("AT+SHCPARA;+SHPARA=\"data\",\"" + body + "\"")
6
7
'''
8
Make a GET, POST requests to the specified server
9
'''
10
def get_data(server, path):
11
return issue_request(server, path, None, "GET")
12
13
def send_data(server, path, data):
14
return issue_request(server, path, data, "POST")

Replace the existing issue_request() function with this code:

1
def issue_request(server, path, body, verb):
2
result = ""
3
4
# Check the request verb
5
code = 0
6
verbs = ["GET", "PUT", "POST", "PATCH", "HEAD"]
7
if verb.upper() in verbs:
8
code = verbs.index(verb) + 1
9
else:
10
print("ERROR -- Unknown request verb specified")
11
return ""
12
13
# Attempt to open a data session
14
if start_session(server):
15
print("HTTP session open")
16
# Issue the request...
17
set_request_header()
18
print("HTTP request verb code:",code)
19
if body != None: set_request_body(body)
20
response = send_at_get_resp("AT+SHREQ=\"" + path + "\"," + str(code))
21
start = ticks_ms()
22
while ((ticks_ms() - start) < 90000):
23
if "+SHREQ:" in response: break
24
response = read_buffer(1000)
25
26
# ...and process the response
27
lines = split_msg(response)
28
for line in lines:
29
if len(line) == 0: continue
30
if "+SHREQ:" in line:
31
status_code = get_field_value(line, 1)
32
if int(status_code) > 299:
33
print("ERROR -- HTTP status code",status_code)
34
break
35
36
# Get the data from the modem
37
data_length = get_field_value(line, 2)
38
if data_length == "0": break
39
response = send_at_get_resp("AT+SHREAD=0," + data_length)
40
41
# The JSON data may be multi-line so store everything in the
42
# response that comes after (and including) the first '{'
43
pos = response.find("{")
44
if pos != -1: result = response[pos:]
45
end_session()
46
else:
47
print("ERROR -- Could not connect to server")
48
return result

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

1
'''
2
Make a request to a sample server
3
'''
4
def process_command_get():
5
print("Requesting data...")
6
server = "YOUR_BEECEPTOR_URL"
7
endpoint_path = "/api/v1/status"
8
process_request(server, endpoint_path)
9
10
def process_command_post():
11
print("Sending data...")
12
server = "YOUR_BEECEPTOR_URL"
13
endpoint_path = "/api/v1/logs"
14
process_request(server, endpoint_path, "{:.2f}".format(sensor.read_temp()))
15
16
def process_request(server, path, data=None):
17
# Attempt to open a data connection
18
if open_data_conn():
19
if data is not None:
20
result = send_data(server, path, data)
21
else:
22
result = get_data(server, path)
23
24
if len(result) > 0:
25
# Decode the received JSON
26
try:
27
response = json.loads(result)
28
# Extract an integer value and show it on the display
29
if "status" in response:
30
process_command_num("NUM=" + str(response["status"]))
31
except:
32
print("ERROR -- No JSON data received. Raw:\n",result)
33
else:
34
print("ERROR -- No JSON data received")
35
36
# 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:

1
elif cmd.startswith("POST"):
2
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:

    { "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:

1
twilio api:supersim:v1:sms-commands:create \
2
--sim "<YOUR_SIM_NAME_OR_SID>" \
3
--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

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.