© Joseph Coburn 2020
J. CoburnBuild Your Own Car Dashboard with a Raspberry Pihttps://doi.org/10.1007/978-1-4842-6080-7_7

7. Temperature Monitoring

Chapter goal: Install a temperature sensor and read the values into the application.
Joseph Coburn1 
(1)
Alford, UK
 

Every car needs a temperature sensor. Knowing what the outside temperature is useful for all kinds of purposes. At the basic level, it’s useful to know if you should wear a coat and a hat because it’s cold, or to leave your coat in the car because it’s warm out. On a functional level, it’s dangerous to drive in low temperatures, with the risk of icy roads impacting your driving style. By knowing that it’s cold outside, you can adjust your driving accordingly. This project is an excellent basis for expansion – you could use it to install additional sensors for your cabin or engine, for example.

Hardware Configuration

Always turn off and disconnect the Pi’s power supply when working with electronic circuits. While the low voltage used for these circuits will not kill you and is unlikely to seriously injure you, it’s possible to damage both the components and the Pi. It’s also possible to start a fire – either by connecting a component wrong or creating a short-circuit, which causes a part to overheat and subsequently ignites any nearby fuel sources, such as paper, or the box the Pi came in. Always double-check your circuits before applying power and never risk an accident by taking chances.
../images/488914_1_En_7_Chapter/488914_1_En_7_Fig1_HTML.jpg
Figure 7-1

DS18B20 temperature sensor wired into the Pi

To sense the temperature, you need to connect the Pi to a temperature sensor – illustrated in Figure 7-1. The Dallas DS18B20 is perfectly suited for this. It’s tiny, low-power, and affordable. This sensor can vary in price, depending on where you purchase it from. It’s likely to cost a few cents from an electronic components retailer up to several dollars from Amazon or a big-box store. If purchasing components individually, you’ll need to also buy a 4.7k ohm resistor. I’d recommend you buy a complete unit consisting of a DS18B20, 4.7k resistor, and integrated LED. Such a circuit will greatly simplify your build, and costs just a few dollars more. Models such as the ARCELI DS18B20 or the DollaTek DS18B20 are all suitable; just make sure that they use the DS18B20 component.
../images/488914_1_En_7_Chapter/488914_1_En_7_Fig2_HTML.jpg
Figure 7-2

DS18B20 temperature sensor module

The DS18B20 is a digital thermometer, shown in Figure 7-2. It can sense temperatures from -55°C to +125°C within an accuracy of 0.5°C. It only requires one connecting cable between it and the Pi – besides power. It’s also able to share this single data connection with multiple other units, allowing potentially thousands of different temperature sensors to connect to the Pi over one cable – although this project only requires one. This interface of communication over one wire is called 1-wire . By default, the Pi’s physical pin seven (GPIO4) handles this communication.

Begin by identifying the DS18B20. Notice how one side is curved, while the other has a flat spot. Each of the three legs serves a different function, so make sure to connect everything correctly. Work with the flat spot facing you. From left to right, these legs are as follows:
  • Ground (GND) – This is the negative side, or 0 volts.

  • Signal – This is where the Pi connects to retrieve the temperature readings.

  • Live – This is where your main power input comes in, either 3.3V or 5V.

Look at the illustrated Pi drawing to see the completed circuit. If you’re not sure how to read this, trace each wire back from its source, and copy it to link your two components together. Note that this diagram uses the Pi 3, which has a functionally identical pin layout to the Pi 4.

The Pi’s pins are numbered in two different ways – by their layout and by their function. Pins are arranged in two rows of 20 and zigzag left to right. The top-left pin is number one, the top-right pin is number two, the pin one down from the top left is number three, and so on. Pins referenced this way are using their physical or board location. The more popular alternative references pins by their GPIO purpose. As many pins serve specialist purposes, it makes sense to reference the main ones you need to use. This order may appear a little jumbled up, but it greatly emphasizes the GPIO pins over any others. This pin referencing style is called the Broadcom layout, or BCM for short.

To identify the general-purpose input/output (GPIO) pins on your Pi, connect over SSH and run the pinout command; the output of which is shown in Figure 7-3:
pinout
../images/488914_1_En_7_Chapter/488914_1_En_7_Fig3_HTML.jpg
Figure 7-3

Pi 4 GPIO pinout output of the pinout command

This command tells you the exact purpose of every GPIO pin on your Pi – they vary slightly between older Pi models. Looking at the Pi 4 from above, with the USB ports at the bottom and the GPIO pins on the top right, the three pins you need for this project are as follows:
  • 1 (physical) or 3v3 power (BCM) – 3.3V

  • 6 (physical) or ground (BCM) – Ground or GND

  • 7 (physical) or 4 (BCM) – GPIO4

The 3.3V and GND pins provide the electricity the sensor needs to function. GPIO4 is a general-purpose data connection, which the Pi can use to communicate with the outside world. Here’s where you’ll connect the middle leg of your sensor.

Connect the DS18B20 to your Pi via the 4.7k ohm resistor. This resistor serves a very important function. It prevents the sensor or the Pi from creating a short-circuit and getting destroyed, and it’s called a pull-up resistor. If connected to the ground instead of the positive voltage, its name changes to a pull-down resistor. Figure 7-4 shows the wiring diagram between the Pi, a breadboard, and the DS18B20 temperature sensor.
../images/488914_1_En_7_Chapter/488914_1_En_7_Fig4_HTML.jpg
Figure 7-4

Breadboard wiring diagram for the temperature sensor

It also ensures that the readings from the sensor are accurate and not muddled up with spurious readings. It helps to ensure the Pi can tell the difference between the real sensor values and noise from the environment. This noise isn’t audible; it’s interference from all around you – mains power, other data signals, your wireless network, or various other things that exist to interfere with your temperature reading. The Pi has built-in resistors on every GPIO pin, but this sensor needs a slightly different value to operate correctly.

Once connected, go back and double-check your work before applying power.

Pi Configuration

Open an SSH connection to your Pi. To begin using the Pi’s GPIO pin with the temperature sensor, you need to configure it. The config.txt file located inside the /boot/ folder configures several aspects of the Pi. HDMI resolution, video quality, pin settings, and more are handled here. Begin by opening this file in your command-line editor of choice:
sudo nano /boot/config.txt
Add this line at the bottom of the file, taking care to keep all remaining values:
dtoverlay=w1-gpio
Save and exist, and then restart the Pi:
sudo reboot

Once restarted, the Pi will have access to its GPIO pins using the 1-wire protocol. This config option is a device tree overlay . It’s a bit like a driver in Microsoft Windows systems. It lets the Pi’s Linux kernel have access to many different hardware configurations, without getting into the complex guts of operating system logic required to support such a variety of hardware.

Flask Temperature Sensor Logic

Back on your computer, you need to instruct the Pi (through Python) to read the temperature values from the sensor. For this section of the project, you’ll create a Sensors class, which will handle all current and future communication with the Pi’s sensors. You’ll also learn about blueprints. This Flask pattern lets you store your application code outside of the create_app function.

It’s possible to write 100% of the code to read these values, but it’s much easier to use an open source library to handle the heavy lifting. While it’s not too difficult to handle this yourself, it detracts from the main focus of this project, and it isn’t as simple as you’d imagine. To facilitate this process, the Python package W1ThermSensor (https://github.com/timofurrer/w1thermsensor) needs installing. This works on both the Pi and your desktop computer.

From inside a pipenv shell , install W1ThermSensor and regenerate the Pipfile.lock:
pipenv install W1ThermSensor
pipenv lock
Create a folder to store your blueprints, along with a new blueprint file called data. This should live inside your Pi_Car directory:
touch Pi_Car/data.py
This data.py file will contain your main Flask route. It will collate the date from all the sensors, and serve it up, ready for the application to consume later on. Create the sensors file, which will house your Sensors class:
touch Pi_Car/sensors.py
Here’s the code you need for your sensors file:
from flask import current_app as app
try:
    from w1thermsensor import W1ThermSensor
except Exception:
    W1ThermSensor = None
class Sensors:
    @staticmethod
    def get_external_temp():
        """
        Safely read the external temperature
        :return: Integer of current temperature
        """
        app.logger.info("Starting to read temperature sensor")
        try:
            sensor = W1ThermSensor()
            temperature = sensor.get_temperature()
        except TypeError as e:
            app.logger.warning(
                f"Unable to use primary temperature sensor in this environment: {e}"
            )
            temperature = 0
        except Exception as e:
            app.logger.error(
                f"Unknown problem with primary external temperature sensor: {e}"
            )
            temperature = 0
        app.logger.info("Finished reading temperature sensor")
        app.logger.debug(f"Temperature: {temperature}")
        return int(temperature)
That may look like a lot of code, but much of it should be familiar by now. Notice the copious use of logging, at various different levels. There are only three lines needed to read a temperature – the library does much of the work for you:
from w1thermsensor import W1ThermSensor
sensor = W1ThermSensor()
temperature = sensor.get_temperature()

Begin by importing the library into your code. Create a new sensor object, based on the W1ThermSensor class. Finally, use the get_temperature function to read the current temperature.

The rest of this logic is logging, and defensive handling. All of this ensures the Pi can keep on working even if the temperature sensor is removed. If you’re driving a car, it wouldn’t make sense for the radio to break if the temperature sensor fails, and the same is true here. If this logic is allowed to fail, the Pi cannot continue its work with all the other (possibly more important) sensors. Notice how the temperature variable is set to zero if any errors occur. This ensures that the temperature sensor can fail entirely, and you’d only lose this data. No other aspect of the system is impacted, which is how code should be designed almost all of the time.

Notice the exception handling around the library import:
try:
    from w1thermsensor import W1ThermSensor
except Exception:
    W1ThermSensor = None

Generally speaking, you should try to avoid generic exception handling. This code handles any error, rather than certain specific errors. It’s a bit of a brutish approach, but it works in this use case because of one specific flaw in the library. By performing this import, the W1ThermSensor library begins to run its code. Just importing it is enough for it to configure the device to read the sensor. This sounds like a good thing, but it’s another anti-pattern. In Python and object-oriented programming languages, it’s good to explicitly run your logic. Running code on import is an implicit action – and you may not even know it’s happening.

It’s not possible to log this failure at the point of import. The Flask logger is not accessible until you are in a class – imports get evaluated before the app boots, so Flask hasn’t started configuring the app or the logger when this code executes.

In this case, this import fails when running locally on your computer. It only works on the Pi or other devices which support the 1-wire protocol. This library provides custom exceptions you can handle, but again, just importing these runs the config code, and fails again.

By setting W1ThermSensor to None upon error, you can safely run this module on devices other than the Pi – such as your computer. Later on, the exception handling around the temperature object creation also handles this:
except TypeError:
    app.logger.warning("Unable to use primary temperature sensor in this environment")
     temperature = 0

Trying to access the library code when it has failed and been set to None raises a TypeError, which is safely handled (and logged appropriately) here.

This library does provide the ability to prevent this behavior, but in my opinion, it just pushes the same problem to a different part of your code. This library is still one of the best for this purpose, and this minor issue isn’t enough to completely derail the experience. It makes unit testing more difficult, but you’ll notice there are no tests for this portion of the code. While you could technically write some tests, they would deliver very little value, as most of the work is handled by the library. In an ideal world, you would write these tests, but software development is often performed in an imperfect environment, and as such, the time to implement tests on this code (while considering the workarounds needed with the imports) outweighs the benefits.

Finally, the value of temperature is cast to an integer using the int function – which is part of Python’s core library. This ensures that it is always a number and rounds it to the nearest whole number. How many cars have you seen which display the precise temperature, to several decimal places? It’s not necessary, and as the sensor is only accurate to within half a degree anyway, you’re not losing critical information.

Inside data.py, here’s the code you need:
from flask import Blueprint, jsonify
from .sensors import Sensors
from flask import current_app as app
data_blueprint = Blueprint("data", __name__)
@data_blueprint.route("/")
def show():
    app.logger.info("Starting to retrieve core data")
    temperature = Sensors.get_external_temp()
    result = {"temperature": temperature}
    app.logger.info("Finished retrieving core data")
    app.logger.debug(f"Core data: {result}")
    return jsonify(result)

There are very few lines here. Much of it should be familiar from the “Hello, world!” Flask route you created inside app.py.

Let’s start with this import:
from flask import current_app as app

Inside Flask, you can access the running app through current_app , which is aliased to the shorter app. This lets you access your configs, your logger object, and anything else configured by your create_app function. This app object is used to write the application logs in this file, which route to the correct log handlers.

Blueprints are Flask’s way of letting you move application routes outside of your create_app and into their own individual files. By creating an instance of the Blueprint class called data_blueprint, you’re building everything Flask needs to know about – using the decorator to tell Flask that everything inside your function is part of the data blueprint:
@data_blueprint.route("/")
This forward slash is your URL route. It can be anything you like – it’s what you’ll type in your browser’s URL bar. Note the call to your Sensor class:
temperature = Sensors.get_external_temp()

You defined this function as a static method. It requires no access to any other parts of the class, which means you don’t need to define an instance of the class first, you can just access the object directly.

Finally, the data is returned as JSON using the jsonify function. This converts it from a Python dictionary object to a JSON object – which is essentially a string. Flask only allows specific shape objects or strings as valid return types from routes, as it has to render these in the browser.

Back inside your app.py, you need to tell Flask about your new route. Begin by importing it at the top of the file:
from .data import data_blueprint
Now remove your “Hello, World!” route from inside create_app, and replace it with a blueprint registration:
app.register_blueprint(data_blueprint)
This is functionally equivalent to creating routes inside create_app, but it keeps your logic very tidy, and compartmentalized into their own files, each with a specific purpose. This is what your complete app.py file now looks like:
import logging
from flask import Flask
from logging.handlers import RotatingFileHandler
from .data import data_blueprint
def create_app(config_file="config/local_config.py"):
    app = Flask(__name__)  # Initialize app
    app.config.from_pyfile(config_file, silent=False)  # Read in config from file
    # Configure file-based log handler
    log_file_handler = RotatingFileHandler(
        filename=app.config.get("LOG_FILE_NAME", "config/pi-car.log"),
        maxBytes=10000000,
        backupCount=4,
    )
    log_file_handler.setLevel(app.config.get("LOGGER_LEVEL", "ERROR"))
    log_file_handler.setFormatter(
        logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
    )
    app.logger.addHandler(log_file_handler)
    app.logger.info("----- STARTING APP ------")
    app.register_blueprint(data_blueprint)
    app.logger.info("----- FINISHED STARTING APP -----")
    return app
Start Flask and load up your route in your web browser of choice. Notice how your logs fill up with warnings, and there is no temperature displayed. With no temperature sensor on your computer, it’s impossible to read the temperature! The code is working perfectly – it continues to run just fine without access to the temperature sensor. Commit your code and perform a build on the Pi by restarting.
../images/488914_1_En_7_Chapter/488914_1_En_7_Fig5_HTML.jpg
Figure 7-5

Application logs for the temperature sensor

Visit your Pi’s URL, and bask in the overwhelming glory that is your network-connected temperature sensor. The sample application logs are shown in Figure 7-5, and Figure 7-6 shows the JSON output served to your web browser. It may not look pretty, but it’s perfectly functional. Later on in this book, you’ll learn how to make all the data you’re collecting look much nicer, but for now, all you need to care about is functionality. I’ll never forget a phrase I was told in my first programming job: “Make it work, and then make it look pretty. It’s all well and good having flashy lights and bright colors, but if it’s core function doesn’t work, nobody can use your code.” It’s stuck with me ever since.
../images/488914_1_En_7_Chapter/488914_1_En_7_Fig6_HTML.jpg
Figure 7-6

JSON output of temperature sensor

Finish testing your sensor by warming it up – either with your breath or holding it (briefly) near a radiator or other heat source. Reload your app to see the increased temperature displayed.

If you’re struggling to see these changes on your Pi, then open up your application logs, visible in /home/pi/Documents/Pi-Car/pi-car.log. What do you notice about them? Are there any errors or stack traces visible? If so, go back and double-check your code. Test it first by running it on your computer. You may need to change your logging level to something more verbose first. Check your sensor is installed correctly (making sure to shut down and unplug the Pi first).

Chapter Summary

In this chapter you learned how to connect a digital temperature sensor module to the Pi and defensively program your application to read this data and expose it to the network through your Flask server. You also learned how to write your code such that it can handle a total sensor failure – either through incorrect wiring, a broken sensor, or any number of other hardware faults that could prevent the sensor from working.

In the next chapter, you’ll continue to expand the system by building a boot sensor.

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

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