© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
C. BellBeginning MicroPython with the Raspberry Pi Picohttps://doi.org/10.1007/978-1-4842-8135-2_13

13. Introducing IoT for the Cloud

Charles Bell1  
(1)
Warsaw, VA, USA
 

Thus far in the book, we have learned that the Raspberry Pi Pico is a great small microcontroller that has a lot of power in such a small package. It’s inexpensive and easy to program. However, there is one thing missing – there is no way to connect it to the Internet. At least, not directly because it doesn’t have WiFi support. We will see how to add WiFi support in this chapter.

In previous chapters, you’ve seen a number of projects, ranging from very basic to advanced in difficulty; it is time to discuss how to make your IoT data viewable by others via the cloud. More specifically, you will get a small glimpse at what is possible with the more popular cloud computing services and solutions.

I say a glimpse because it is not possible to cover all viable solutions available in cloud services solutions for IoT in a single chapter. Once again, this is a case where learning a little bit about something and seeing it in practice will help you get started.

In this chapter, we will get an overview of what the cloud is and how it is used for IoT solutions. The chapter also presents a concise overview of the popular cloud systems for IoT as well as a short example using two of our earlier projects to give you a sense of what is possible and how projects can be modified to use the Internet.

The Cloud: Isn’t That Just Marketing Hype?

Don’t believe all the hype or sales talk about any product that includes “cloud” in its name. Cloud computing services and resources should be accessible via the Internet from anywhere, available to you via subscription (fee or for free), and permit you to consume or produce and share the data involved. Also, consider the fact that you must have access to the cloud to get to your data. Thus, you have no alternative if the service is unreachable (or down).

Since the technologies presented are quite unique in implementation (but straightforward in concept), I keep the project hardware and programming to a minimal effort.

Overview

Unless you live in a very isolated location, you have been bombarded with talk about the cloud and IoT. You’ve seen advertisements in magazines and on television, or read about it in other books, or attended a seminar or conference. Unless you’ve spent time learning what cloud means, you are wondering what all the fuss is about.

What Is the Cloud?

Simply stated,1 the cloud is a name tagged to services available via the Internet. These can be servers you can access (running as a virtual machine on a larger server), systems that provide access to a specific software or environment, or resources such as disks or IP addresses that you can attach to other resources. The technologies behind the cloud include grid computing (distributed processing), virtualization, and networking. The correct scientific term is cloud computing. Although a deep dive into cloud computing is beyond the scope of this book, it is enough to understand that you can use cloud computing services to store your sensor data.

What Is Cloud Computing Then?

The term cloud computing is sadly overused and has become a marketing term for some. True cloud computing solutions are services that are provided to subscribers (customers) via a combination of virtualization, grid computing (distributed processing and storage), and facilities to support virtualized hardware and software, such as IP addresses that are tied to the subscription rather than a physical device. Thus, you can use and discard resources on the fly to meet your needs.

These resources, services, and features are priced by usage patterns (called subscription plans or tiers), in which you can pay for as little or as much as you need. For example, if you need more processing power, you can move up to a subscription level that offers more CPU cores, more memory, and so forth. Thus, you only pay for what you need, which means that organizations can potentially save a great deal on infrastructure.

A classic example of this benefit is a case where an organization experiences a brief and intense level of work that requires additional resources to keep their products and services viable. Using the cloud, organizations can temporarily increase their infrastructure capability and, once the peak has passed, scale things back to normal. This is a lot better than having to rent or purchase a ton of hardware for that one event.

Sadly, there are some vendors that offer cloud solutions (typically worded as cloud enabled or simply cloud) that fall far short of being a complete solution. In most cases, they are nothing more than yesterday’s Internet-based storage and visualization. Fortunately, Microsoft Azure is authentic: a full cloud computing solution with an impressive array of features to support almost any cloud solution you can dream up.

Tip

If you would like to know more about cloud computing and its many facets, see https://en.wikipedia.org/wiki/Cloud_computing.

How Does the Cloud Help IoT?

OK, so now that we know what cloud systems are, how do they help me with my IoT projects? There are a variety of ways, but most common are mechanisms for storing and presenting your data rather than storing it locally or even remotely on another system such as a dedicated database server. That is, you can send the data you collect from your sensors to the cloud for storage and even use additional cloud services to view the data using charts, graphs, or just plain text. The sky is the limit with respect to how you can present your data.

But storing data isn’t the only feature you can leverage in the cloud. There are other services that you can use to link to yet other services to form a solution. For example, most paid IoT cloud systems provide features that can “talk” to each other allowing you to link them together to quickly build a solution. The features are often called components rather than services, but both terms apply.

For example, in Microsoft Azure, you can store your data with one of several components and then link it to others that allow you to modify the data via queries, others to route the data to other places (even to another cloud service vendor), and to one of several components for displaying the data. Yes, it really is a set of building blocks like that.

Now that we’ve had a general overview of cloud systems, let’s look at those that support IoT projects directly.

IoT Cloud Systems

There are a number of IoT cloud vendors that offer all manner of products, capacities, and features to match about anything you can conjure for an IoT project. With so many vendors offering IoT solutions, it can be difficult to choose one. The following is a concise list of the more popular IoT offerings from the top vendors in the cloud industry:

Most of the vendors offer commercial products, but a few like Google, Azure, Arduino, IFTTT, and ThingSpeak offer limited free accounts. A few are free like Adafruit IO and Arduino IoT Cloud but may limit you to a particular platform or a smaller set of features. As you may surmise, some of the offerings are complex solutions with steep learning curve, but the IFTTT and ThingSpeak offerings are simple and easy to use. Since we want a solution that is easy to use (and free!), we will then use ThingSpeak in the next chapter to round out our introduction to IoT cloud systems.

Tip

If you want or need to use one of the other vendors, be sure to read all of the tutorials thoroughly before jumping into your code.

Let’s look at some of the types of services available in cloud systems that support IoT projects.

IoT Cloud Services Available

IoT projects offer an amazing opportunity to expand our knowledge of the world around us and to observe events from all over the world no matter where we are located. To address these capabilities, IoT cloud services provide an array of services that you can leverage in your applications.

There are services for collecting data, managing your devices, performing analytics, and even application and processing extensions for you to exploit. For example, some vendors include complete user management where you can provide user accounts for people to log in and use your cloud solution and see your data.

The following lists a number of the types of services available. Some vendors may not offer all of the services, and a service common among the vendors may work very differently from one vendor to another. However, this should give you an idea of what services are available and a general idea of the feature set:
  • Device management: Allows you to set up, manage, and track what devices are in your IoT network.

  • Data storage: Permits storage of your IoT data either on a temporary (typically free for a number of days) or permanent (paid) storage.

  • Data analytics: Allows you to perform analysis on your data to find trends, outliers, or any form of analytical query.

  • Data query and filters: You can perform queries or filter your IoT data after it has been sent to the cloud service for detailed presentations or transformations.

  • Big data: Permits you to store vast amounts of data and perform operations on the data (think data warehousing).

  • Visualization tools: Various dashboards and graphics you can use to help present your data in meaningful ways (spreadsheet, pie charts, etc.).

  • High availability: Provides features that allow you to operate even if portions of your cloud servers (or the vendor’s) fail or go offline due to network issues.

  • Third-party integration: Allows you to connect your IoT services to other IoT servers from other vendors. For example, connecting your Adafruit IO data to IFTTT for triggering an SMS message.

  • Security (data, user): Provides support for managing user accounts, security access, and more for your applications.

  • Encryption: Allows you to encrypt your data either in the cloud or when transmitting the data from one service to another.

  • Deployment: Similar to device management, but on a grander scale where you build IoT devices using common profiles, operating systems, configurations, etc.

  • Scalability: The ability to scale from a small number of devices and services to many devices. This is often only available in the larger, paid vendor services.

  • APIs (Rest, programming): Allow you to write code to communicate directly with the services instead of issuing web requests. Often part of the larger, paid vendor services.

For our beginning IoT projects, we will be focusing on a subset of these services, which can be grouped into several categories. Let’s look at a few of the most common services you may want to start using right away.

Data Storage

These services allow you to store your data in the cloud rather than on your local device. Some data such as alerts or notices do not need to be stored, and you should think about if you would need the data in the future and will be project dependent. For example, if you wanted to create a weather alert project, you may not care what the temperature was a week or even a month ago. However, if you want to do some amateur weather forecasting, you will want to store data for some time (perhaps years). You may consider storing the data locally, which may be possible for some platforms such as the Raspberry Pi, but the Arduino and similar boards have very limited storage capabilities.

Thus, if you need to store your data for some period and storing it locally is not an option, you should consider this when selecting a cloud vendor. Look for how data will be stored, the mechanisms needed to send the data to the service, and how to get the data out of the service.

Data Transformation (Queries)

These services allow you to perform queries on the data as it flows to or through the cloud services. You may want to show only a subset of the data to your users, or you may want to filter the data so that data from certain devices, dates, etc. are shown for one of several views.

The case where you’d want to consider these are for IoT projects that collect data from multiple sensors and multiple devices, and the data is stored for a period of time. For example, if you have devices geographically distributed over a wide area, you may only want to see data from a subset of those devices. Similarly, if you have data from several time periods, hours, days, and weeks, you may only want to see data from a specific time.

Visualization Tools

These services along with routing and messaging are the most commonly used for beginning IoT projects. These are simply services that allow you to see your data on the Internet. It may be nothing more than a simple list of the data, or it may be an elaborate data dashboard complete with controls that users can use to manipulate the display. Fortunately, most cloud vendors provide a robust set of tools (some more than one) that you can use to present your data to yourself or your users.

Routing and Messaging

These services are the heart or the bones of the IoT cloud. They encompass the glue to bind different services together. More specifically, they provide mechanisms for you to connect your devices to services and those services to other services such as queries, filters, and visualization tools, permitting you to build an IoT solution using several cloud services. We’ll see an example of such a service in the next section.

Now that we’ve had an overview of the IoT cloud services and the most common services we will encounter, let’s jump into a simple example using a web page to control hardware.

But first, let’s talk about basic networking capabilities for the Pico.

Connecting Your Pico to the Internet

At the time when the Pico was launched, there were no options available for you to connect your Pico to the Internet except through connecting your Pico to another microcontroller (or Raspberry Pi) to provide the connectivity. Those options are still available (and valid), but they are not simple to set up and program. What we want is a module that we can connect to our Raspberry Pi to connect it to the Internet and provide basic TCP/IP operability. Fortunately, such a module exists! Before we look at that module, let’s review what modules are available for us today.

Pico WiFi Modules

There are currently few modules available for connecting our Pico to the Internet. These include the following. While this is a concise list, there are sure to be more options available in the future:

Most are made to work as an external module that you have to program separately and then use either a serial or similar code library to transfer data back and forth. This is the same concept as connecting your Pico to another microcontroller, which isn’t sufficient for getting started quickly or making it easy.

Let’s discover a bit more about each of these options.

ESP8266 WiFi Module for Raspberry Pi Pico

This module is a WiFi add-on that allows you to connect your Pico to the Internet via an intermediate module. Specifically, the ESP8266 WiFi Module has an ESP-12 chip onboard that is designed to process commands via a serial connection. Figure 13-1 shows the module. Notice the ESP-12 module on the right. There are also two buttons used for programming the ESP-12 module (reset and boot). On the bottom of the board is a set of female headers that permit you to attach the module directly to your Pico or use it with the Pico Omnibus host board that we’ve seen in previous chapters (https://shop.pimoroni.com/products/pico-omnibus).
Figure 13-1

ESP8266 WiFi Module (courtesy of thepihut.com)

The module is controlled via UART AT commands supporting the TCP/UDP communication protocol. The following shows an excerpt from one of the sample code files found on the product website:
sendCMD("AT","OK")
sendCMD("AT+CWMODE=3","OK")
sendCMD("AT+CWJAP=""+SSID+"",""+password+""","OK",20000)
sendCMD("AT+CIFSR","OK")
sendCMD("AT+CIPSTART="TCP",""+ServerIP+"","+Port,"OK",10000)
sendCMD("AT+CIPMODE=1","OK")
sendCMD("AT+CIPSEND",">"

As you can see, we are not using any special libraries; rather, we’re sending AT commands over a UART (think serial) connection to the module. While this helps eliminate GPIO issues (pins used by modules that you need for other modules), it is a bit tedious to program TCP/IP connections this way. Fortunately, there are several working examples on the product website.2

See www.waveshare.com/wiki/Pico-ESP8266 for documentation and more information about using this module.

DiP-Pi WiFi Master for Raspberry Pi Pico

This module is similar to the last module in that it also has a female header on the bottom to allow you to connect it to your Pico, and it supports a similar WT8266 WiFi chip that you access via AT commands. Figure 13-2 shows the DiP-Pi WiFi Master.
Figure 13-2

DiP-Pi WiFi Master for Raspberry Pi Pico (courtesy of thepihut.com)

Sadly, while there are WiFi code examples listed on their list of examples (https://dip-pi.com/ready-to-use-examples), the WiFi links are not active, suggesting there may be examples in the future. However, the operating manual for the product has details on the features of the WiFi module as well as links to the AT commands supported.

See https://dip-pi.com/installation-and-operating-manuals to download the documentation for this module.

Maker Pi Pico Base

We first saw the Maker Pi Pico Base in Chapter 1. Recall, it supports Grove modules as well as a host of other nice features. While it does not have a WiFi module, the Pico Base has a special connector on the right side that allows you to connect an ESP-01 module to the board to provide Internet capabilities. Figure 13-3 shows the base with the ESP-01 connector highlighted.
Figure 13-3

Maker Pi Pico Base (courtesy of thepihut.com)

Like the other modules, this requires programming the ESP-01 via AT commands similar to the ESP8266 WiFi Module. Fortunately, you can load MicroPython on the ESP-01 and write your Python code to connect to the Internet. Sadly, there are no examples on the product website at this time to show you how to do that, but you could follow similar examples from the other modules as a guide.

If you already own a Maker Pi Pico Base and an ESP-01 (or similar module), this may be the least expensive option. So long as you can sort the AT commands, you can make this option work for you.

See https://github.com/CytronTechnologies/MAKER-PI-PICO for documentation and more information about using this module.

Pico Wireless Pack

The Pico Wireless Pack is another excellent product of pimoroni.com. The module has the familiar female header on the bottom so that you can use it to connect directly to your Pico. It also has an SD card! Wow.

While it too is based on an ESP32 chip, you do not have to use AT commands to access it. In fact, Pimoroni provides a custom MicroPython uf2 image that contains all of the Pimoroni Pico–specific libraries including the network library. Figure 13-4 shows the Pico Wireless Pack.
Figure 13-4

Pico Wireless Pack (courtesy of thepithut.com)

The Pico Wireless Pack is a departure from the last modules that require you to use AT codes to control the onboard ESP processor. Instead, we can use a library designed to simplify the use of the WiFi module. To illustrate the difference in how it is programmed, the following shows the same code as the preceding AT command example. Which one do you think is more intuitive?
import project8.ppwhttp as ppwhttp
WIFI_SSID = "your SSID here!"
WIFI_PASS = "Your PSK here!"
ppwhttp.start_wifi(WIFI_SSID, WIFI_PASS)

This is much easier to understand than using the AT commands, unless, of course, you are proficient in their use.

There is also a user-configurable button, an RGB LED, and an SD card reader on the module, which is a nice touch. See https://shop.pimoroni.com/products/pico-wireless-pack for documentation and more information about using this module.

Caution

The documentation for the Pico Wireless Pack states that the library for the SD card is experimental, but so long as you use it for base file reading and writing, you should not have a problem.

So, Which One Do You Choose?

Only one of these modules supports a programming interface other than the UART-based AT commands. That means there is only one choice for those of us who want to connect our Pico to the Internet and use code that resembles how other platforms use networking code. Thus, we will use the one module that allows us to use our Pico to work with TCP/IP connections – the Pico Wireless Pack from pimoroni.com.

Using the Pico Wireless Pack

To use the Pico Wireless Pack, you must download all of the Pimoroni-specific libraries and set up your Pico to use them, which is a potentially time-consuming process. Fortunately, you can avoid all that and simply download their custom Pico boot image and copy it to your Pico.

Start by visiting https://github.com/pimoroni/pimoroni-pico/releases/ and download the file located in the v0.3.2 folder named similar to the following (the version number of the folder and file may differ):
v0.3.2/pimoroni-pico-v0.3.2-micropython-v1.17.uf2

You can refer to Chapter 1 for how to set up your Pico to use the new boot image, but, briefly, you simply remove the Pico from your PC, press and hold the BOOTSEL button, then connect the Pico to your PC. Once the Pico opens as a drive, you can copy the .uf2 file to the drive and then remove the Pico and reconnect it.

Note

You must use the custom .uf2 from Pimoroni to use the Pico Wireless Pack examples and the projects in this chapter and the next.

Unlike some of the other WiFi offerings for the Pico, the Pico Wireless Pack has several simple examples available on the Pimoroni GitHub site. You can find example WiFi code for the Wireless Pack at https://github.com/pimoroni/pimoroni-pico/tree/main/micropython/examples/pico_wireless.

Let’s look at a couple of simple examples so we can ensure the module is working on our WiFi. One is a simple utility, but the other is a nifty example of how you can build a simple web server and run it on your Pico. Specifically, it is a simple HTTP (hypertext transfer protocol3) listener that accepts commands on a specific port and responds by sending preformatted HTML back to the client.

We won’t go through the code at this time; rather, we will concentrate on the mechanics of getting the examples to work. We will see more about the details of how the HTTP server works in a later section.

Download the entire repo by visiting https://github.com/pimoroni/pimoroni-pico and clicking the Code button and choosing Download Zip. Once downloaded, you can unzip the file and navigate to the folder to see the code for the examples.

We will look at two examples. The first is a simple WiFi scanner that finds the WiFi access points within the area. To use this example, open Thonny and navigate to the <download dir> pimoroni-pico/tree/main/micropython/examples/pico_wireless where <download dir> refers to the location on your PC where you downloaded and unzipped the GitHub code.

Next, connect your Pico and copy the pphttp.py and scan_networks.py files to your Pico. You can then open and run the scan_networks.py file. You should see output that lists the WiFi access points in your area like the following. You should see your own WiFi on this list. If you do not, try moving your Pico closer to the router. Once you find your WiFi, you are ready to try to connect to it.
Found 5 network(s)...
0: FrogPond
1: DIRECT-FB-HP OfficeJet Pro 8730
2: Snapping1
3: Snapping2
4: ATT-WIFI-0123

Once you determine your Pico can find your wireless network, you can then test it further by running the rgb_http.py example. This example lets you control the RGB LED on the Pico Wireless Pack using your browser. It demonstrates how you can control hardware over the Internet, which is a large part of what the IoT is all about!

To run this example, you will need to copy the pphttp.py, rgb_http.py, and secrets.py files to your Pico. Then, you will need to open the secrets.py file and enter the SSID and password for your WiFi. For example, if your SSID is named Snapper1 and password is secret_code, open the secrets.py file and edit it as shown. Be sure to save the file on your Pico.
WIFI_SSID = "Snapper1"
WIFI_PASS = "secret_code"
Once you’ve saved the file, you can open the rgb_http.py file and run it. When the code starts, you will see the Pico will connect to your WiFi and then print out several messages. The one we must find is the one that shows the IP address that the Pico is using to host the HTTP server. This is known as “listening” since the code is waiting for a connection on that IP and port. The following shows an example of the messages you should see with the IP and port for the listening message shown in bold:
Connecting to Snapper1...
Connected!
Starting server...
Server listening on 192.168.1.20:80

Once you run the server, connect to it with the IP address and port shown in the output using your web browser on your PC. Using the information in the preceding example, you would use http://192.168.1.20:80 in the URL box. Your browser would then connect to the HTTP server on the Pico and display the web page.

Go ahead and try it out. Open your browser and specify the IP address shown in the console output. Figure 13-5 shows what you should see in your browser window.
Figure 13-5

RGB HTTP example website

If you do not see the web page, be sure to double-check that your PC is connected to the same network as your Pico. This can happen if you have multiple WiFi networks, or your PC is hardwired to a networking device on another network.

You can now use the drop-down boxes to change the value for each of the red, green, and blue variables. You can use the drop-down or type in a specific value for each. Let’s try several examples. Just type in the value for each of the boxes and then click Set LED. For example, you could try the base colors such as red (255,0,0), green (0,255,0), and blue (0,0,255). You should see the RGB LED on the Pico Wireless Pack change to match the values you entered.

Back in the Thonny console, you can see the diagnostic messages printed each time you connect to the Pico and change the values for the RGB LED. Notice we see there are two commands being processed: GET for retrieving data from the server and PUT for sending data to the server. The GET returns the result of changing the web page data, and the PUT is where the RGB values are sent to the server:
Client connected!
Serving GET on /...
Success! Sending 200 OK
Client connected!
Serving POST on /...
Set LED to 255 0 0
Success! Sending 200 OK
Client connected!
Serving POST on /...
Set LED to 255 255 0
Success! Sending 200 OK
Client connected!
Serving POST on /...
Set LED to 255 255 255
Success! Sending 200 OK
Client connected!
Serving POST on /...
Set LED to 0 0 0
Success! Sending 200 OK

Once that works, you are ready to start adding networking to your projects. Let’s look at some of the projects in this book and see how we can turn them into simple IoT projects.

IoT Project Examples

Let’s see how to apply what we learned to two of our example projects to complete the IoT portion for each. We are going to use the pedestrian crossing example from Chapter 7 and the soil moisture monitor project from Chapter 8. We will be creating a rudimentary web server for each project running on the Pico. This will allow you to access the project from anywhere on your network (or beyond).

Rather than develop the web server code from scratch, we will be mimicking the code from the Pimoroni example code for the Pico Wireless Pack. If you have not yet downloaded the code, please visit https://github.com/pimoroni/pimoroni-pico and download the code. Once downloaded, we will want to copy the example code files from <download dir> pimoroni-pico/tree/main/micropython/examples/pico_wireless and into your own project folder.

For example, you can create project7 and project8 folders for the next two projects. Then, copy the secrets.py files in the example source code to each of the project folders and modify it for your WiFi. Now you are ready to start working on the following examples.

Example 1: Pedestrian Crossing

In this example, we will use the pedestrian crossing example from Chapter 7. Recall, this project simulates a pedestrian crossing signal where the pedestrian presses a button to request the traffic lights cycle to allow them to cross the street. Instead of a button, we will use a web page to trigger the walk request. Yes, we will see how to remotely control the hardware and our code over the network. Let’s get started!

Set Up the Hardware

For this project, you will need to refer to Chapter 7 and set up the hardware using a breadboard and the LEDs and resistors just like you did before. However, this time we will be using the Pico Omnibus host board from Pimoroni. Table 13-1 shows an updated hardware list for the project.
Table 13-1

Required Components for the Pedestrian Crossing Web Example

Component

Qty

Description

Cost

Links

Red LED

2

Pack of 25

$4.00

www.adafruit.com/product/299

Single

$0.35

www.sparkfun.com/products/9590

Yellow LED

1

Pack of 25

$4.95

www.adafruit.com/product/2700

Single

$0.35

www.sparkfun.com/products/9594

Green LED

2

Pack of 25

$4.00

www.adafruit.com/product/298

Single

$0.35

www.sparkfun.com/products/9592

220 or 330 Ohm resistors

5

Variety Kit

$7.95

www.sparkfun.com/products/10969

Pack of 25

$0.75

www.adafruit.com/product/2780

Pico Omnibus

1

Pico host board

$8.25

https://shop.pimoroni.com/products/pico-omnibus

Pico Wireless Pack

1

WiFi

$13.20

https://shop.pimoroni.com/products/pico-wireless-pack

Breadboard

1

Prototyping board, full-sized

$5.95

www.sparkfun.com/products/12615

$5.95

www.adafruit.com/product/239

Jumper wires

6

M/F jumper wires, 7" (set of 30)

$2.25

https://www.sparkfun.com/products/11026

Notice the jumper wires need to be the M/F type since the GPIO on the Pico Omnibus has male pins. Go ahead and plug your Pico into the Pico Omnibus and the Pico Wireless Pack onto the right side of the board. We will use the left-side GPIO headers for the LEDs. But use care, because the GPIO header rows are reversed to make it easy to add modules designed to mount to the bottom of the Pico. Thus, you must read the label on the Pico Omnibus carefully to ensure you use the correct pins.

Figure 13-6 shows the Pico Omnibus with the Pico Wireless Pack installed. Notice the left side (Deck 1) is where you will connect the jumper wires for the LEDs.
Figure 13-6

Pico Omnibus with the Pico Wireless Pack

There is one other change. The Pico Wireless Pack uses some of the GPIO pins that we had in the Chapter 7 implementation. Thus, we must use different pins. Table 13-2 shows the pins we will use for the LEDs. The changes are shown in bold.
Table 13-2

Connections for the Pedestrian Crossing Web Example

Physical Pin

GPIO Num/Function

Connection

3

GND

Breadboard ground (bottom)

6

GP5

Resistor for red LED (stoplight)

5

GP4

Resistor for yellow LED (stoplight)

4

GP3

Resistor for green LED (stoplight)

1

GP1

Resistor for red LED (walk light)

0

GP0

Resistor for green LED (walk light)

N/A

Breadboard ground

All LED negative side

N/A

Resistor

All LED positive side

Figure 13-7 shows a schematic of how the wiring should be oriented for the Pico Omnibus to the breadboard hosting the LEDs.
Figure 13-7

Stoplight simulation wiring (web version)

Once you have the code wired, you are ready to start writing the code.

Write the Code

The code for this example will use the core code from the project in Chapter 7, but we will not be using the main() function. Rather, we will see how to use the Pimoroni library to create a simple HTTP server (also called a listener). The code will send a short HTML-based response (a simple web page) to the client that includes a form containing a single button for the walk request. The listener will listen on port 80.

We will also see an advanced technique using the threading library available on the Pico. As you will see, this library permits us to execute a function in a second core processor on the Pico. It is an excellent way to keep two things going at the same time and get a bit more processing at the same time.

The concept of the HTML server is quite simple. The code listens for a connection on the socket port and then receives the request (in this case, in the form of an HTML GET method) and sends an HTML response (the web page). If the button is pressed, the walk cycle will commence by calling the cycle_lights() function from the Chapter 7 project.

If you’ve never used HTML code before, don’t worry as the example code will provide everything you need. You don’t have to learn HTML for this chapter, but a basic knowledge would be helpful if you want to elaborate on the project or use the HTML server concept for your own projects. A reliable source of information about HTML can be found at www.w3schools.com/html/.

Since this is all new code, we will step through all of the parts so you can see how it works, but we will skip the code for controlling the LEDs. Please refer to Chapter 7 if you have not completed that project.

We will create a new file named pedestrian_crossing_web.py. Go ahead and open a new file in Thonny if you want to follow along.

Let’s begin with the code needed for the import section.

Imports

We need a few libraries in the import section. We need the Pin, _thread, utime, and sys libraries, which are standard libraries for the Pico. The Pimoroni-specific library is named ppwhttp. Notice we place the import statement in a try…else block and print a message if the library cannot be found (the ImportError is raised). This is a common way to test to see if a library is present on the Pico:
# Import libraries
from machine import Pin
import _thread
import utime
import sys
# Check for the Pimoroni http class library.
try:
    import ppwhttp
except ImportError:
    raise RuntimeError("Cannot find ppwhttp. Have you copied ppwhttp.py to your Pico?")

Let’s look at the global variables we need.

Global Variables

We will need to define the web page we will send to the client and variables for the LEDs. The HTML for the page is represented as a string. If the HTML code were complex or longer, it is best to store the code in a separate file, but since the code is short, we will leave it in the code. Again, don’t worry about learning all the details for the HTML code. As you can see, it is simple to understand, but the tags (those in the square brackets) may not be familiar:
# HTML web page for the project
MAIN_PAGE = """<!DOCTYPE html>
<html>
  <head>
    <title>Beginning MicroPython</title>
  </head>
  <center><h2>Pedestrian Crosswalk Simulation</h2></center><br>
  <center>A simple project to demonstrate how to control hardware over the Internet.</center><br><br>
  <form>
    <center>
      <button name="WALK" value = "PLEASE" type="submit" style="height: 50px; width: 100px">REQUEST WALK</button>
    </center>
  </form>
</html>
"""
# Setup the button and LEDs
stoplight_red = Pin(5, Pin.OUT)
stoplight_yellow = Pin(4, Pin.OUT)
stoplight_green = Pin(3, Pin.OUT)
pedestrian_red = Pin(1, Pin.OUT)
pedestrian_green = Pin(0, Pin.OUT)
# Setup lists for the LEDs
stoplight = [stoplight_red, stoplight_yellow, stoplight_green]
pedestrian_signal = [pedestrian_red, pedestrian_green]

We will also need to create a number of functions to manage the HTML server and control the LEDs.

Functions Needed

We will create five functions as follows:
  • cycle_lights(): The same function from Chapter 7. You can copy it from the Chapter 7 project unchanged.

  • get_home(): A callback function for the HTML server. This function is called when a client connects to the server. It simply returns the web page.

  • get_walk(): A callback function for the HTML server. This function is called when the client clicks the button on the web page.

  • server_loop_forever(): A special function that contains the main loop for the HTML server. It is used as a parameter for the threading module enabling the HTML server to run on the other microcontroller core on the Pico.

  • main(): The main function we will call when the script is loaded and executed.

Let’s look at the code for these functions.

As mentioned, the cycle_lights() function is unchanged from the Chapter 7 project, so we will skip the details.

The two functions for the HTML server are very simple. The following shows the code for both functions:
@ppwhttp.route("/", methods=["GET", "POST"])
def get_home(method, url, data=None):
    return MAIN_PAGE
@ppwhttp.route("/?WALK=PLEASE", methods=["GET"])
def get_walk(method, url, data=None):
    if method == "GET":
        cycle_lights()
    return MAIN_PAGE

The key to how these functions work is the decorator before each function. Notice the @ppwhttp.route() decorator. This is used by the pwhttp library to identify functions used to “route” control when a client connects and sends a GET or POST method. The first parameter to the decorator defines the path portion of the URL sent. This is how the route is determined. For example, if the client enters http://192.168.1.20:80, the get_home() function is called. Similarly, if the client enters http://192.168.1.20:80/?WALK=PLEASE, the get_walk() function is called. Finally, notice inside the get_walk() function, we test the method, and if it is a GET operation, we call cycle_lights(). This completes the simple HTML web server!

Note

The port (:80) of the URL is optional since port 80 is the default for HTML.

Next is the function we use for the threading library, server_loop_forever(), as shown in the following. When this function is called from the threading library (it is only called once), it starts the HTML server and then drops into a loop calling the handle_http_request() function of the pwhttp library:
def server_loop_forever():
   server_sock = ppwhttp.start_server()
   while True:
       ppwhttp.handle_http_request(server_sock)
       utime.sleep(0.01)
Finally, we have the main() function, which is where everything comes together. We place the LED setup code, call the start_wifi() function of the pwhttp library to connect to our WiFi, then call the start_new_thread() function of the threading library to launch a new thread. The following shows the main() function:
def main():
    # Turn off the LEDs
    for led in stoplight:
       led.off()
    for led in pedestrian_signal:
       led.off()
    # Start with green stoplight and red pedestrian_signal
    stoplight[2].on()
    pedestrian_signal[0].on()
    ppwhttp.start_wifi()
    # Handle the server polling loop on the other core!
    _thread.start_new_thread(server_loop_forever, ())

Recall, we place the SSID and password of our WiFi router in the secrets.py file, which must be uploaded to the Pico. The pwhttp library will open the file and read the values when the start_wifi() function is called.

The main() function is called using the normal mechanism we’ve used in previous projects located at the bottom of the file.

That’s all there is to it. Listing 13-1 shows the completed code for this project with comments and code for the LEDs removed for brevity.
# Import libraries
from machine import Pin
import _thread
import utime
import sys
# Check for the Pimoroni http class library.
try:
    import project7.ppwhttp
except ImportError:
    raise RuntimeError("Cannot find ppwhttp. "
                       "Have you copied ppwhttp.py to your Pico?")
# HTML web page for the project
MAIN_PAGE = """<!DOCTYPE html>
<html>
  <head>
    <title>Beginning MicroPython</title>
  </head>
  <center><h2>Pedestrian Crosswalk Simulation</h2></center><br>
  <center>A simple project to demonstrate how to control hardware over the Internet.</center><br><br>
  <form>
    <center>
      <button name="WALK" value = "PLEASE" type="submit" style="height: 50px; width: 100px">REQUEST WALK</button>
    </center>
  </form>
</html>
"""
# Setup the button and LEDs
stoplight_red = Pin(5, Pin.OUT)
stoplight_yellow = Pin(4, Pin.OUT)
stoplight_green = Pin(3, Pin.OUT)
pedestrian_red = Pin(1, Pin.OUT)
pedestrian_green = Pin(0, Pin.OUT)
# Setup lists for the LEDs
stoplight = [stoplight_red, stoplight_yellow, stoplight_green]
pedestrian_signal = [pedestrian_red, pedestrian_green]
def cycle_lights():
    print("START WALK")
...
    print("END WALK")
@ppwhttp.route("/", methods=["GET", "POST"])
def get_home(method, url, data=None):
    return MAIN_PAGE
@ppwhttp.route("/?WALK=PLEASE", methods=["GET"])
def get_walk(method, url, data=None):
    if method == "GET":
        cycle_lights()
    return MAIN_PAGE
def server_loop_forever():
   server_sock = ppwhttp.start_server()
   while True:
       ppwhttp.handle_http_request(server_sock)
       utime.sleep(0.01)
def main():
    # Turn off the LEDs
    for led in stoplight:
       led.off()
    for led in pedestrian_signal:
       led.off()
    # Start with green stoplight and red pedestrian_signal
    stoplight[2].on()
    pedestrian_signal[0].on()
    ppwhttp.start_wifi()
    # Handle the server polling loop on the other core!
    _thread.start_new_thread(server_loop_forever, ())
if __name__ == '__main__':
    try:
        main()
    except (KeyboardInterrupt, SystemExit) as err:
        print(" bye! ")
sys.exit(0)
Listing 13-1

Completed Code for the Pedestrian Crossing Web Example

OK, now we’re ready to execute the project.

Execute

Before executing the project, be sure to upload the pedestrian_crossing_web.py to the root of the Pico onboard drive. You also need to create a project7 folder on the Pico and upload the Pico Wireless Pack library (ppwhttp.py) file into that folder. Finally, you also need to upload the secrets.py file from the Pico Wireless Pack library and modify it to include your WiFi SSID and password. Upload this file to the root of the Pico onboard drive.

OK, now we’ve got the code setup to control our LEDs, and we have the code for a simple HTML server setup to listen on 80. All we need now is the IP address of that board to point our web browser. We can get that from our debug statements by running the code. Listing 13-2 shows the initial run for the project.
Connecting to Snapper1...
Connected!
>Starting server...
Server listening on 192.168.1.20:80
Client connected!
Serving GET on /...
Success! Sending 200 OK
Client connected!
Serving GET on /?WALK=PLEASE...
START WALK
END WALK
Success! Sending 200 OK
Listing 13-2

Running the Pedestrian Crossing Web Project

Notice in this case the IP address is 192.168.1.20. All we need to do is put that in our browser as shown in Figure 13-8.
Figure 13-8

Executing the pedestrian crossing web project

Once you enter the URL, you should see a web page like the image shown. If you don’t, be sure to check the HTML in your code to ensure it is exactly like what is shown; otherwise, the page may not display properly. You should also ensure the network your PC is connected to can reach the network to which your board is connected. If your home office is set up like mine, there may be several WiFi networks you can use. It is best if your board and your PC are on the same network (and same subnet).

Tip

If your Pico doesn’t connect to your WiFi within a reasonable time, you may need to click Stop in Thonny and rerun the project to reset the Wireless Pack.

Once you get that sorted out, verify the green LED for the stoplight is on and the red LED for the pedestrian crossing is on. All other LEDs are off. If you do not see something similar, go back and check your connections again.

Now go ahead and click the button. Remember, the walk button will engage, and you will see the lights cycle, but you won’t be able to do anything until the walk cycle is complete. This is because we don’t return the response HTML until after the cycle is complete (see the code to convince yourself).

Notice in the output window in Thonny there are debug messages printed for each time the client connects (the code accepts the connection and GET request) as well as a statement about what it is doing. You should see something similar.

Now, let’s look at a second example.

Example 2: Soil Moisture Monitor

In this example, we will use the plant monitoring project from Chapter 8. This project used one or more soil moisture sensors to read the relative moisture in the soil for one or more plants. While the features of the project are the same as we saw in Chapter 8, the code for this example is more complex.

Note

The example is meant to show what is possible rather than a complete project. Suggestions on how to improve the code are presented in a later section.

We will be using more advanced HTML code, but once again will not explain all of the details. If you’d like to know how each of the tags is used, you may want to consult a WWW resource or simply Google “HTML tags.”

Also, while this project is based on the project in Chapter 8, we will not be using an OLED screen because we will be writing code to a file and reading it when a client connects to the web server returning as a table. As you will see, writing data to a file is much easier and uses less complex code.

We will also be using a different real-time clock (RTC) module. We will use the Pico RTC from waveshare.com because it has a set of pass-through (also called stacking) headers so you can mount the Pico on top and the RTC module to the Pico Omnibus saving us from wiring it to the GPIO headers. Figure 13-9 shows the Pico RTC DS3231 module. See www.waveshare.com/wiki/Pico-RTC-DS3231 for more details about this module.
Figure 13-9

Pico RTC DS3231 (courtesy of thepihut.com)

Let’s look at the hardware for this example.

Set Up the Hardware

For this project, you will need to refer to Chapter 8 and set up the hardware using a breadboard and the soil moisture modules just like you did before. However, this time we will be using the Pico Omnibus host board from Pimoroni and a micro-SD card to store the data. Table 13-3 shows an updated hardware list for the project.
Table 13-3

Required Components for the Plant Monitoring Example

Component

Qty

Description

Cost

Links

Soil moisture

1+

Sensor

$6.95

www.sparkfun.com/products/13637

RTC module

1

Pico RTC DS3231

$14.88

https://thepihut.com/products/precision-rtc-module-for-raspberry-pi-pico-ds3231

CR1220 coin cell

1

Battery

$0.88

https://thepihut.com/products/cr1220-12mm-diameter-3v-lithium-coin-cell-battery

Pico Omnibus

1

Pico host board

$8.25

https://shop.pimoroni.com/products/pico-omnibus

Pico Wireless Pack

1

WiFi

$13.20

https://shop.pimoroni.com/products/pico-wireless-pack

Breadboard

1

Prototyping board, full-sized

$5.95

www.sparkfun.com/products/12615

$5.95

www.adafruit.com/product/239

Jumper wires

6

F/F jumper wires, 6" (set of 10)

$5.95

www.sparkfun.com/products/11710

Micro-SD card

1

Micro-SD card (any size)

Varies

Commonly sourced

Notice the jumper wires need to be the F/F type since the GPIO on the Pico Omnibus has male pins and the soil moisture sensors also have male pins. Go ahead and plug your Pico into the Pico Omnibus and the Pico Wireless Pack onto the right side of the board like we did in the last example.

Note

Remember, the GPIO header is reversed on the Pico Omnibus. Be sure to read the label on the host board to ensure you have the right pin selected.

There is one other change. The RTC module uses some of the GPIO pins that we had in the Chapter 8 implementation. Thus, we must use different pins. Table 13-4 shows the pins we will use for the LEDs. The changes are shown in bold.
Table 13-4

Connections for the Plant Monitoring Web Example

Physical Pin

GPIO Num/Function

Connection

3

GND

Ground for Sensor 1

8

GND

Ground for Sensor 2

21

GP18

Power for Sensor 2

22

GP17

Power for Sensor 1

33

GP27

Signal for Sensor 1

34

GP28

Signal for Sensor 2

Figure 13-10 shows a schematic of how the wiring should be oriented for the Pico Omnibus to the breadboard hosting the soil moisture sensors.
Figure 13-10

Plant monitoring wiring (web version)

Once you have the code wired, you are ready to start writing the code.

Write the Code

The code for this example will use some of the code from the project in Chapter 8. We will use the read_timer.py file without changes. We must modify the SoilMoisture class (soil_moisture.py) to work with a file instead of storing the values read in memory. Finally, we will not need the display library.

Note

We will examine the new and changed code in this section. Other code modules used are unchanged from Chapter 8. Refer to Chapter 8 for details on those modules.

However, there are two additional libraries we will need for this project. We will need a library for the SD card and another for the RTC module.

Once again, the SD card support on the Pico Wireless Pack is considered experimental, but the basic operations for file reading and writing work just fine. While Pimoroni does not currently supply a MicroPython library, we can find one elsewhere. The library that works best is found at https://forums.pimoroni.com/t/pico-wireless-how-to-access-sd-card/17751/3. Simply open Thonny and create a new file, then copy the code from the website into the file and name it sdcard.py. You will upload this file to the project8 folder on the Pico later.

The library for the RTC module can be found on the vendor’s website in the form of a zip file (www.waveshare.com/w/upload/2/26/Pico-rtc-ds3231_code.zip). Download the file and unzip it and then locate the file named ds3231.py in the python folder. You will upload this file to the project8 folder on the Pico later.

However, the library function named read_time() prints a string for the time. That won’t work for us since we need to get the time either as a tuple like we did in Chapter 8 or, since the read_time() function already formats the datetime for printing, return the string instead. Thus, you will need to modify this class slightly for use with this project.

To do so, first copy the file named ds3231.py to your project folder and then open it and apply the following changes shows a unified difference file where the lines marked with a are removed and those with a + are added.4 You will copy this file to the project8 folder on your Pico later:
diff.exe" -u  .... awPico-rtc-ds3231_codePico-rtc-ds3231_codepythonds3231.py ds3231.py
--- .... awPico-rtc-ds3231_codePico-rtc-ds3231_codepythonds3231.py        2021-12-18 17:51:19.863781600 -0500
+++ ds3231.py   2021-12-21 15:31:55.456127500 -0500
@@ -55,7 +55,7 @@
         d = t[3]&0x07  #week
         e = t[4]&0x3F  #day
         f = t[5]&0x1F  #month
-        print("20%x/%02x/%02x %02x:%02x:%02x %s" %(t[6],t[5],t[4],t[2],t[1],t[0],self.w[t[3]-1]))
+        return "20%x/%02x/%02x %02x:%02x:%02x" %(t[6],t[5],t[4],t[2],t[1],t[0])
     def set_alarm_time(self,alarm_time):
         #    init the alarm pin

Notice we are only changing the print() statement to return the string without the day of the week.

Now that we have the libraries we need and the RTC library modified, let’s start by looking at the main code.

Main Code

The main code is like the code for the last project. However, this time we will use a file to store the HTML code (since it doesn’t change) and a list of HTML-formatted strings for populating an HTML table with the data from the file.

Unlike the last project, the HTML code does not include a button, but we can format a command manually on the URL. We can use this technique to allow access to commands without using buttons or other user interface features. It also helps to make these commands harder to use to prevent overuse. For example, we can provide a clear log command. We would use a URL like http://192.168.42.140/CLEAR, which submits a GET request to the HTML server. We can capture that command and clear the log when it is issued.

The following sections explain the initialization code and the functions needed. We will see the complete code in a later section. Let’s start with the HTML code.

HTML Code (Files)

We will store the HTML code needed in files to save memory. Recall by reading a row at a time, we do not have to take up space with the strings in our code. As your projects grow in complexity, this could become an issue. Thus, this project demonstrates a way to save some memory.

The HTML for this project creates a web page with a simple table that includes all the data in the file at the time of the request. To make things easier, we will use three files. The first file (named part1.html) will contain the HTML code up to the table rows, and the second file (named plant_data.csv), which is populated by the SoilMoisture class, and the third (named part2.html) will contain the remaining HTML code.

The first file, part1.html, is shown in Listing 13-3. This file establishes the table HTML code. It also establishes characteristics for the table including text alignment, border size, and padding – all through cascading style (<style> tag). Don’t worry if this looks strange or alien. You can google for W3C standards to see how we use the tag to control the style of the web page.
<!DOCTYPE html>
<html>
  <head>
    <title>Beginning MicroPython - Project 8</title>
    <meta http-equiv="refresh" content="30">
    <style>
      table, th, td {
          border: 1px solid black;
          border-collapse: collapse;
      }
      th, td {
          padding: 5px;
      }
      th {
          text-align: left;
      }
    </style>
  </head>
  <center><h2>Beginning MicroPython - Project 8</h2></center><br>
  <center>A simple project to demonstrate how to retrieve sensor data over the Internet.</center>
  <center><br><b>Plant Monitoring Data</b><br><br>
    <table style="width:75%">
      <col width="180">
      <col width="120">
      <col width="125">
      <col width="125">
      <tr><th>Datetime</th><th>Sensor Number</th><th>Raw Value</th><th>Moisture</th><th>Location</th></tr>
Listing 13-3

HTML Code (part1.html)

Notice the meta tag. Here is an example of how we can add HTML code to automatically refresh the page periodically. In this case, it will refresh every 30 seconds.

Notice the table code. Again, don’t worry if this seems strange. It works and it is very basic. Those familiar with HTML may want to embellish and improve the code. The last line establishes the header for the table.

The second file, plant_data.csv, contains the data. We will use a constant to populate a properly formatted HTML table row. The following shows an example of what a row of data would look like in the file and how that data is transformed to HTML. We will see the HTML for the table row in the next section.
# Raw data
2021-08-08 17:26:17,1,78,dry,Small fern on bottom shelf
# HTML table row
<tr><td>2021-08-08 17:26:17</td><td>1</td><td>78</td><td>dry</td><td>Small fern on bottom shelf </td></tr>
The last file, part2.html, contains the closing tags so it isn’t very large. But since we’re reading from files, we include this file. The following shows the code in the second file:
    </table>
  </center>
</html>

So, how do we use these files? When we send a response back to the client (the web page), we read the first file sending one row at a time, then read the data file sending one row at a time, then read the last file sending one row at a time. We will use a helper function to read the data file in the SoilMoisture class.

Imports
The imports we need for the main code include those for the threading (_thread), operating system (uos), time (utime), system (sys), RTC (ds3231), the read timer (read_timer), the soil moisture sensors (soil_moisture), and finally the Pimoroni Wireless Pack library (pwhttp). The complete list of imports is shown in Listing 13-4. If you want to follow along, open a new file and name it main.py in Thonny.
# Import libraries
import _thread
import uos
import utime
import sys
from project8.ds3231 import ds3231               # RTC library
from project8.read_timer import ReadEvent        # Read event timer class
from project8.soil_moisture import SoilMoisture  # SoilMoisture class
# Check for the Pimoroni http class library.
try:
    import project8.ppwhttp as ppwhttp
except ImportError:
    raise RuntimeError("Cannot find ppwhttp. Have you copied ppwhttp.py to your Pico?")
Listing 13-4

Imports for main.py

Now let’s look at the constants.

Constants
We need a string we can use to create the rows for the table as it is read from the file. The following shows the string used. Notice we use replacement syntax so that we can use the format() function to fill in the details:
# HTML web page for the project
HTML_TABLE_ROW = "<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>"
Recall we will be using a special command in the form of /CLEAR to clear the log, for example, http://192.168.1.20/CLEAR. Once you enter that URL, unless redirected, the web browser will remain at that locator. To force the browser to return to the home page, we use a meta tag that we will populate with the IP address for our web server later. The following shows the format string we will use:
HTML_REDIRECT = '<meta http-equiv="Refresh" content="0; url=''{0}''" />'

Now let’s look at the setup code.

Setup Code
Unlike other forms of the main.py code we’ve seen in other examples, we will keep this one simple and place the setup code at the global level. We will need to open the HTML files and read them into memory, set up the RTC module, and set up the SoilMoisture class. Listing 13-5 shows the setup code. Since the latter is similar to the project from Chapter 8, we will skip the details. Refer to Chapter 8 for how to format the dictionary data for specifying the soil moisture sensors.
# Setup code
print("Welcome to the Plant Monitor Web Version! ")
# Read base HTML pages
with open("/part1.html") as html_file:
    WEB_PART1 = "".join(html_file.readlines())
with open("/part2.html") as html_file:
    WEB_PART2 = "".join(html_file.readlines())
# RTC Setup
I2C_SDA = 20
I2C_SCL = 21
rtc = ds3231(0, I2C_SCL, I2C_SDA)
# Setup Sensors class
sensor_list = [
    {
        'pin': 27,
        'power': 17,
        'location': 'Green ceramic pot on top shelf',
        'nick': 'Ivy',
    },
    {
        'pin': 28,
        'power': 18,
        'location': 'Fern on bottom shelf',
        'nick': 'Fern',
    }
]
# Setup the soil moisture object instance from the SoilMoisture class
plants = SoilMoisture(rtc, sensor_list)
Listing 13-5

Setup Code

Next, we will need three helper functions for the web server operations.

Helper Functions
For the routing operations, we will create a function named get_home() to route the home GET request that calls the get_html_sensor_data() function from the SoilMoisture class (explained in a later section) and another function named clear_log() to route the /CLEAR to route the GET request that calls the clear_log() function of the SoilMoisture class. Listing 13-6 shows the code for these functions.
@ppwhttp.route("/", methods=["GET", "POST"])
def get_home(method, url, data=None):
    # Read data and return web page.
    DATA_PART = ""
    try:
        DATA_PART = plants.get_html_sensor_data(HTML_TABLE_ROW)
        except Exception as err:
        print("Error reading data.", err)
    return "".join([WEB_PART1, DATA_PART, WEB_PART2])
@ppwhttp.route("/CLEAR", methods=["GET"])
def clear_log(method, url, data=None):
    if method == "GET":
        plants.clear_log()
    addr = ".".join(map(str, ppwhttp.get_ip_address()))
    redirect = HTML_REDIRECT.format("http://{0}:80".format(addr))
    return "".join([WEB_PART1, redirect, WEB_PART2])
Listing 13-6

Web Server Functions

We will also need a function to read the values from the sensor. To make the web server responsive to client requests, we will use a thread to run the soil moisture sensor reads. Recall, the soil moisture sensors need a length startup time to read data. The following shows the helper function we will use:
def read_sensors(plants):
    print("Reading sensors...")
    plants.read_sensors()
    print("Sensor read complete. Sleeping.")

Finally, we will use a function for the main portion of the code.

Main Function
The main() function will be called when the code module is loaded on startup. It is responsible for starting the WiFi, web server, and a loop to read the sensor data when the read event fires. Listing 13-7 shows the main() function. Since most of the code is familiar, we will leave the details as an exercise.
def main():
    ppwhttp.start_wifi()
    server_sock = ppwhttp.start_server()
    utime.sleep(2)
    # Main loop for reading client requests
    data_read_event = ReadEvent()
    while True:
        ppwhttp.handle_http_request(server_sock)
        utime.sleep(0.01)
        # Check to see if it is time to read the data
        if data_read_event.time_to_read():
            data_read_event.reset()
            # Handle the sensor reading loop on the other core!
            try:
                _thread.start_new_thread(read_sensors, [plants])
            except Exception as ex:
                print("ERROR: Cannot read sensors:", ex)
                sys.exit(1)
Listing 13-7

Main Function

Let’s look at the completed code.

Complete Code
Now that we have seen all the parts of the code module, let’s look at the completed code. Listing 13-8 shows the complete code for the main code module with comments removed for brevity. Once again, we can save this file as main.py.
# Import libraries
import _thread
import uos
import utime
import sys
from project8.ds3231 import ds3231               # RTC library
from project8.read_timer import ReadEvent        # Read event timer class
from project8.soil_moisture import SoilMoisture  # SoilMoisture class
# Check for the Pimoroni http class library.
try:
    import project8.ppwhttp as ppwhttp
except ImportError:
    raise RuntimeError("Cannot find ppwhttp. Have you copied ppwhttp.py to your Pico?")
# Constants
# HTML web page for the project
HTML_TABLE_ROW = "<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>"
HTML_REDIRECT = '<meta http-equiv="Refresh" content="0; url=''{0}''" />'
# Global Variables
WEB_PART1 = ""
WEB_PART2 = ""
# Setup code
print("Welcome to the Plant Monitor Web Version! ")
# Read base HTML pages
with open("/part1.html") as html_file:
    WEB_PART1 = "".join(html_file.readlines())
with open("/part2.html") as html_file:
    WEB_PART2 = "".join(html_file.readlines())
# RTC Setup
I2C_SDA = 20
I2C_SCL = 21
rtc = ds3231(0, I2C_SCL, I2C_SDA)
# Setup Sensors class
sensor_list = [
    {
        'pin': 27,
        'power': 17,
        'location': 'Green ceramic pot on top shelf',
        'nick': 'Ivy',
    },
    {
        'pin': 28,
        'power': 18,
        'location': 'Fern on bottom shelf',
        'nick': 'Fern',
    }
]
# Setup the soil moisture object instance from the SoilMoisture class
plants = SoilMoisture(rtc, sensor_list)
@ppwhttp.route("/", methods=["GET", "POST"])
def get_home(method, url, data=None):
    # Read data and return web page.
    DATA_PART = ""
    try:
        DATA_PART = plants.get_html_sensor_data(HTML_TABLE_ROW)
    except Exception as err:
        print("Error reading data.", err)
    return "".join([WEB_PART1, DATA_PART, WEB_PART2])
@ppwhttp.route("/CLEAR", methods=["GET"])
def clear_log(method, url, data=None):
    if method == "GET":
        plants.clear_log()
    addr = ".".join(map(str, ppwhttp.get_ip_address()))
    redirect = HTML_REDIRECT.format("http://{0}:80".format(addr))
    return "".join([WEB_PART1, redirect, WEB_PART2])
def read_sensors(plants):
    print("Reading sensors...")
    plants.read_sensors()
    print("Sensor read complete. Sleeping.")
def main():
    ppwhttp.start_wifi()
    server_sock = ppwhttp.start_server()
    utime.sleep(2)
    # Main loop for reading client requests
    data_read_event = ReadEvent()
    while True:
        ppwhttp.handle_http_request(server_sock)
        utime.sleep(0.01)
        # Check to see if it is time to read the data
        if data_read_event.time_to_read():
            data_read_event.reset()
            # Handle the sensor reading loop on the other core!
            try:
                _thread.start_new_thread(read_sensors, [plants])
            except Exception as ex:
                print("ERROR: Cannot read sensors:", ex)
                sys.exit(1)
if __name__ == '__main__':
    try:
        main()
    except (KeyboardInterrupt, SystemExit) as err:
        print(" bye! ")
sys.exit(0)
Listing 13-8

Main Code Module

Now that we have the main code module, let’s look at the soil moisture class.

Soil Moisture Class

The SoilMoisture class is based on the class with the same name from Chapter 8, but with some significant changes. Most significantly, the comma-separated file on the SD card to store the data (plant_data.csv) is saved on the SD card on the Wireless Pack, which permits easy removal of the data to transfer it to your PC (you don’t need the Pico powered on and connected to your PC). And, instead of returning only the last value read for each sensor, it returns all rows from the file as well as any recently read values. This way, we can populate a list (table) to show the user all values read.

However, to do so reliably when run with multiple threads, we will need to use a special threading concept called a lock to protect critical portions of the code that change class variables to ensure only one thread can change the value(s) at a time. We will see how this works as we work through the code.

Note

Be sure to refer to Chapter 8 for tips on calibrating the sensors. The code to calibrate the sensors is in Chapter 8.

Other than those changes, the features of the class remain the same, so we will only look at those functions that are removed or changed.

Briefly, this class will read the soil moisture sensors and record the data in a CSV file. The class is designed to read any number of sensors via a list of dictionaries passed when the class is instantiated. Recall from Chapter 8, we will use a new list of dictionaries that contain the Pin class instantiations for controlling the power (turning on or off) and the ADC class instantiations for reading data (signal pin). The following is an example of how to define two sensors in the dictionary. Please refer to Chapter 8 for more details.
sensor_list = [
    {
        'pin': 27,
        'power': 17,
        'location': 'Green ceramic pot on top shelf',
        'nick': 'Ivy',
    },
    {
        'pin': 28,
        'power': 18,
        'location': 'Fern on bottom shelf',
        'nick': 'Fern',
    }
]
Public Functions

The Chapter 8 implementation used three public methods to clear the log (clear_log()), get the last values read (get_values()), and a long-running function to read the sensors (read_sensors()). The read_sensors( ) function has been simplified, and the changes are easy to see.

We will also use the same private functions except for _format_time( ), which is no longer needed because we modified the read_time() function from the RTC class to return a formatted time string.

The version of the class for this project uses all of these functions and the existing private functions as mentioned but renames the get_values() function to get_html_sensor_data() to describe the new behavior more accurately. The get_html_sensor_data() accepts as a parameter a format string and returns all rows in memory formatted with the format string. This function therefore is used when the user visits the home (or root) of the web service and the get_home() function is called in main.py.

The code for these functions are simple enough, and most of it is unchanged from Chapter 8, but some explanation is needed for the get_html_sensor_data() function. In this function, we loop over the rows from the file and most recently read values and use the format string to format the rows and return them. We also see how we resolve the cache of values read since the code was launched. Notice we keep track of how many values (rows) were added, and when we reach the limit for the constant CACHE_LIMIT, we write the new rows to the end of the file. This way, if the server is stopped, we reduce the risk of losing all data stored in memory; rather, we may lose up to CACHE_LIMIT values if the code is stopped or crashes. Listing 13-9 shows the code for the function. Take some time to read through it to ensure you understand how it works. We will discuss the lock in the next section.
def get_html_sensor_data(self, HTML_FORMAT_STR):
    html_sensor_data = ""
    self.rw_lock.acquire()
    print("Read lock acquired.")
    for row in self.cached_values:
        cols = row.strip(" ").split(",") # split row by commas
        html = HTML_FORMAT_STR.format(cols[0], cols[1], cols[2],
 cols[3], cols[4])
        html_sensor_data += html
    # If there is too much data in the cache, write it to disk.
    if self.values_read >= CACHE_LIMIT:
        start_index = len(self.cached_values) - self.values_read
        self.values_read = 0
        print("Writing cache to disk...", end="")
        try:
            log_file = open(LOG_FILE, 'a')
            for index in range(start_index, len(self.cached_values)):
                row = self.cached_values[index]
                log_file.write("{0} ".format(row))
            log_file.close()
        except Exception as ex:
            print("ERROR: Cannot write cache to disk.", ex)
        print("done.")
    try:
        self.rw_lock.release()
        print("Read lock released.")
    except Exception as ex:
        print("ERROR: Cannot release read lock.", ex)
    return html_sensor_data
Listing 13-9

get_html_sensor_data() Function

Notice the use of the lock. Let’s talk about that for a moment before we see the completed code for the class.

Using Locks

Since we are working with two threads, the main execution and a thread we launch periodically to read data from the sensors when the read timer fires, we have the potential for two threads to access the same variables at the same time. More specifically, the self.cached_values variable is read by the get_html_sensor_data() function and written to in the read_sensors() function. If they are accessed at the same time, we could have one of several potentially critical errors or the code may fail, or not. It is completely unpredictable. Fortunately, there is a mechanism we can use to “lock” parts of the code so only one thread can access the critical section (the protected memory or variables) at a time.

The threading class provides a lock we can use to manage the read and write operations. To create the lock, we place the following in the constructor:
# Lock for read/write
self.rw_lock = _thread.allocate_lock()
For each section of code that accesses the critical areas, we first attempt to acquire the lock with self.rw_lock.acquire(), which by default waits until the lock is available. If it is not, it will wait indefinitely until it is. A lock is made available (or unlocked) with the self.rw_lock.release() function. The following illustrates how the lock is used to ensure only one thread is reading or writing the self.cached_values and self.values_read variables at one time:
self.rw_lock.acquire()
print("Write lock acquired.")
self.cached_values.append(value_read)
self.values_read += 1
self.rw_lock.release()

We also add the lock to the clear_log() function to ensure data isn’t being read or written when the log is cleared. See the completed code for more details.

Caution

The threading library for MicroPython is a much simplified version from the Python base. As such, it may not be as robust and could fail under certain conditions such as high contention. Take care when intentionally designing reentrant code in MicroPython.

Using the SD Card
There is one more change that is important to discuss. Recall, we are using the SD card reader on the Wireless Pack. Recall, we will use the sdcard.py library we downloaded earlier. This requires an SPI instance to work. The following illustrates how we set up the SPI interface and mount the drive. We will place this code in the constructor:
# Setup SD card via SPI
sck_pin = Pin(18, Pin.OUT)
mosi_pin = Pin(19, Pin.OUT)
miso_pin = Pin(16, Pin.OUT)
sd_pin = Pin(22)
sd_spi = SPI(0, sck=sck_pin, mosi=mosi_pin, miso=miso_pin)
sd = SDCard(sd_spi, sd_pin)
# Mount SD card
uos.mount(sd, "/sd")

Now, let’s look at the completed code for the class.

Completed Code
We will name the code module soil_moisture.py and place it on our Pico in the project8 folder (or similar). Listing 13-10 shows the completed code for the SoilMoisture class. Take a moment to read through the code to see how it works. Notice the constants that define the thresholds for wet and dry soil measurements. Recall, we got these through experimenting with the threshold example code in Chapter 8.
# Import libraries
from machine import ADC, Pin, SPI
import _thread
import uos
from utime import sleep
from project8.sdcard import SDCard
# Constants
LOG_FILE = "/sd/plant_data.csv"
# Thresholds for the sensors
LOWER_THRESHOLD = 500
UPPER_THRESHOLD = 2500
UPDATE_FREQ = 120   # seconds
# Max number of rows to store in memory before writing to disk
CACHE_LIMIT = 2
class SoilMoisture:
    def __init__(self, rtc, sensor_list):
        # Lock for read/write
        self.rw_lock = _thread.allocate_lock()
        # Setup SD card via SPI
        sck_pin = Pin(18, Pin.OUT)
        mosi_pin = Pin(19, Pin.OUT)
        miso_pin = Pin(16, Pin.OUT)
        sd_pin = Pin(22)
        sd_spi = SPI(0, sck=sck_pin, mosi=mosi_pin, miso=miso_pin)
        sd = SDCard(sd_spi, sd_pin)
        # Mount SD card
        uos.mount(sd, "/sd")
        # If LOG_FILE is not present, create it
        try:
            uos.stat(LOG_FILE)
        except OSError:
            print("Creating log file. ")
            log_file = open(LOG_FILE, "w")
            log_file.close()
        # Load data into memory
        self.cached_values = []
        log_file = open(LOG_FILE, "r")
        print("Reading data from disk...", end="")
        for row in log_file:
            self.cached_values.append(row)
        log_file.close()
        self.values_read = 0
        print("done.")
        self.rtc = rtc
        # Loop through the sensors specified and setup a new dictionary
        # for each sensor that includes the power and ADC pins defined.
        self.sensors = []
        sensor_num = 1
        for sensor in sensor_list:
            # Setup the dictionary for each soil moisture sensor
            soil_moisture = {
                'sensor': ADC(Pin(sensor['pin'])),
                'power': Pin(sensor['power'], Pin.OUT),
                'location': sensor['location'],
                'sensor_num': sensor_num
            }
            sensor_num += 1
            self.sensors.append(soil_moisture)
    
    def clear_log(self):
        print("Clearing log file")
        log_file = open(LOG_FILE, 'w')
        log_file.close()
        # Lock is on the self.cached_values variable
        self.rw_lock.acquire()
        print("Write (clear) lock acquired.")
        self.cached_values = []
        self.values_read = 0
        try:
            self.rw_lock.release()
            print("Write (clear) lock released.")
        except Exception as ex:
            print("ERROR: Cannot release write (clear) lock.", ex)
    def get_html_sensor_data(self, HTML_FORMAT_STR):
        html_sensor_data = ""
        self.rw_lock.acquire()
        print("Read lock acquired.")
        for row in self.cached_values:
            cols = row.strip(" ").split(",") # split row by commas
            html = HTML_FORMAT_STR.format(cols[0], cols[1], cols[2],
 cols[3], cols[4])
            html_sensor_data += html
        # If there is too much data in the cache, write it to disk.
        if self.values_read >= CACHE_LIMIT:
            start_index = len(self.cached_values) - self.values_read
            self.values_read = 0
            print("Writing cache to disk...", end="")
            try:
                log_file = open(LOG_FILE, 'a')
                for index in range(start_index, len(self.cached_values)):
                    row = self.cached_values[index]
                    log_file.write("{0} ".format(row))
                log_file.close()
            except Exception as ex:
                print("ERROR: Cannot write cache to disk.", ex)
            print("done.")
        try:
            self.rw_lock.release()
            print("Read lock released.")
        except Exception as ex:
            print("ERROR: Cannot release read lock.", ex)
        return html_sensor_data
    def _get_value(self, adc, power):
        total = 0
        # Turn power on
        power.high()
        for i in range (0,10):
            # Wait for sensor to power on and settle
            sleep(1)
            # Read the value
            value = adc.read_u16()
            total += value
        # Turn sensor off
        power.low()
        return int(total/10)
    def read_sensors(self):
        for sensor in self.sensors:
            # Read the data from the sensor and convert the value
            value = self._get_value(sensor['sensor'], sensor['power'])
            print("Reading sensor {0} - value: {1}"
                  "".format(sensor['sensor_num'], value))
            value_read = ("{0},{1},{2},{3},{4}"
                          "".format(self.rtc.read_time(),
                                    sensor['sensor_num'],
                                    value, self._convert_value(value),
                                    sensor['location']))
            self.rw_lock.acquire()
            print("Write lock acquired.")
            self.cached_values.append(value_read)
            self.values_read += 1
            try:
                self.rw_lock.release()
                print("Write lock released.")
            except Exception as ex:
                print("ERROR: Cannot release read lock.", ex)
    def _convert_value(self, value):
        # If value is less than lower threshold, soil is dry else if it
        # is greater than upper threshold, it is wet, else all is well.
        if (value <= LOWER_THRESHOLD):
            return "dry"
        elif (value >= UPPER_THRESHOLD):
            return "wet"
        return "ok"
Listing 13-10

SoilMoisture Class

OK, now it is time to give the code a go and run it.

Execute

Before executing the project, be sure to upload the main.py and secrets.py files to the root of the Pico onboard drive. Remember to modify the secrets.py file to include your WiFi SSID and password. You also need to create a project8 folder on the Pico and upload the Pico Wireless Pack library (ppwhttp.py), the modified library for the RTC (ds3231.py), the soil moisture class (soil_moisture.py), the SD card library (sdcard.py), and the read timer class (read_timer.py) files into that folder. Finally, you also need to upload the secrets.py file from the Pico Wireless Pack library. Upload this file to the root of the Pico onboard drive.

Note

You should insert the soil moisture sensors in the plant soil before powering on your Pico.

OK, now we’ve got the code setup to read the soil moisture in one or more plants, and we have the code for a simple HTML server setup to listen on port 80. All we need now is the IP address of that board to point our web browser. We can get that from our debug statements by running the code. Listing 13-11 shows a sample run for the project.
Welcome to the Plant Monitor Web Version!
Reading data from disk...done.
Connecting to Snapper2...
Starting server...
Server listening on 192.168.1.20:80
Reading sensors...
Reading sensor 1 - value: 606
Write lock acquired.
Write lock released.
Client connected!
Serving GET on /...
Read lock acquired.
Read lock released.
Success! Sending 200 OK
Reading sensor 2 - value: 499
Write lock acquired.
Write lock released.
Sensor read complete. Sleeping.
Client connected!
Serving GET on /...
Read lock acquired.
Writing cache to disk...done.
Read lock released.
Success! Sending 200 OK
Client connected!
Serving GET on /...
Read lock acquired.
Read lock released.
Success! Sending 200 OK
Client connected!
Serving GET on /...
Read lock acquired.
Read lock released.
Success! Sending 200 OK
Client connected!
Serving GET on /...
Read lock acquired.
Read lock released.
Success! Sending 200 OK
Listing 13-11

Running the Plant Monitor Web Project

Notice in this case the IP address is 192.168.1.20. All we need to do is put that in our browser as shown in Figure 13-11.
Figure 13-11

Executing the plant monitor web project

Once you enter the URL, you should see a web page like the image shown. If you don’t, be sure to check the HTML in your code to ensure it is exactly like what is shown; otherwise, the page may not display properly.

Tip

If your Pico doesn’t connect to your WiFi within a reasonable time, you may need to click Stop in Thonny and rerun the project to reset the Wireless Pack.

If everything is working, you can click refresh in your browser to read all of the values read from the file including those in the cache. Or you can wait until the meta tag refresh fires, which will automatically refresh the page. Neat!

Improving the Code

This example will suffice to show you what is possible for creating a small web server to present your data collected from sensors. As such, it is not intended to be run for extended periods because of how the data is stored in memory to make retrieval fasters. Additional work is needed to make it a longer-running project. The following are suggestions on how to improve the code for a more robust, longer-running project. The first two are ways to improve the current design where the historical data is presented in the web page, and the last is an alternative to show only the last values read for each sensor:
  • Change the code to always read data from the SD card (the plant_data.csv file) rather than memory. Hint: You will need to use the read/write lock to protect writing to the file when new values are read from the sensors.

  • Make the code read all of the rows from the SD card on start, but store all new values read into memory writing them to the file on the SD card only after 20 or more values are read.

  • Change the code to only display the latest values for the sensors writing all old values to the file on the SD card. This is the easiest and most robust option to consider making the code a long-running project.

Once you have both examples working, congratulations. You have just created your first complete IoT projects. How cool is that?

Summary

When you take a typical electronics project such as a weather station, electronic game, home automation, etc. and connect it to the Internet, you’ve just upped the capabilities of that small project.

We saw two simple examples of this by connecting two of our example projects to the Internet. Each used a simple web server to allow us to control hardware and get information from sensors. The technique demonstrated can help you add Internet capabilities to more of the projects in this book. You are only limited by your imagination!

In this chapter, we learned more about cloud systems and how they can be used in IoT projects. Now that you’ve seen how easy it is to get started and how little code is needed in your projects, you can begin to modify your own projects. But we’ve just scratched the surface here. There is so much more that can be done with another simple, free cloud solution.

In the next chapter, we will expand our tour of cloud systems for IoT by looking at one of the most popular free options: ThingSpeak – a popular, easy-to-use, cloud-based IoT data hosting service from MathWorks. You will learn how to send your data to the cloud and display it using nice, easy-to-use graphics using the previous example projects.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.236.100.210