In this assignment, you will wire three sensors to your Raspberry Pi, to sense light, motion and temperature. You will write code to collect this data, visualise it and trigger actions based on basic data processing.



In this assignment, we will make our prototype more aware of its context, with the use of sensor data, Bucket, and some simple data processing.

Step 1 Organise the Development and Test Workflow

Throughout the course, we use the Raspberry Pi as a home hub. We execute our Python scripts on it. In contrast with our personal computer, we can leave it run the whole week so that we can experience the functionality that we developed.

However, there are some functionalities that we can only test on the Raspberry Pi. Collecting data from sensors is one of them, as we need to connect the wires. In this step, we suggest a workflow to continuously code on your machine (where it is convenient) and test on the Raspberry Pi (where the sensors are). The cycle goes like this:

Task 1.1 Create a Branch

Back to your repository in VS Code, do not forget to pull the latest version and start up your python virtual environment.

First, we create a Git branch. In VS Code, click on your Git branch in the bottom left corner. Then, select Create a new branch from . It will prompt you for the name of your new branch (e.g. explore-sensor-jacky ) as well as the branch you want to start (e.g. master ).

Alternatively, you can open the Terminal and type in the following git checkout command in which ‘-b’ stand for ‘new branch’.

git checkout -b explore-sensor-jacky master

Task 1.2 Get the code on Raspberry Pi

Open a second Terminal, so that you can use the Terminal on your machine as well as on the Raspberry Pi.

In the new Terminal, use ssh to connect to you Raspberry Pi.

As we use Git for the first time, we can set the basic user information. For this, type in the three following commands, replacing with your name and email address. The third one allows you to store your credential so that you do not need to type then every time you pull or push.

git config --global user.name "Jon Doe"
git config --global user.email "jon@example.com"
git config --global credential.helper store

Then, we want to get our code from GitHub. We use the command git clone with the URL of our repository. We find the URL of our repository on its first page, on GitHub, when we click on the green button ‘Code’.

git clone https://github.com/id5415/id5415-project-demo-team.git

It should create a folder with the name of our repository. We can enter this repository with cd . By default, we are on the master branch. we can confirm this with the command git status As we want to use the code from our new branch, we switch with checkout. Replace the name of the branch by yours. Note this time there is no ‘-b’. We do not want to create a new branch, simply switch to an existing one.

git checkout explore-sensor-jacky

Task 1.3 Set up the Python env

Before executing our code, we need to set up the element which we do not get from GitHub: the virtual environment and the environment variables.

For the virtual environment, you are now used to the following 2 commands to create and activate it.

virtualenv venv
source ./venv/bin/activate

Now, because our lightbulb Thing on Bucket is secured with a private key, we need to copy the private key of the lightbulb from our laptop to the Raspberry Pi. We can do that with the scp command we used in the first lab experiment. This command is copying files remotely, from our project directory where private.pem is stored to the Raspberry Pi directory:

Windows To use the scp command in Windows you first need to download & install the SCP client software SCP Client for Windows x64.

scp private.pem [username]@[hostname].local:~/PATH_TO_YOUR_PROJECT_FOLDER/ #It will copy the file to your project directory on Pi

Then, we need to specify the thing id and private key of our Raspberry Pi, like we did for the light bulb in the previous lab experiment. For this, we create a file .env , using the command line text editor nano (think of it like TextEdit on Mac or NotePad on Windows).

nano .env

Enter the following two lines, replacing the thing id by the thing id our your Raspberry Pi (on both lines).

LIGHTBULB_THING_ID=YOUR_LIGHTBULB_THING_ID
PRIVATE_KEY_PATH=PATH_TO_YOUR_PROJECT_FOLDER/private.pem
LIGHTBULB_IP_ADDRESS=IP_ADDRESS_OF_BULB
LOG_LEVEL=INFO

Note You can also copy the .env file from your machine to the pi as similarly we did with private.pem. However, you need to change the private_key_path. To exit, press CTRL + x . It prompts you to save, type in y and ENTER to say ‘yes’.

We are now set up: we have a branch on which we can continuously push from our machine and pull from the Raspberry Pi to execute.

Once we are happy with our code, the development and test cycle is completed. We will merge this branch into the master branch and delete it. We will walk through these steps at the end of this assignment.

Step 2 Set up the Sensors

Starting on the hardware side, we want to connect sensors to the Raspberry Pi. The Raspberry Pi can interact with peripherals (such as our sensors) through its General Purpose Input/Output pins (GPIO). Here is an overview of these pins:

Overview of the Raspberry Pi's GPIO

Careful, the number of each physical pin on the Raspberry Pi is different from the GPIO number in the code. For example, the physical pin 15 is GPIO22 in the code. As you use different libraries, be careful which nomenclature you are using. For now:

Task 2.1 Wire the Sensor to the Raspberry Pi

As part of the prototyping kit, we have two sensors:

  1. DHT11 (for Digital Humidity and Temperature) - this will be our temperature and relative humidity sensor;
  2. LDR (for Light Dependent Resistor) - this will be our light sensor!

Here we show you a way to wire these sensors to the Raspberry Pi. However, you can use any general GPIO pin for these connections.

For the LDR, you will need a 1uF capacitor and some wires (both provided in the kit). You can connect the circuit to the Raspberry Pi as follows (GPIO 18):

LDR / Raspberry Pi wiring

For the DHT11, you will need a 10kΩ resistor and some wires (both provided in the kit). You can connect the circuit to the Raspberry Pi as follows (GPIO 4):

DHT11 / Raspberry Pi wiring

Task 2.2 Install Python packages for GPIO

Once we have our wiring, we can switch back to the code! There is a list of packages that we need to install to use the GPIO from Python. On the Raspberry Pi with virtual environment activated, type in the following commands:

# GPIO library
pip install RPI.GPIO

# GPIO zero library for LDR
pip install gpiozero

# major library to interface with circuit python libraries
pip install adafruit-blinka

# library for our dht sensor
pip install adafruit-circuitpython-dht

pip install dcd-sdk

# necessary system dependency
sudo apt-get install libgpiod2

Note: we install the last dependency with apt-get instead of pip . apt-get is the package manager (like pip ) of the Raspberry Pi operating system. We install a library for the Raspberry Pi which is required to use the GPIO with Python. The sudo command tells the Raspberry Pi to execute the script with administrator privilege. It will prompt you to enter the password of your Raspberry Pi.

Task 2.3 Import Sensor Packages

Back on our machine, we create a new Python file src/sensing.py . In this script, we write Python code to explore the sensor data collection without disturbing light.py .

We must first import the packages we installed:

import board # for our board pins
# import DHT sensor library
from adafruit_dht import DHT11
# import light sensor from GPIO 0
from gpiozero import LightSensor # class for ldr connection

Note: Here you will probably see an ‘import error’ withe libraries DHT11, board… ignore this error for now. The reason for this error is that the libraries are installed in pi and not in the machine.

Task 2.4 Initialise a Sensor Object for each Sensor

Each sensor will have a sensor object (as defined in the self-study material) through which you can collect data and control the sensor specifications. Let’s create one for each of these:

# suing gpio pin 4
dht_sensor = DHT11(board.D4)
# defining our Light sensor object using GPIO 18
LDR_PIN = 18
ldr_sensor = LightSensor(LDR_PIN)

We are now ready to collect some data! We will be using these classes to retrieve data and visualize it.

Task 2.5 Read raw test sensor data in your script

As a test, let’s read the values of our sensors (after we have set our sensor objects) and print them out in the Terminal. To do this, we use print() three times, together with each of the three following statements for the measurements.

python src/sensing.py

Step 3 Structure data Collection into a Class

At this stage, we should have working sensors: an output of all three values every time we execute our Python script. Now we need to structure our data collection, do some basic processing, and send this data to Bucket.

Task 3.1 Class Structure

Let’s use this opportunity to define a class , as presented in the self-study material. This class will handle the data collection for our three sensors, send the data to Bucket and get us ready to trigger action.

Copy and paste the following structure in a new file src/sensor_data_collector.py .

# import DHT sensor library
from adafruit_dht import DHT11
# import light sensor from GPIO 0
from gpiozero import LightSensor  # class for ldr connection
import threading # so we can collect data every x seconds

class SensorDataCollector:

  def __init__(self, dht_pin, ldr_pin, thing, collection_frequency=10):

  def collect(self):


  def update_temperature(self):


  def update_humidity(self):


  def update_light(self):

In this structure, you can recognise our package imports at the top. We add the new package threading which will enable you to collect data at regular interval.

Then we have the skeleton of our class:

Note the self keywords is a parameter of each method, which points to the instance of the class itself. We can use it to access any variable we store inside the class: the so-called attributes .

Task 3.2 Construct the Object

The constructor of the class is initialising the object. Thus, we provide the pin of our two sensors, as well as the collection_frequence (how often we will read the sensors).

def __init__(self, dht_pin, ldr_pin,  collection_frequency=10):
  # Store the collection frequency as attribute
  self.collection_frequency = collection_frequency
  # Store the access to the 2 sensors
  self.dht_sensor = DHT11(dht_pin)
  self.ldr_sensor = LightSensor(ldr_pin)
  # Store the last value of each sensor (currently 'None' as we do not know there values yet)
  self.temperature = None
  self.humidity = None
  self.light = None

Note we use self , referring to the object itself, to initialise the attribute of this object.

Task 3.3 Collect the Light

def collect(self):
  # Set a timer to automatically call this function again
  threading.Timer(self.collection_frequency, self.collect).start()
  # Call the three sensor methods
  self.update_temperature()
  self.update_humidity()
  self.update_light()

Task 3.4 Define the Light Method

Each of the three methods which collect data from sensors will looks similar. In essence: collecting the value as we did in Step 2. However, Input/output function are prone to errors (e.g. a sensor not available, failing to respond). We will use the try-catch statement introduced in the self-study material to handle these cases when the reading data fails. Let’s use update_light() as an example:

def update_light(self):
  try:
    # A value between 0 (dark) and 1 (light)
    self.light = self.ldr_sensor.value
    print(self.light)
    sleep(1)
  except RuntimeError as error:
    # LDR Errors
    print(error.args[0]) # specify the problem
  except Exception as error:
    # this means there is a problem with the actual sensor
    raise error

Task 3.5 Write the Humidity and Light Functions

Repeat the same process to create the methods update_temperature() and update_humidity() .

Task 3.6 Main

The final step is a function main that create an object of the class SensorDataCollector to collect data. Let’s create a file src/main.py as follows.

import board  # for our board pins
from sensor_data_collector import SensorDataCollector

LDR_PIN = 18
DHT_PIN = board.D4

COLLECTION_FREQUENCY = 5  # in seconds

def main():
    collector = SensorDataCollector(DHT_PIN, LDR_PIN, COLLECTION_FREQUENCY)
    collector.collect()

main()

You will recognise the import of our class at the top, followed by the specification of our sensor pin and collection frequency.

main() is building an object of our class SensorDataCollector and call the collect() method on this object (initiating the data collection). We do not forget to call main() at the bottom of our script.

Our script is ready for testing. Let’s commit and push on GitHub, so that we can pull on the Raspberry Pi and execute.

python src/main.py

Step 4 Send Sensor Data to Bucket

In this step, we want to upgrade our class to automatically send data to Bucket.

Task 4.1 Initialise the Thing and Properties

We want to create a Thing object like we did in the previous lab experiment. In src/main.py , we can import the definition of a Thing and call its constructor Thing() in main() . Then, we pass this object to the constructor of SensorDataCollector()

from dcd.bucket.thing import Thing

def main():
  rpi_thing = Thing()
  collector = SensorDataCollector(DHT_PIN, LDR_PIN, rpi_thing, COLLECTION_FREQUENCY)
  collector.collect()

Then, we update the constructor of SensorDataCollector to take a new parameter (our thing)

def __init__(self, dht_pin, ldr_pin, thing, collection_frequency=10):

Inside the constructor, we can now store the thing and ‘find or create’ three property: one for each of our sensors.

  self.rpi_thing = thing
  # Find or create a property to store light
  self.light_property = self.rpi_thing.find_or_create_property(
      "LDR sensor", "LIGHT")
  # Find or create a property to store temperature
  self.temp_property = self.rpi_thing.find_or_create_property(
      "DHT Temperature", "TEMPERATURE")
  # Find or create a property to store humidity
  self.humidity_property = self.rpi_thing.find_or_create_property(
      "DHT Humidity", "RELATIVE_HUMIDITY")

Task 4.2 Send property data

At this stage, sending data to Bucket is only a step away: Our Thing is initialised, we have a property got each sensor. We now use these properties to send data each time we collect a new value.

Here is an example for the in update_light() . You note the extra line? This is where we update the values of our light property. Note the double parenthesis and the trailing comma, both important to tell Python that we have only one value, but we still want it to be part of a list (of 1 element).

def update_light(self):
  try:
      self.light = self.ldr_sensor.value  # between 0 (dark) and 1 (light)
      print(self.light)
      self.light_property.update_values((self.light,))
      sleep(1)
  except RuntimeError as error:
      print(error.args[0])
  except Exception as error:
      raise error

Go ahead and add a similar line in update_temperature() and update_humidity() to send all 3 values to Bucket.

Our script is ready for testing. Let’s commit and push on GitHub, so that we can pull on the Raspberry Pi and execute.

python src/main.py

Step 5 Events and Actions

As the last step, we want to trigger actions based on the data we collect. For now, we define the most basic action: showing the data in the Terminal. This action looks like this:

def action(values):
    print('ready for action')
    print(values)

The parameter values gives us the last values of our three sensors as they have just been collected.

Task 5.1 Define a Handler

We do not want to mix collection and actions. It is important to separate these two aspects so that we can reuse our code. Thus, we paste the actions() in the main.py and we tell our collector to use it as a ‘handler’. A handler is a method that we define to ‘handle’ something. In our case, to handle the action based on the new data.

def main():
  rpi_thing = Thing()
  collector = SensorDataCollector(DHT_PIN, LDR_PIN, rpi_thing, COLLECTION_FREQUENCY)
  collector.collect()
  collector.setHandler(actions)

def actions(values):
  print('ready for actions')
  print(values)

Task 5.2 Set a Handler

What is this setHandler() about? This is on us to define it. What we want is to store this function, so that we can call it whenever there is new data. Thus, we add the method setHandler() to our class SensorDataCollector .

def setHandler(self, handler):
  self.handler = handler

Because all attributes of an object need to be define in the constructor, we add the following at the bottom of the constructor: there might be a handler, but for now there is none!

self.handler = None

Task 5.3 Call a Handler

Finally, we want to trigger our actions() function each time we collect new data. The collect() function seem to be the right place for this, as it is call ‘every time we collect new data’. Note that we check first if a handler has been set before trying to call it.

if self.handler != None:
  self.handler({
      'temperature': self.temperature,
      'humidity': self.humidity,
      'light': self.light
  })

Task 5.4 An example of Action

Let’s define a simple event and action pair: we want to detect when the room lights gets turned ON/OFF.

So what do we have to do? Every time we get a new time value, we need to see if its value has crossed the threshold.

Lets make a is_light_on() function that we call in actions() . We get our new light value (to check our threshold, you should make a global variable (maybe after the sensor pin definition) to hold the previous light value:

def is_light_on(values, threshold = 0.1):
  new_value = values["light"] # get the light value
  # our threshold by default is 0.1 but you may need to adjust this
  global prev_value # you need this to
  if(new_value > threshold  and  prev_value < threshold): # we crossed threshold
    print("Light switch has been flipped on")

  prev_value = new_value # updating our previous value at the end

Note that you can call is_light_on like so: is_light_on(values) because the threshold by default is 0.1. If you want to specify it, you can do so as well: is_light_on(values, new_threshold).

Task 5.5 Your turn

From this, can you create another action function

In the following lab experiment, we will connect these actions to the lightbulb and test them.

Merge and Push Once you are done with your development and test cycle, do not forget to merge your branch into your master branch.

Updating the CHANGELOG file In this assignment you have made significant additions to your prototype. Edit the file CHANGELOG.md and add what you have achieved in this assignment.

Sensors and time-series
Prototyping Connected Product - Self-Study 3

Maintain, Connect and Structure Code
Prototyping Connected Products - Lab Experiment 3

TU Delft IoT Rapid-Proto Labs Erasmus +

The European Commission's support for the production of this publication does not constitute an endorsement of the contents, which reflect the views only of the authors, and the Commission cannot be held responsible for any use which may be made of the information contained therein.