Author | Nejat Hakan |
nejat.hakan@outlook.de | |
PayPal Me | https://paypal.me/nejathakan |
Raspberry Pi workshop - Mini Weather Station
Introduction
Welcome to the Raspberry Pi Mini Weather Station workshop! This comprehensive guide is designed for university students and enthusiasts eager to dive into the world of embedded systems, IoT (Internet of Things), and practical programming using the versatile Raspberry Pi. Over the course of this workshop, you will not only learn the theoretical underpinnings of the Raspberry Pi and its components but also gain hands-on experience by building a functional Mini Weather Station.
The Raspberry Pi, a low-cost, credit-card-sized computer, has revolutionized DIY electronics and programming education. Its accessibility and power make it an ideal platform for a myriad of projects, from simple LED blinkers to complex home automation systems, and in our case, a device that can sense and record environmental conditions.
This workshop will take you on a journey starting from the very basics of what a Raspberry Pi is, how to set it up, and navigate its Linux-based operating system. We will then explore Python programming, the language of choice for many Raspberry Pi projects, and learn how to interface with external hardware, specifically sensors that measure temperature, humidity, and atmospheric pressure. You'll learn how to read data from these sensors, store it systematically, and even automate the data collection process.
A significant portion of this guide is dedicated to "Workshop" sections. These are practical, step-by-step tutorials designed to reinforce the concepts discussed in each theoretical part. By following these workshops, you will incrementally build your Mini Weather Station, applying your newfound knowledge in a real-world context.
We will also delve into important aspects like disk preparation architectures, understanding file systems relevant to the Raspberry Pi, and strategies for ensuring the longevity and reliability of your SD card-based system. This includes discussions on file systems like ext4 and FAT32, and techniques such as implementing read-only root filesystems or offloading write-intensive tasks.
By the end of this workshop, you will have:
- A solid understanding of Raspberry Pi hardware and software.
- Proficiency in basic Linux command-line operations.
- The ability to write Python scripts for hardware interaction.
- Experience in connecting and reading data from environmental sensors.
- Skills in data logging and storage.
- A fully functional Mini Weather Station built by you!
This project is more than just an academic exercise; it's a stepping stone into the exciting fields of IoT, data science, and embedded engineering. The skills you acquire here will be transferable to a wide range of future projects and professional endeavors. So, let's get started on this exciting learning adventure!
1. Getting Started with Your Raspberry Pi
Before we can build our weather station, it's crucial to understand the core component we'll be using: the Raspberry Pi. This section will introduce you to the Raspberry Pi, its various models, and the essential peripherals you'll need to get up and running. Familiarizing yourself with the hardware is the first step towards mastering its capabilities.
What is a Raspberry Pi?
The Raspberry Pi is a series of small single-board computers (SBCs) developed in the United Kingdom by the Raspberry Pi Foundation. The original goal was to promote the teaching of basic computer science in schools and in developing countries. However, due to its low cost, modularity, and open design, it quickly became popular with hobbyists, makers, and even for industrial applications.
At its heart, a Raspberry Pi is a fully functional computer. It has a processor (CPU), memory (RAM), graphics processing capabilities, input/output ports (like USB and HDMI), network connectivity (Ethernet and/or Wi-Fi), and, most importantly for our project, a set of General Purpose Input/Output (GPIO) pins. These GPIO pins allow the Raspberry Pi to interact with the physical world by connecting to sensors, motors, LEDs, and other electronic components.
Unlike a typical desktop or laptop computer, the Raspberry Pi does not come with built-in storage for the operating system or user files. Instead, it uses a microSD card as its "hard drive." This makes it easy to swap out operating systems or start fresh if something goes wrong. It typically runs Linux-based operating systems, with Raspberry Pi OS (formerly Raspbian) being the officially supported option.
The Raspberry Pi's versatility stems from its ability to run a full operating system, allowing users to perform tasks ranging from web browsing and document editing to complex computations and, crucially for us, interfacing with hardware to read sensor data, control actuators, and communicate over networks.
Raspberry Pi Models and Specifications
Since its launch in 2012, numerous Raspberry Pi models have been released, each offering different features, performance levels, and form factors. For our Mini Weather Station project, most recent models will suffice, but understanding the differences can be helpful.
Here's a brief overview of some key models and their evolution:
- Raspberry Pi 1 Model A/B/A+/B+:
These were the initial models. The Model B was the first mainstream version, featuring an ARM11 processor, 256MB or 512MB RAM, USB ports, HDMI, and Ethernet (on Model B). The A/A+ models were lower-cost versions with fewer ports and less RAM. - Raspberry Pi 2 Model B:
A significant upgrade, featuring a quad-core ARM Cortex-A7 processor and 1GB of RAM. This model offered a substantial performance boost over its predecessors. - Raspberry Pi 3 Model B/B+/A+:
Introduced a more powerful quad-core ARM Cortex-A53 (64-bit) processor and, crucially, built-in Wi-Fi and Bluetooth. The Model B+ offered improved networking and thermal management. This generation is a good baseline for many projects. - Raspberry Pi 4 Model B:
A major leap in performance, with a quad-core ARM Cortex-A72 processor, and options for 1GB, 2GB, 4GB, or 8GB of RAM. It also features USB 3.0 ports, dual micro-HDMI outputs supporting 4K resolution, and Gigabit Ethernet. This is currently the most powerful standard model and is excellent for more demanding projects, though it might be overkill for a simple weather station unless you plan extensive data processing or a complex web interface directly on the Pi. - Raspberry Pi Zero/Zero W/Zero WH/Zero 2 W:
These are ultra-small, low-cost versions. The Pi Zero W and WH include Wi-Fi and Bluetooth. The Pi Zero 2 W offers a significant performance upgrade over the original Zero, using a System-in-Package (SiP) based on the Raspberry Pi 3's processor. These are great for embedded projects where space and power are critical, but they have fewer USB ports (requiring a hub for multiple peripherals during setup) and often require soldering headers for GPIO access (unless you get the WH model with pre-soldered headers). - Raspberry Pi Pico/Pico W:
It's important to distinguish the Raspberry Pi Pico from the other Raspberry Pi models. The Pico is a microcontroller, not a single-board computer. It runs MicroPython or C/C++ directly on the chip without an operating system like Linux. While excellent for many embedded tasks, it's a different class of device and not what we'll be using for this particular workshop, which leverages a full OS environment. The Pico W adds Wi-Fi.
For our Mini Weather Station, a Raspberry Pi 3 Model B/B+, a Raspberry Pi 4 Model B, or even a Raspberry Pi Zero 2 W would be suitable. The key requirements are GPIO pins for sensor interfacing and network connectivity (Wi-Fi or Ethernet) if you wish to access it remotely or send data over the internet. Older models like the Pi 2 can also work but might be slower.
Key Specifications to Consider:
- Processor (CPU):
Determines the computational power. More cores and higher clock speeds mean faster processing. - RAM:
Affects multitasking capabilities and the ability to handle larger datasets or more complex software. 1GB is generally sufficient for our project. - GPIO Pins:
All Raspberry Pi models (except Pico, which has them but in a microcontroller context) feature a 40-pin GPIO header. This is essential for connecting sensors. - Connectivity:
- USB Ports:
For keyboard, mouse, and other peripherals. - HDMI Port:
For connecting a display (standard, mini, or micro depending on the model). - Ethernet Port:
For wired network connection. - Wi-Fi/Bluetooth:
For wireless connectivity (built-in on many recent models).
- USB Ports:
- Storage:
All models use a microSD card for the operating system and data storage.
When choosing a model for this workshop, if you already own a Raspberry Pi 3 or 4, it's perfect. If purchasing new, a Raspberry Pi 4 Model B (2GB RAM) offers a good balance of performance and price for future projects as well, though a Pi 3 B+ is also very capable. A Pi Zero 2 W is a good budget-friendly and compact option if you're comfortable with its smaller form factor and potentially needing a USB hub during initial setup.
Essential Peripherals and Accessories
To get your Raspberry Pi up and running, and to build our weather station, you'll need a few essential items:
- Raspberry Pi Board:
The specific model you've chosen (e.g., Raspberry Pi 4 Model B, Raspberry Pi 3 Model B+). - MicroSD Card:
- Capacity:
A minimum of 8GB is recommended, but 16GB or 32GB is better for flexibility and longevity. - Class:
Class 10, U1, or U3 (A1 or A2 rated cards are even better for application performance). A good quality, reasonably fast card is important for overall system responsiveness and reliability. We'll discuss SD card classes in more detail later.
- Capacity:
- Power Supply:
- This is critical. Using an underpowered or poor-quality supply can lead to instability, data corruption, and strange behavior.
- Raspberry Pi 4:
Requires a USB-C power supply, rated for at least 3.0A (e.g., 5V/3A). - Raspberry Pi 3 and earlier models (including Pi Zero/Zero 2 W):
Use a micro-USB power supply, typically rated for 2.5A (e.g., 5V/2.5A). - Always use the official Raspberry Pi power supply or a reputable third-party one designed for your specific Pi model.
- Input Devices (for initial setup):
- USB Keyboard:
Standard USB keyboard. - USB Mouse:
Standard USB mouse. - Once set up, you can often operate the Raspberry Pi "headless" (without a monitor, keyboard, or mouse) via SSH or VNC.
- USB Keyboard:
- Display and Cable (for initial setup):
- Monitor or TV:
Any display with an HDMI input. - HDMI Cable:
- Raspberry Pi 4:
Requires a micro-HDMI to HDMI cable. - Raspberry Pi 1, 2, 3:
Require a standard HDMI to HDMI cable. - Raspberry Pi Zero/Zero 2 W:
Require a mini-HDMI to HDMI adapter or cable.
- Raspberry Pi 4:
- Monitor or TV:
- Computer with SD Card Reader:
You'll need this to write the operating system image to the microSD card. Most laptops have a built-in SD card reader; otherwise, a USB SD card reader will be necessary. - (Optional but Recommended) Case:
Protects your Raspberry Pi from physical damage, dust, and accidental short circuits. Many cases also offer cooling solutions like heatsinks or fans. - (Optional) Ethernet Cable:
If you prefer a wired network connection or if your Pi model doesn't have Wi-Fi (or if Wi-Fi setup is problematic initially). - (For the Weather Station Project) Sensors and Components:
- DHT22 (or AM2302) Sensor:
Measures temperature and humidity. A common and easy-to-use digital sensor. (Alternatively, a DHT11 can be used, but it's less accurate). - BMP280 or BME280 Sensor:
Measures atmospheric pressure and temperature. The BME280 also measures humidity. These typically use the I2C communication protocol. We will focus on the BME280 for a more comprehensive weather station. - Breadboard:
For prototyping and connecting components without soldering. - Jumper Wires:
Male-to-female and male-to-male wires to connect sensors to the Raspberry Pi's GPIO pins and the breadboard. - (Optional) Resistor:
A 4.7kΩ to 10kΩ pull-up resistor is sometimes recommended for the DHT22 data line, though many modules come with one built-in.
- DHT22 (or AM2302) Sensor:
Having these items ready will ensure a smooth setup process and an enjoyable project-building experience.
Workshop Preparing Your Raspberry Pi
This workshop will guide you through the initial unboxing and physical setup of your Raspberry Pi. We are not installing the OS yet; that's in the next section. This is purely about identifying components and making physical connections.
Objective:
To familiarize yourself with the Raspberry Pi board, its ports, and to correctly connect the basic peripherals required for the first boot-up.
Materials Needed:
- Your Raspberry Pi board
- Official power supply for your Pi model
- USB Keyboard
- USB Mouse
- Monitor with HDMI input
- Appropriate HDMI cable for your Pi model (micro-HDMI for Pi 4, mini-HDMI for Pi Zero, standard HDMI for Pi 1/2/3)
- (Optional) Raspberry Pi case
- (Optional) Anti-static wrist strap
Steps:
-
Inspect Your Raspberry Pi:
- Carefully take your Raspberry Pi out of its packaging. Handle it by the edges to avoid touching the components directly and to prevent static discharge. If you have an anti-static wrist strap, it's good practice to use it.
- Identify the main components and ports:
- Processor (SoC):
Usually a square chip, sometimes covered by a heatsink or a metal shield. - RAM Chip:
Often located near the processor. - MicroSD Card Slot:
Typically on the underside of the board. - USB Ports:
For keyboard, mouse, etc. - Ethernet Port:
For wired networking (if your model has one). - HDMI Port(s):
For video output. Note if it's standard, mini, or micro HDMI. - Power Input Port:
Micro-USB or USB-C. - Audio Jack:
(3.5mm) For audio output/composite video on older models. - GPIO Pins:
A double row of 40 pins. These are crucial for our weather station. - Wi-Fi/Bluetooth Module:
(If applicable) Often a small metal-shielded component.
- Processor (SoC):
-
(Optional) Install the Raspberry Pi in its Case:
- If you have a case, now is a good time to install the Pi.
- Most cases snap together or use small screws. Follow the instructions provided with your case.
- Ensure all ports are accessible and that the Pi is securely fitted. Some cases come with heatsinks; if so, apply them to the specified chips (usually CPU, sometimes RAM or USB controller) before closing the case. The heatsinks typically have adhesive backing.
-
Connect the Display:
- Connect one end of your HDMI cable to the HDMI port on your monitor or TV.
- Connect the other end to the appropriate HDMI port on your Raspberry Pi.
- Reminder: Pi 4 uses micro-HDMI, Pi Zero uses mini-HDMI, Pi 1/2/3 use standard HDMI.
- Ensure your monitor is powered on and set to the correct HDMI input source.
-
Connect Input Devices:
- Plug your USB keyboard into one of the USB ports on the Raspberry Pi.
- Plug your USB mouse into another USB port.
- If you are using a Raspberry Pi Zero/Zero 2 W, you might need a USB OTG (On-The-Go) adapter (if it has a micro-USB port for data) and potentially a USB hub to connect both keyboard and mouse, unless you plan to use it headless from the start (which we'll cover later).
-
Prepare the MicroSD Card (Do NOT Insert Yet):
- Take your microSD card. We will prepare it with the operating system in the next section. For now, just have it ready. Do not insert it into the Raspberry Pi yet.
-
Prepare the Power Supply (Do NOT Connect Yet):
- Have your official Raspberry Pi power supply ready. Double-check it's the correct type (USB-C for Pi 4, micro-USB for others) and rating.
- Crucial Note:
The Raspberry Pi does not have a power button. It powers on as soon as you connect the power supply and starts booting from the microSD card (if a valid OS is present). We will connect the power supply last in the next section, after the SD card is prepared and inserted.
-
Review Your Connections:
- Double-check all connections:
- HDMI cable securely connected to both Pi and monitor.
- Keyboard and mouse securely plugged into USB ports.
- Ensure your workspace is clear and there are no conductive materials touching the Raspberry Pi board if it's not in a case.
- Double-check all connections:
You have now physically prepared your Raspberry Pi for its first boot, which we'll perform after installing the operating system on the microSD card. This initial hands-on interaction with the board is important for understanding its physical layout.
2. Setting Up the Operating System
With the Raspberry Pi hardware familiarized and peripherals ready, the next crucial step is to install an operating system (OS) onto the microSD card. The OS is the software that manages the Pi's hardware and resources, allowing you to run applications and interact with the system. This section covers choosing an OS, understanding SD card requirements, detailed disk preparation architectures and file systems, imaging the SD card, and the initial boot-up configuration.
Choosing an Operating System (Raspberry Pi OS)
While the Raspberry Pi can run various operating systems (including Ubuntu, OSMC for media centers, RetroPie for gaming), the officially supported and most commonly used OS is Raspberry Pi OS (formerly known as Raspbian). Raspberry Pi OS is a derivative of Debian Linux, optimized specifically for Raspberry Pi hardware.
Why Raspberry Pi OS?
- Optimization:
Tailored for the Raspberry Pi's ARM architecture, ensuring good performance and stability. - Hardware Support:
Includes drivers and firmware for all Raspberry Pi hardware components, including GPIOs, camera modules, and display interfaces. - Pre-installed Software:
Comes with a suite of useful software, including programming tools (Python, Scratch, Java, C/C++), office applications (LibreOffice), a web browser (Chromium), and system configuration utilities. - Large Community Support:
Being the official OS, it has a vast and active user community, making it easy to find tutorials, solutions to problems, and project ideas. - Regular Updates:
The Raspberry Pi Foundation actively maintains and updates Raspberry Pi OS with new features, security patches, and performance improvements.
Versions of Raspberry Pi OS:
Raspberry Pi OS is typically available in a few versions:
- Raspberry Pi OS with desktop and recommended software:
The full version, including the graphical desktop environment (LXDE-based, now PIXEL) and a comprehensive set of pre-installed applications. This is ideal for beginners or those who want a complete desktop experience. It requires a larger SD card (at least 8GB, 16GB recommended). - Raspberry Pi OS with desktop:
Includes the graphical desktop environment but with fewer pre-installed applications. Users can install additional software as needed. This offers a balance between functionality and SD card space. - Raspberry Pi OS Lite:
A minimal image without a graphical desktop environment. Users interact with the OS solely through the command-line interface (CLI). This version is much smaller, boots faster, and consumes fewer resources, making it ideal for embedded projects (like our weather station if you plan to run it headless) or server applications.
For this workshop, you can choose either "Raspberry Pi OS with desktop" or "Raspberry Pi OS Lite."
- If you are new to Linux or prefer a graphical interface for setup and development, the desktop version is recommended. You can still access the command line via a terminal application.
- If you are comfortable with the command line and want a lean system, or if you are using a Pi with limited resources (like an older model or a Pi Zero), the Lite version is a good choice. Our weather station script will ultimately run from the command line.
For learning purposes, starting with the "Raspberry Pi OS with desktop" can be more user-friendly, and all command-line operations can still be performed through its terminal.
Understanding SD Card Requirements and Classes
The microSD card is not just storage; it's the Raspberry Pi's hard drive, containing the OS, applications, and your data. The performance and reliability of your Pi heavily depend on the quality of the microSD card.
Capacity:
- Minimum:
8GB is the absolute minimum, especially for desktop versions of Raspberry Pi OS. - Recommended:
16GB or 32GB provides a good balance. It offers enough space for the OS, several applications, and data logs for our weather station. - Larger Sizes (64GB+):
Can be used, but ensure your Pi model and the imager tool support SDXC formatting (exFAT for the boot partition if necessary, though typically the imager handles this). For very large cards, ensure they are genuine and from reputable brands.
Speed Class:
SD cards are rated with various speed classes, which indicate their minimum sequential write speed. This is important for OS performance, boot times, and application loading.
- Class 2, 4, 6:
Older and slower. Avoid if possible. - Class 10:
Minimum recommended speed. Indicates a minimum sequential write speed of 10 MB/s. - UHS Speed Class 1 (U1):
Equivalent to Class 10 for minimum write speed (10 MB/s). Often supports higher peak speeds. - UHS Speed Class 3 (U3):
Minimum sequential write speed of 30 MB/s. Better for 4K video recording, but also offers a snappier experience on the Pi. - Video Speed Class (V10, V30, etc.):
Similar to UHS classes, indicating minimum sustained write speeds for video. V10 is 10 MB/s, V30 is 30 MB/s.
Application Performance Class:
This is a newer standard that measures random read/write performance (IOPS - Input/Output Operations Per Second), which is often more critical for OS and application responsiveness than sequential speeds.
- A1 (Application Performance Class 1):
Minimum random read 1500 IOPS, random write 500 IOPS. - A2 (Application Performance Class 2):
Minimum random read 4000 IOPS, random write 2000 IOPS. A2 cards offer significantly better random access performance and can make the Raspberry Pi OS feel much snappier. They require host driver support, which modern Raspberry Pi kernels provide.
Recommendations:
- Brand:
Choose reputable brands like SanDisk, Samsung, Kingston, Lexar, etc. Avoid unbranded or suspiciously cheap cards, as they are often counterfeit, slow, or unreliable. - Ideal Choice:
An A1 or A2 rated Class 10/U1/U3 card from a reputable brand with 16GB or 32GB capacity is a good investment for your Raspberry Pi.
SD Card Wear:
SD cards use NAND flash memory, which has a limited number of write/erase cycles. For applications with frequent writes (like continuous data logging directly to the SD card), this can eventually lead to card failure. We will discuss strategies to mitigate this later, such as:
- Using high-endurance SD cards (designed for dashcams or surveillance).
- Minimizing unnecessary writes (e.g., disabling swap, reducing logging).
- Mounting the root filesystem as read-only.
- Logging data to RAM (tmpfs) and periodically syncing to the SD card, or logging to an external USB drive or network location.
For our initial project setup, a standard good-quality A1/A2 card will be fine.
Disk Preparation Architectures and File Systems
When you image Raspberry Pi OS onto an SD card, the imager tool typically creates two main partitions: a small boot partition and a larger root partition. Understanding these and the file systems they use is important.
Common File Systems for Raspberry Pi (ext4, FAT32)
A file system is a method and data structure that an operating system uses to control how data is stored and retrieved.
-
FAT32 (File Allocation Table 32):
- Usage on Pi:
This is used for the boot partition (typically labeled/boot
when mounted in Linux). - Characteristics:
- Wide Compatibility:
FAT32 is readable by almost all operating systems (Windows, macOS, Linux). This is crucial because the Raspberry Pi's bootloader (in its firmware) needs to read the initial boot files (kernel, device tree blobs, etc.) from this partition, and it's simple enough for the firmware to understand. It also allows you to easily access boot configuration files (likeconfig.txt
andcmdline.txt
) from another computer if needed for troubleshooting. - Limitations:
- Maximum file size of 4GB.
- Maximum partition size of 2TB (though often practically limited to 32GB by some OS formatting tools).
- Lacks features of modern journaling file systems like permissions (in the Linux sense), ownership, and robustness against corruption.
- Wide Compatibility:
- Why it's used for /boot:
Its simplicity and cross-platform compatibility are key for the initial boot phase.
- Usage on Pi:
-
ext4 (Fourth Extended Filesystem):
- Usage on Pi:
This is used for the root partition (labeled/
), which contains the main Linux operating system, user files, and applications. - Characteristics:
- Journaling:
ext4 is a journaling file system. This means it keeps a log (journal) of changes that are about to be made to the file system. If the system crashes or loses power unexpectedly, the journal can be used to bring the file system back to a consistent state quickly, reducing the risk of data corruption. This is very important for stability. - Permissions and Ownership:
Supports standard Linux file permissions (read, write, execute for owner, group, others) and ownership, which are essential for a multi-user operating system and for security. - Performance:
Generally offers good performance for a wide range of workloads. - Large File and Partition Support:
Supports very large files and partition sizes, far beyond the needs of a typical Raspberry Pi SD card. - Linux Native:
It's the de facto standard file system for most Linux distributions.
- Journaling:
- Why it's used for / (root):
Its robustness, support for Linux permissions, and overall performance make it ideal for the main operating system partition.
- Usage on Pi:
Why ext4 is the Default for the Root Partition
As mentioned above, ext4 is chosen for the root partition (/
) due to several key advantages critical for a Linux system:
- Robustness:
Journaling significantly protects against file system corruption in case of power outages or unclean shutdowns – a common scenario for embedded devices like the Raspberry Pi. - Permissions:
Linux relies heavily on its permission model (owner, group, other; read, write, execute) to secure files and manage access. ext4 fully supports this model. FAT32 does not have a comparable permission system suitable for a full Linux OS. - Special Files:
Linux uses various types of "special" files, such as device files (in/dev
), symbolic links, and sockets. ext4 handles these correctly. - Efficiency and Scalability:
While an SD card is small, ext4 is designed for much larger storage and handles many small files (common in an OS) efficiently.
The Role of the FAT32 Boot Partition
The Raspberry Pi's boot process is a multi-stage affair:
- SoC Boot ROM (First Stage Bootloader):
When the Pi powers on, a small piece of code embedded in the Broadcom SoC (System on Chip) executes. This code is minimal and its primary job is to load the next stage bootloader from an accessible storage medium. - Second Stage Bootloader (
bootcode.bin
on SD Card):
The SoC Boot ROM looks for a FAT32 formatted partition on the SD card and loadsbootcode.bin
. This second-stage bootloader is more capable and initializes some basic hardware like SDRAM. - GPU Firmware (
start.elf
):
bootcode.bin
then loads the GPU firmware (start.elf
) and related files (fixup.dat
). The Raspberry Pi actually boots with the VideoCore IV GPU taking the lead initially. The GPU firmware reads configuration files likeconfig.txt
(for hardware parameters) andcmdline.txt
(for kernel boot arguments). - Linux Kernel (
kernel.img
orkernel7.img
,kernel7l.img
,kernel8.img
):
Finally,start.elf
loads the Linux kernel image into RAM and transfers execution to it. The kernel then mounts the ext4 root file system and proceeds with the Linux boot process (initializing drivers, starting services likesystemd
orSysVinit
).
The FAT32 boot partition is essential because:
- The initial bootloaders in the Pi's SoC and
bootcode.bin
are designed to read from a simple, widely compatible file system like FAT32. - It allows easy modification of boot parameters (
config.txt
,cmdline.txt
) by mounting the SD card on any Windows, macOS, or Linux computer, which can be a lifesaver if the Pi fails to boot.
When you use an imager tool like Raspberry Pi Imager, it automatically creates these two partitions:
- A small FAT32 partition (usually around 256MB) mounted as
/boot
once Linux is running. - A larger ext4 partition taking up the rest of the SD card space, mounted as the root filesystem (
/
).
Considerations for SD Card Longevity (Read-only root, logging to RAM/external drive)
Standard SD cards are not designed for the constant, heavy write operations that a full desktop OS can sometimes generate (e.g., swap file usage, frequent logging, browser caches). This can lead to premature wear and failure of the SD card. For an embedded project like a weather station that might run 24/7, this is a significant concern.
Here are several strategies to mitigate SD card wear:
-
Minimize Swap Usage:
- Swapping (using disk space as virtual RAM) involves heavy read/write operations. Raspberry Pi OS, by default, often uses a small swap file or partition (
dphys-swapfile
). - Action:
For Pis with 1GB RAM or more, and for dedicated tasks like our weather station, consider disabling or significantly reducing swap. - Monitor RAM usage to ensure you're not running out of memory.
- Swapping (using disk space as virtual RAM) involves heavy read/write operations. Raspberry Pi OS, by default, often uses a small swap file or partition (
-
Optimize Logging:
- System logs and application logs can generate many writes.
- Action:
- Configure services to log less verbosely if possible.
- Consider using
log2ram
or a similar utility.log2ram
creates a RAM disk (tmpfs) for log directories like/var/log
. Logs are written to RAM and periodically synced to the SD card (e.g., on shutdown or hourly). This drastically reduces SD card writes. - Redirect logs to an external USB drive or a remote syslog server.
-
Mount Root Filesystem as Read-Only (
ro
):- This is an advanced technique but highly effective for appliance-like devices. The OS runs entirely from a read-only root filesystem. Any necessary writes are redirected to a RAM disk (tmpfs) or a separate writable partition (e.g., on a USB drive).
- How it works:
- The
/boot
partition is often kept read-write for easier kernel updates or configuration changes (though it can also be made read-only with a remount process for updates). - The root filesystem (
/
) is mounted read-only. - Specific directories that require write access (e.g.,
/tmp
,/var/run
,/var/log
, user home directories if needed) are mounted astmpfs
(stored in RAM) or overlaid with a writable layer usingoverlayfs
.
- The
- Pros:
Maximizes SD card lifespan, makes the system more resilient to corruption from power loss (as no writes are happening to critical system areas). - Cons:
More complex to set up and manage. Software updates or configuration changes require temporarily remounting the root filesystem as read-write. Applications not designed for a read-only environment may misbehave. - Tools:
Utilities likeoverlayroot
or custom scripts can help manage a read-only root setup. Raspberry Pi OS has an option inraspi-config
(under Performance Options -> Overlay File System) to enable a basic overlay for read-only operation, though it might need further customization for specific needs.
-
Use an External USB Drive for Write-Intensive Data:
- For data logging (like our weather station data), consider writing directly to a USB flash drive or an external HDD/SSD. These are generally more robust for frequent writes than SD cards.
- Mount the USB drive and configure your data logging script to save files there.
-
Choose High-Endurance SD Cards:
- As mentioned earlier, these cards are specifically designed for continuous write operations (e.g., in dashcams, security cameras). They use more durable NAND flash and advanced wear-leveling algorithms. They are more expensive but can be worth it for critical applications.
For this workshop, we will initially set up a standard system. Once the weather station is operational, you might consider implementing some of these longevity strategies, especially log2ram
or logging data to a USB drive, as a follow-up exercise. A full read-only root setup is advanced but a valuable skill for deploying robust embedded systems.
Imaging the SD Card
"Imaging" or "flashing" is the process of writing the OS image file (a compressed archive containing the complete file system structure) onto the microSD card.
Recommended Tool: Raspberry Pi Imager
The Raspberry Pi Foundation provides an official tool called Raspberry Pi Imager, which simplifies this process. It's available for Windows, macOS, and Linux (including Raspberry Pi OS itself).
Advantages of Raspberry Pi Imager:
- Downloads the latest OS images directly, so you don't need to find and download them manually.
- Verifies the download and the write process.
- Can pre-configure options like hostname, enabling SSH, Wi-Fi credentials, locale settings before the first boot. This is extremely useful for headless setups.
- Supports advanced options like cloud-init for more complex automated setups.
Steps using Raspberry Pi Imager:
-
Download and Install Raspberry Pi Imager:
- Go to the official Raspberry Pi website (raspberrypi.com or raspberrypi.org) and navigate to the "Software" or "Downloads" section.
- Download Raspberry Pi Imager for your computer's operating system (Windows, macOS, or Linux).
- Install and run the application.
-
Prepare the MicroSD Card:
- Insert your microSD card into the SD card reader connected to your computer.
- Backup any important data from the microSD card, as this process will erase everything on it.
-
Using Raspberry Pi Imager:
- CHOOSE DEVICE (Optional but helpful):
Select your Raspberry Pi model. This helps filter the OS list. -
CHOOSE OS:
- Click the "CHOOSE OS" button.
- Select "Raspberry Pi OS (other)".
- Choose the version you prefer:
- "Raspberry Pi OS Bullseye (64-bit)" or "(32-bit)" for the version with a desktop. (Bullseye is a codename for a Debian release. Newer releases like Bookworm may be available). Typically, 32-bit is fine for older Pis or if 2/4GB RAM. 64-bit can offer performance benefits on Pi 3/4/Zero2W with sufficient RAM, but may have slightly less mature software compatibility in some edge cases, though this is improving rapidly. For this project, either is fine.
- "Raspberry Pi OS Lite Bullseye (64-bit)" or "(32-bit)" for the command-line only version.
- The imager will download the selected OS.
-
Advanced Options (Highly Recommended for Headless Setup or Convenience):
- Before selecting storage, click the gear icon (⚙️) for advanced options. This menu allows pre-configuration.
- Set hostname:
Choose a name for your Pi on the network (e.g.,piweatherstation
). Default israspberrypi
. - Enable SSH:
Crucial for headless access. Select "Enable SSH" and "Use password authentication." - Set username and password:
The default username ispi
. It's highly recommended to change this for security. You can set a new username (e.g.,weatheradmin
) and a strong password. If you keeppi
, at least change the default password.- Note: Some newer Raspberry Pi OS images force user creation on first boot if not pre-configured. Pre-configuring here is best.
- Configure wireless LAN:
If you want to use Wi-Fi, check this box and enter your Wi-Fi network's SSID (name) and password. Also, select your Wi-Fi country. - Set locale settings:
Set your time zone and keyboard layout. - Click "SAVE" when done with advanced options.
-
CHOOSE STORAGE:
- Click the "CHOOSE STORAGE" button.
- Select your microSD card from the list. Be extremely careful to select the correct drive, as choosing the wrong one could erase data from another drive on your computer. The imager usually shows the drive size, which helps.
-
WRITE:
- Click the "WRITE" button.
- Confirm that you want to proceed and that all data on the selected microSD card will be erased.
- The imager will now download the OS image (if it hasn't already), write it to the microSD card, and then verify the write. This process can take some time (10-30 minutes or more) depending on your internet speed, SD card speed, and computer.
- Once complete, you'll see a "Write Successful" message. You can now safely eject the microSD card.
- CHOOSE DEVICE (Optional but helpful):
Alternative Tool: BalenaEtcher
BalenaEtcher is another popular, user-friendly tool for writing images to SD cards and USB drives. It works similarly: select image file (you'd download the .img
file manually from the Raspberry Pi website), select drive, flash. However, it doesn't offer the advanced pre-configuration options of Raspberry Pi Imager.
First Boot and Initial Configuration (raspi-config)
After successfully imaging the microSD card, it's time for the first boot.
-
Insert the MicroSD Card:
- Carefully insert the imaged microSD card into the microSD card slot on your Raspberry Pi (usually on the underside). Ensure it's oriented correctly and clicks into place (on some models).
-
Connect Peripherals (if not already done):
- Ensure your monitor (HDMI), keyboard (USB), and mouse (USB) are connected.
- (Optional) Connect an Ethernet cable if you prefer a wired network connection and haven't configured Wi-Fi.
-
Power On:
- Connect the official power supply to the Raspberry Pi's power port (micro-USB or USB-C).
- The Raspberry Pi will power on automatically. You should see activity on the Pi's LEDs (typically a red power LED and a green activity LED for SD card access).
- Your monitor should display the boot sequence.
-
Initial Boot Process (Desktop Version):
- If you installed a desktop version of Raspberry Pi OS and did not use the advanced options in Raspberry Pi Imager to pre-create a user, you'll likely be guided through a setup wizard on the first boot.
- This wizard will ask you to:
- Set your country, language, and timezone.
- Create a user account and password (the old default
pi
user with passwordraspberry
is no longer standard for fresh images due to security reasons; you are forced to create a new user). - Connect to a Wi-Fi network (if not pre-configured or using Ethernet).
- Check for software updates and install them. This is recommended.
- Reboot after updates.
- If you did pre-configure a user via Raspberry Pi Imager, you should boot directly to the desktop or login prompt.
-
Accessing the Terminal:
- Desktop Version:
Once at the desktop, you can open a terminal window by clicking the terminal icon (usually looks like>_
) in the top panel or by navigating through the applications menu (Accessories > Terminal). - Lite Version:
You will boot directly to a command-line login prompt. Enter the username and password you configured (orpi
/raspberry
for very old images, though this is unlikely now).
- Desktop Version:
-
raspi-config
Utility:raspi-config
is a command-line tool that allows you to configure various system settings. Even if you used the desktop setup wizard or Raspberry Pi Imager's advanced options, it's good to know aboutraspi-config
.- Open a terminal and type:
- You'll be presented with a menu-driven interface. Navigate using arrow keys, Tab, and Enter.
- Key options in
raspi-config
:- System Options:
Password
: Change user password (important if you're still using a default).Hostname
: Set the network name for your Pi.Boot / Auto Login
: Configure whether to boot to desktop or CLI, and if auto-login is enabled.Network at Boot
: Wait for a network connection before proceeding with boot.
- Display Options:
Configure screen resolution (usually handled automatically). - Interface Options:
This is very important for our project.SSH
: Enable/disable Secure Shell server for remote command-line access. (Should be enabled if you set it in Imager).VNC
: Enable/disable VNC server for remote graphical desktop access.SPI
: Enable/disable SPI interface (needed for some sensors/devices, not typically DHT22 or BME280 via I2C).I2C
: Enable/disable I2C interface. Crucial for the BME280 sensor. You will need to enable this.Serial Port
: Configure serial port access.1-Wire
: Enable/disable 1-Wire interface (used by DS18B20 temperature sensors, not DHT22).Remote GPIO
: Allow GPIO access from another machine over the network.
- Performance Options:
Overclock
: (Use with caution, may require better cooling and void warranty on some models). Not needed for this project.GPU Memory
: Allocate RAM to the GPU. Default is usually fine.Overlay File System
: Option to make the root filesystem read-only (advanced, for SD card longevity).
- Localisation Options:
Set timezone, keyboard layout, Wi-Fi country. (Likely set during initial setup/Imager). - Advanced Options:
Expand Filesystem
: Ensures the OS uses the entire SD card space. Raspberry Pi Imager usually does this automatically. If not, run this.Network Interface Names
: Predictable network interface names.
- Update: Update
raspi-config
tool itself.
- System Options:
- After making changes in
raspi-config
, you'll often be prompted to reboot for them to take effect. Select<Finish>
to exit.
-
Update Your System:
- It's always good practice to ensure your system is up-to-date. Open a terminal and run:
sudo apt update
refreshes the list of available software packages.sudo apt full-upgrade -y
upgrades all installed packages to their newest versions (-y confirms automatically). This might take some time.- A reboot might be necessary after a major upgrade (e.g., if the kernel was updated):
sudo reboot
Your Raspberry Pi now has a fresh operating system installed and is configured for basic use. You're ready to move on to exploring the Linux environment and then Python programming.
Workshop Installing Raspberry Pi OS and Initial Setup
Objective:
To install Raspberry Pi OS on a microSD card using Raspberry Pi Imager, perform the first boot, and do initial essential configurations including enabling I2C.
Materials Needed:
- Computer with internet access and an SD card reader
- MicroSD card (min 8GB, 16GB+ Class 10/A1/A2 recommended)
- Raspberry Pi
- Power supply for the Pi
- Monitor, HDMI cable, USB keyboard, USB mouse
Steps:
-
Download and Install Raspberry Pi Imager:
- On your main computer, open a web browser and go to
https://www.raspberrypi.com/software/
. - Download the Raspberry Pi Imager for your operating system (Windows, macOS, or Linux).
- Install the imager application.
- On your main computer, open a web browser and go to
-
Prepare the MicroSD Card:
- Insert the microSD card into your computer's SD card reader.
- Important: Back up any data you want to keep from the microSD card, as it will be erased.
-
Use Raspberry Pi Imager to Write the OS:
- Launch Raspberry Pi Imager.
- Click "CHOOSE DEVICE" and select your Raspberry Pi model (e.g., "Raspberry Pi 4").
- Click "CHOOSE OS". Select "Raspberry Pi OS (other)", then choose "Raspberry Pi OS Bullseye (32-bit)" (or 64-bit if you prefer and have a compatible Pi like Pi 3, 4, or Zero 2 W). For simplicity, 32-bit desktop is fine.
- Note: "Bullseye" might be replaced by a newer version name like "Bookworm" over time. Choose the latest stable release.
- Click the Gear icon (Advanced options). Configure the following:
- Check "Set hostname" and enter a name, e.g.,
myweatherpi
. - Check "Enable SSH" and select "Use password authentication."
- Check "Set username and password." Enter a new username (e.g.,
student
) and a strong password. Make a note of these credentials. - Check "Configure wireless LAN." Enter your Wi-Fi SSID and password. Select your Wi-Fi country. (Skip if using Ethernet).
- Check "Set locale settings." Select your Time zone (e.g.,
Europe/London
) and Keyboard layout (e.g.,us
). - Click "SAVE".
- Check "Set hostname" and enter a name, e.g.,
- Click "CHOOSE STORAGE". Select your microSD card. Verify carefully that it's the correct drive.
- Click "WRITE". Confirm you want to erase the card and proceed.
- Wait for the writing and verification process to complete. This may take 10-30 minutes.
- Once done, Raspberry Pi Imager will show a success message. Eject the microSD card from your computer.
-
First Boot of the Raspberry Pi:
- Ensure your Raspberry Pi is powered off (unplugged).
- Insert the newly imaged microSD card into the Pi's microSD card slot.
- Connect the HDMI cable to the Pi and your monitor.
- Connect the USB keyboard and USB mouse to the Pi.
- (Optional) Connect an Ethernet cable if you're not using Wi-Fi.
- Finally, connect the power supply to the Raspberry Pi. It will boot up automatically.
- Observe the boot process on your monitor. Since you pre-configured a user, it should boot to the desktop or a login prompt. If it's a login prompt, use the username and password you set in Raspberry Pi Imager.
- If this is the very first boot of a new OS image and you didn't pre-configure a user (not recommended), you might see a welcome wizard. Follow its prompts to set country, language, timezone, user/password, and Wi-Fi.
-
Open a Terminal and Update System:
- Once at the desktop, find the Terminal icon (usually
>_
on the top panel) and click it to open a command-line terminal. - If you are on Raspberry Pi OS Lite, you are already at the command line after login.
- Type the following commands, pressing Enter after each:
(Wait for it to finish)
(This might take a while. The
-y
automatically confirms prompts.) - If it asks about restarting services, it's usually safe to allow it.
- Once at the desktop, find the Terminal icon (usually
-
Configure Interfaces using
raspi-config
:- In the terminal, type:
- Navigate using arrow keys, Tab, and Enter.
- Go to
Interface Options
. - Select
I2C
. When asked "Would you like the ARM I2C interface to be enabled?", select<Yes>
and press Enter. Press Enter again on the confirmation. - (SSH should already be enabled from the Imager settings, but you can verify it here if you wish).
- Select
<Finish>
at the bottom of the main menu. - It will ask if you want to reboot. Select
<Yes>
. The Raspberry Pi will restart.
-
Verify Network Connection and SSH (Optional but good practice):
- After rebooting and logging in, open a terminal.
- Find your Pi's IP address:
or
(Look for
wlan0
if on Wi-Fi, oreth0
if on Ethernet. Note down the IP address, e.g.,192.168.1.105
). - From another computer on the same network, try to SSH into your Pi (if you enabled SSH):
- On Windows, use PowerShell or a client like PuTTY.
- On macOS/Linux, use the terminal.
- Command:
ssh your_username@your_pi_ip_address
(e.g.,ssh student@192.168.1.105
). - Accept the host key fingerprint if prompted (type
yes
). - Enter the password you set for your user.
- If you can log in, SSH is working! Type
exit
to close the SSH session.
You have now successfully installed and configured Raspberry Pi OS. Your Pi is updated, I2C is enabled (which we'll need for one of our sensors), and you're ready to learn about the Linux command line in more detail.
3. Introduction to Linux and the Command Line
The Raspberry Pi OS is a flavor of Linux, a powerful and versatile family of open-source operating systems. While the desktop environment provides a familiar graphical user interface (GUI), a significant amount of the Raspberry Pi's power and flexibility, especially for embedded projects and server tasks, is unlocked through the Command Line Interface (CLI). This section will introduce you to why Linux is used, basic but essential commands, package management, and text editing in the terminal.
Why Linux on the Raspberry Pi?
Linux was chosen as the primary operating system for the Raspberry Pi for several compelling reasons:
- Open Source and Cost-Free:
Linux is open source, meaning its source code is freely available to view, modify, and distribute. This aligns with the Raspberry Pi Foundation's educational mission and helps keep the platform's cost low, as there are no OS licensing fees. - Stability and Reliability:
Linux is renowned for its stability and ability to run for extended periods without crashing or needing reboots. This is crucial for embedded systems like our weather station, which might operate continuously. - Performance and Efficiency:
Linux is highly efficient and can run well on hardware with limited resources, such as the earlier Raspberry Pi models. Its kernel is modular, allowing for customization to include only necessary components, further optimizing performance. - Hardware Support:
The Linux kernel has extensive support for a vast range of hardware, including the ARM processors used in Raspberry Pis and the various peripherals and interfaces like GPIO, I2C, SPI, USB, and networking. - Flexibility and Customization:
Linux can be tailored for a wide array of tasks, from a general-purpose desktop to a dedicated server, a media center, or an embedded controller. This flexibility is ideal for the diverse projects Raspberry Pi users undertake. - Strong Command Line Interface (CLI):
The Linux CLI (often referred to as the "shell," with "Bash" being a common one) is incredibly powerful for automation, scripting, remote management, and fine-grained system control. - Vast Software Ecosystem:
A massive amount of open-source software, development tools, libraries, and applications are readily available for Linux, often pre-compiled or easily compilable for the ARM architecture of the Raspberry Pi. - Security:
Linux has a robust security model based on users, groups, and permissions. With proper configuration and maintenance (like regular updates), it can be a secure platform. - Community Support:
Linux has one of the largest and most active developer and user communities worldwide. This means abundant documentation, forums, tutorials, and quick help when encountering issues. - Educational Value:
Learning Linux provides valuable skills applicable in many areas of computing, from system administration and software development to cybersecurity and cloud computing.
For the Raspberry Pi, Linux (specifically Debian, on which Raspberry Pi OS is based) provides a mature, feature-rich, and well-supported foundation that allows users to fully exploit the hardware's capabilities.
Basic Linux Commands (ls, cd, pwd, mkdir, rm, cp, mv, sudo)
The command line is a text-based interface where you type commands to tell the computer what to do. When you open a Terminal window or log in via SSH, you are interacting with a shell program (usually bash
- Bourne Again SHell).
Here are some fundamental commands. Try them out in your Raspberry Pi's terminal.
-
pwd
(Print Working Directory):- Shows the full path of the directory you are currently in.
- Example:
Output might be
/home/student
(if your username isstudent
). The/
at the beginning indicates the root of the file system.
-
ls
(List):- Lists the files and directories in the current working directory.
- Example:
- Common options:
ls -l
: Long format, shows details like permissions, owner, size, and modification date.ls -a
: Shows all files, including hidden files (those starting with a dot, e.g.,.bashrc
).ls -lh
: Long format with human-readable file sizes (e.g., 1K, 23M, 2G).- You can combine options:
ls -lah
-
cd
(Change Directory):- Navigates between directories.
- Example:
(This moves you into the
Documents
directory, assuming it exists in your current location.) - Special directory names:
cd ..
: Moves up one directory level (to the parent directory).cd ~
or justcd
: Moves to your home directory (e.g.,/home/student
).cd /
: Moves to the root directory of the file system.cd -
: Moves to the previous directory you were in.
- You can use absolute paths (starting with
/
) or relative paths (not starting with/
, relative to your current location).cd /var/log
(absolute path)- If you are in
/home/student
andDocuments
is inside,cd Documents
(relative path).
-
mkdir
(Make Directory):- Creates a new directory.
- Example:
(This creates a directory named
MyProject
in the current working directory.) - Common option:
mkdir -p ParentDir/ChildDir
: Creates parent directories if they don't exist.
-
rmdir
(Remove Directory):- Removes an empty directory.
- Example:
-
cp
(Copy):- Copies files or directories.
- Syntax:
cp [options] source destination
- Example (copying a file):
(Copies
myfile.txt
tomybackup.txt
in the current directory.) (Copiesmyfile.txt
into theDocuments
directory.) - Common option (for directories):
cp -r SourceDirectory DestinationDirectory
: Recursively copies a directory and its contents.
-
mv
(Move):- Moves files or directories. It's also used to rename files or directories.
- Syntax:
mv [options] source destination
- Example (moving a file):
(Moves
report.docx
into theFinalReports
directory.) - Example (renaming a file):
(Renames
oldname.txt
tonewname.txt
in the current directory.) - Works similarly for directories.
-
rm
(Remove):- Deletes files or directories. Use with extreme caution! There is no "Recycle Bin" in the traditional CLI. Once deleted with
rm
, files are generally gone for good (recovery is very difficult). - Example (deleting a file):
- Common options:
rm -r DirectoryName
: Recursively deletes a directory and all its contents. Be very careful with this.rm -f filename
: Force deletion, suppresses confirmation prompts (if any) and ignores non-existent files. Often used with-r
asrm -rf DirectoryName
. This command is powerful and dangerous if misused. Double-check your command before pressing Enter.rm -i filename
: Interactive mode, prompts for confirmation before deleting each file.
- Deletes files or directories. Use with extreme caution! There is no "Recycle Bin" in the traditional CLI. Once deleted with
-
sudo
(Superuser Do):- Executes a command with superuser (administrator or "root") privileges. Many system-level tasks require
sudo
because they can affect the entire system. - Example:
(The
apt update
command requires root privileges to modify system package lists.) - When you use
sudo
, you'll typically be prompted for your own user's password (the one you log in with, provided your user is in thesudoers
file, which is default for the user created during Raspberry Pi OS setup). - Principle of Least Privilege: Use
sudo
only when necessary. Running everyday commands as root unnecessarily can pose security risks if you make a mistake.
- Executes a command with superuser (administrator or "root") privileges. Many system-level tasks require
Other Useful Commands:
man command_name
: Displays the manual page for a command (e.g.,man ls
). Pressq
to quit.clear
: Clears the terminal screen.history
: Shows a list of previously executed commands.cat filename
: Displays the content of a file.less filename
: Displays file content page by page (use arrow keys, PageUp/Down;q
to quit). Better thancat
for long files.head filename
: Shows the first few lines of a file.tail filename
: Shows the last few lines of a file.tail -f filename
follows the file as it grows (useful for log files).grep "pattern" filename
: Searches for a text pattern within a file.find . -name "filename_pattern"
: Searches for files by name (e.g.,find . -name "*.txt"
finds all .txt files in the current directory and subdirectories).df -h
: Shows disk free space in human-readable format.free -h
: Shows free/used memory and swap space in human-readable format.top
orhtop
: Displays running processes and system resource usage (CPU, memory).htop
is more user-friendly and may need to be installed (sudo apt install htop
). Pressq
to quit.shutdown
/reboot
:sudo shutdown now
: Shuts down the Pi immediately.sudo shutdown -h +10
: Shuts down in 10 minutes.sudo reboot
: Restarts the Pi.
Practice these commands. Navigating the file system and managing files via the CLI is a fundamental skill for working with Linux and your Raspberry Pi.
Managing Packages with APT (update, upgrade, install)
APT (Advanced Package Tool) is the package management system used by Debian and its derivatives like Raspberry Pi OS. It simplifies installing, updating, and removing software.
Packages are pre-compiled software bundles, along with metadata about dependencies (other packages they require to run). APT handles these dependencies automatically.
Key APT Commands (usually require sudo
):
-
sudo apt update
- This command does not install or upgrade any packages.
- It resynchronizes the package index files from their sources. The sources are defined in
/etc/apt/sources.list
and files in/etc/apt/sources.list.d/
. - Essentially, it downloads the latest list of available packages and their versions from the repositories (online servers hosting the software).
- Always run
sudo apt update
beforeupgrade
orinstall
.
-
sudo apt upgrade
- This command upgrades all currently installed packages to their newest versions, based on the information fetched by
sudo apt update
. - It will not remove any packages. If an upgrade for a package requires removing another installed package,
apt upgrade
will not perform that upgrade. - You can add
-y
to automatically answer "yes" to prompts:sudo apt upgrade -y
.
- This command upgrades all currently installed packages to their newest versions, based on the information fetched by
-
sudo apt full-upgrade
(orsudo apt dist-upgrade
on older systems)- This also upgrades installed packages but is more "intelligent" in handling dependencies.
- It will remove currently installed packages if that's needed to perform an upgrade of another package. This is sometimes necessary for major system updates or when dependencies change significantly.
- Generally,
sudo apt full-upgrade
is preferred for keeping the system fully up-to-date. - Again,
-y
can be used:sudo apt full-upgrade -y
.
-
sudo apt install <package_name>
- Installs a new package.
- Example:
(This installs the
htop
system monitor.) - APT will automatically resolve and install any dependencies the package needs.
- You can install multiple packages at once:
sudo apt install package1 package2
.
-
sudo apt remove <package_name>
- Removes a package but leaves its configuration files on the system.
- Example:
-
sudo apt purge <package_name>
- Removes a package AND its configuration files.
- Example:
-
sudo apt autoremove
- Removes packages that were installed as dependencies for other packages but are no longer needed (e.g., because the package that required them was removed). It's good practice to run this occasionally to free up disk space.
-
apt search <keyword>
- Searches the package lists for packages matching the keyword.
- Example:
-
apt show <package_name>
- Shows detailed information about a package, such as its version, size, dependencies, and description.
- Example:
Regularly updating your system (sudo apt update
followed by sudo apt full-upgrade -y
) is crucial for security and stability, as it provides bug fixes and patches for vulnerabilities.
Editing Files (nano)
You'll often need to edit configuration files or write scripts directly on your Raspberry Pi. While graphical text editors are available on the desktop version (like Geany or Mousepad), knowing a command-line text editor is essential, especially for headless operation or when working in the terminal.
nano
is a simple, user-friendly command-line text editor. It's usually pre-installed on Raspberry Pi OS.
How to use nano
:
-
Opening or Creating a File:
- To open an existing file or create a new one, type:
- If
filename.txt
exists, it will be opened. If not,nano
will start with an empty buffer, and saving will create the file. - To edit system configuration files (which often require root privileges), use
sudo
:
-
The
nano
Interface:- The main part of the screen is for text editing.
- At the bottom, you'll see a list of common commands, prefixed with
^
(which means Ctrl key) orM-
(which means Alt key or Esc key).^G Get Help
^O WriteOut
(Save)^X Exit
^R Read File
^W Where Is
(Search)^K Cut Text
(cuts the current line)^U UnCut Text
(pastes the cut text)- And many more (use
^G
for help).
-
Basic Editing:
- Use arrow keys to navigate.
- Type to insert text.
- Backspace/Delete work as expected.
-
Saving a File:
- Press
Ctrl + O
(WriteOut). nano
will ask for the "File Name to Write". If it's an existing file, the name will be pre-filled. Press Enter to confirm. If it's a new file, type the desired name and press Enter.
- Press
-
Exiting
nano
:- Press
Ctrl + X
(Exit). - If you have unsaved changes,
nano
will ask "Save modified buffer?".- Press
Y
for Yes (it will then prompt for a filename if needed, as in step 4). - Press
N
for No (discard changes). - Press
Ctrl + C
to Cancel and return to editing.
- Press
- Press
Example: Creating a simple text file:
- In your terminal, type:
- Type a few lines of text, e.g.:
- Press
Ctrl + O
. The filenamemysample.txt
should be displayed. Press Enter to save. - Press
Ctrl + X
to exitnano
. - You can view the file content using
cat
:
While nano
is great for beginners, other powerful command-line editors like vim
and emacs
exist. They have steeper learning curves but offer more advanced features for experienced users. For most tasks on the Pi, especially for beginners, nano
is perfectly adequate.
Workshop Navigating and Managing Your Pi via CLI
Objective:
To practice using essential Linux commands to navigate the file system, manage files and directories, and install software.
Materials Needed:
- Your Raspberry Pi, booted up and logged in (either via desktop terminal or SSH).
Steps:
-
Open a Terminal:
- If on Raspberry Pi OS with desktop, open a Terminal window.
- If on Raspberry Pi OS Lite, you are already in the terminal after login.
-
Explore Your Home Directory:
- Confirm your current location:
(It should be something like
/home/your_username
) - List files and directories:
- List in long format with hidden files:
(Notice files like
.bashrc
,.profile
.)
- Confirm your current location:
(It should be something like
-
Create and Navigate Directories:
- Create a new directory for this workshop:
- Verify it was created:
- Navigate into the new directory:
- Confirm your new location:
(Should now be
/home/your_username/WeatherStationProject
) - Create a subdirectory for sensor scripts:
- Create another for data logs:
- List the contents of
WeatherStationProject
: (You should seeSensorScripts
andDataLogs
)
-
Create a Sample File with
nano
:- Navigate into
SensorScripts
: - Create a new text file named
test_script.txt
: - In
nano
, type the following: - Save the file: Press
Ctrl + O
, then Enter. - Exit
nano
: PressCtrl + X
. - Verify the file exists:
- Navigate into
-
Copy and Rename Files:
- Copy
test_script.txt
to a backup file: - List files to see both:
- Rename the backup file:
- List files again to see the change:
- Copy
-
Move Files:
- Navigate back to the parent directory (
WeatherStationProject
): - Create a dummy log file in the current directory:
Type
Date,Temperature,Humidity
then save (Ctrl+O
, Enter) and exit (Ctrl+X
). - Move
dummy_log.csv
into theDataLogs
directory: - Verify it's moved:
(You should see
dummy_log.csv
there.)
- Navigate back to the parent directory (
-
Install, Check, and Remove a Package:
- Update your package list:
- Install
htop
(an interactive process viewer): - Run
htop
: (Observe the processes, CPU/memory usage. Pressq
to quithtop
.) - Remove
htop
: - Try running
htop
again. It should say "command not found". - (Optional) Reinstall it if you like it:
sudo apt install htop -y
.
-
Clean Up (Deleting Files and Directories):
- Navigate back to your home directory:
(or
cd /home/your_username
) - Be very careful with the next command. We want to delete the
WeatherStationProject
directory and everything inside it. - First, check what's inside:
ls WeatherStationProject
andls WeatherStationProject/SensorScripts
andls WeatherStationProject/DataLogs
. - Remove the entire directory:
Self-correction/Alternative: A safer way, especially when learning, is to delete contents first:
However,
# cd WeatherStationProject/SensorScripts # rm * # cd ../DataLogs # rm * # cd .. # rmdir SensorScripts # rmdir DataLogs # cd .. # rmdir WeatherStationProject
rm -r
is common. Just always double-check the path before usingrm -r
. - Verify it's gone:
(The
WeatherStationProject
directory should no longer be listed.)
- Navigate back to your home directory:
(or
This workshop provides a practical feel for using the Linux command line. These commands form the bedrock of interacting with your Raspberry Pi, especially when developing projects or managing it remotely. As you progress, these commands will become second nature.
4. Python Programming Essentials for the Raspberry Pi
Python is the most popular programming language for Raspberry Pi projects, and for good reason. Its simplicity, readability, extensive libraries, and strong support for hardware interfacing make it an ideal choice for beginners and experienced developers alike. This section will cover why Python is favored, how to set up your Python environment on the Pi, fundamental Python concepts, and working with libraries.
Why Python for Hardware Interaction?
Python's suitability for hardware interaction on platforms like the Raspberry Pi stems from several key factors:
-
Readability and Simplicity:
Python's syntax is designed to be clear and human-readable, resembling plain English. This lowers the barrier to entry for programming and makes code easier to understand, write, and maintain, especially for those new to software development or hardware control. -
Extensive Libraries:
Python boasts a vast standard library and an even larger ecosystem of third-party packages available through the Python Package Index (PyPI). For Raspberry Pi users, this includes:- GPIO Control:
Libraries likeRPi.GPIO
,gpiozero
make it straightforward to control the General Purpose Input/Output pins for reading sensor data, controlling LEDs, motors, etc. - Sensor Interfacing:
Specific libraries exist for countless sensors (e.g.,Adafruit_DHT
for DHT temperature/humidity sensors,smbus2
for I2C communication often used by BMP/BME sensors). - Networking:
Built-in modules likesocket
,http.client
, and third-party libraries likerequests
(for HTTP requests),Flask
orDjango
(for web servers) simplify network communication. - Data Processing:
Libraries likeNumPy
for numerical operations,Pandas
for data analysis, andMatplotlib
for plotting are invaluable for projects that collect and analyze data (like our weather station). - And many more:
For image processing (OpenCV), machine learning (TensorFlow Lite, Scikit-learn), databases (SQLite3), etc.
- GPIO Control:
-
Interpreted Language:
Python is an interpreted language, meaning code is executed line by line. This allows for rapid prototyping and testing. You can write a few lines of code, run them immediately, and see the results, which is very beneficial when experimenting with hardware. -
Cross-Platform:
Python code is generally portable across different operating systems (Windows, macOS, Linux) with minimal or no changes, as long as platform-specific libraries (like GPIO control) are handled appropriately. -
Strong Community Support:
Python has a massive and active global community. This translates to abundant tutorials, documentation, forums, and readily available help for any problem you might encounter. -
Pre-installed on Raspberry Pi OS:
Python (typically Python 3) comes pre-installed with Raspberry Pi OS, making it easy to get started without additional setup. -
Educational Focus:
Python is widely taught in schools and universities as an introductory programming language, aligning well with the Raspberry Pi's educational goals.
While other languages like C/C++ can offer higher performance for extremely time-critical hardware interactions, Python's ease of use and the richness of its libraries often make it faster to develop and deploy projects. For most Raspberry Pi applications, including our weather station, Python's performance is more than adequate, and the development speed is a significant advantage.
Setting up Your Python Environment (IDEs, venv)
Raspberry Pi OS comes with Python 3 pre-installed. You can verify this by opening a terminal and typing:
You might also seepython
(which could point to Python 2 on older systems, or Python 3 on newer ones). It's best practice to explicitly use python3
and pip3
to ensure you're using Python 3.
Text Editors vs. IDEs:
-
Text Editors:
nano
: Simple, command-line based, already discussed. Good for quick edits.Geany
: A lightweight graphical text editor with some IDE-like features (syntax highlighting, basic project management). Often pre-installed on Raspberry Pi OS with desktop.Thonny IDE
: Specifically designed for Python beginners. It's user-friendly, comes with Python 3 built-in (its own interpreter or can use the system's), and has a debugger. Thonny is often pre-installed on Raspberry Pi OS with desktop and is highly recommended for beginners writing Python on the Pi.Visual Studio Code (VS Code)
: A powerful, free source code editor developed by Microsoft. It supports Python development extremely well through extensions. While it can run directly on a Raspberry Pi (especially Pi 4 with more RAM), it can be resource-intensive. A common workflow is to write code in VS Code on your main computer and then transfer/run it on the Pi, or use VS Code's remote development capabilities (Remote-SSH extension) to edit code directly on the Pi from your main machine.
-
Integrated Development Environments (IDEs):
- Thonny IDE:
(Mentioned above) Excellent for learning and development directly on the Pi. - Mu:
Another beginner-friendly Python editor, particularly good for working with microcontrollers like the Raspberry Pi Pico, but also supports standard Python on the Pi.
- Thonny IDE:
For this workshop, if you are using Raspberry Pi OS with a desktop, Thonny IDE is an excellent choice. If you are working via SSH or on Raspberry Pi OS Lite, nano
will suffice for editing scripts, or you can use VS Code with Remote-SSH from your main computer.
Using Thonny IDE (if on Desktop):
- Open Thonny from the Raspberry Pi's application menu (usually under "Programming").
- You'll see a script area (top) and a shell area (bottom).
- You can write your Python code in the script area, save it (e.g., as
my_script.py
), and then run it by clicking the green "Run" button or pressing F5. Output and errors will appear in the shell.
Virtual Environments (venv
):
A virtual environment is an isolated Python environment that allows you to manage dependencies for a specific project separately from your system-wide Python installation and other projects. This is highly recommended for any non-trivial Python project.
Why use virtual environments?
- Dependency Management:
Different projects might require different versions of the same library. Virtual environments prevent version conflicts. - Cleanliness:
Keeps your global Python site-packages directory clean. - Reproducibility:
You can easily recreate the environment on another machine by listing its installed packages (usingpip freeze > requirements.txt
).
How to use venv
:
Python 3 includes the venv
module for creating virtual environments.
-
Install
venv
(if not already installed, though usually it is): -
Create a virtual environment:
- Navigate to your project directory (e.g., the
WeatherStationProject
directory we might create later). - Run:
This creates a directory named
myenv
(you can choose any name,venv
or.venv
are common conventions) containing a copy of the Python interpreter and a place to install project-specific libraries.
- Navigate to your project directory (e.g., the
-
Activate the virtual environment:
- On Linux/macOS:
- Your terminal prompt will usually change to show the active environment's name (e.g.,
(myenv) user@hostname:~$
). - While the environment is active,
python
andpip
commands will refer to the versions withinmyenv
, and packages will be installed intomyenv/lib/pythonX.Y/site-packages/
.
-
Install packages into the virtual environment:
- With the environment active:
-
Deactivate the virtual environment:
- When you're done working on the project, simply type:
- Your prompt will return to normal, and
python
/pip
will refer to the system-wide versions again.
For our weather station project, using a virtual environment is good practice, especially as we start installing specific sensor libraries.
Python Basics (Variables, Data Types, Control Flow, Functions)
This is a brief overview. If you're new to Python, it's recommended to follow a more comprehensive Python tutorial alongside this.
-
Variables:
- Used to store data. You don't need to declare their type explicitly.
-
Data Types:
- String (
str
): Text (e.g.,"Hello"
,'World'
). - Integer (
int
): Whole numbers (e.g.,10
,-3
). - Float (
float
): Numbers with decimal points (e.g.,3.14
,-0.5
). - Boolean (
bool
): True or False. - List (
list
): Ordered collection of items, mutable (e.g.,[1, "apple", 3.0]
). - Tuple (
tuple
): Ordered collection of items, immutable (e.g.,(10, 20, 30)
). - Dictionary (
dict
): Unordered collection of key-value pairs (e.g.,{"name": "Pi", "temp": 22}
).
- String (
-
Comments:
- Lines starting with
#
are comments and are ignored by the interpreter.
- Lines starting with
-
Input and Output:
print()
: Displays output to the console.input()
: Reads input from the user (always returns a string).
-
Operators:
- Arithmetic:
+
,-
,*
,/
(division),//
(floor division),%
(modulo),**
(exponentiation). - Comparison:
==
(equal),!=
(not equal),>
,<
,>=
,<=
. - Logical:
and
,or
,not
.
- Arithmetic:
-
Control Flow:
-
Indentation (usually 4 spaces) is crucial in Python; it defines code blocks.if-elif-else
statements: Used for conditional execution. -
for
loops: Iterate over a sequence (like a list, string, or range). -
while
loops: Repeat as long as a condition is true.
-
-
Functions:
- Reusable blocks of code. Defined using
def
.
- Reusable blocks of code. Defined using
-
Error Handling (
try-except
):- Used to catch and handle exceptions (errors) that might occur during execution.
This is particularly important when dealing with hardware, as sensor readings can fail.
try: num = int(input("Enter a number: ")) result = 10 / num print("Result is:", result) except ValueError: print("Invalid input. Please enter a number.") except ZeroDivisionError: print("You cannot divide by zero!") except Exception as e: print(f"An unexpected error occurred: {e}") finally: print("Execution finished (or an error occurred).") # This block always executes
- Used to catch and handle exceptions (errors) that might occur during execution.
Working with Python Libraries (pip)
pip
is the package installer for Python. You use it to install libraries from the Python Package Index (PyPI) and other sources. If you're using a virtual environment, pip
will install packages into that environment.
Common pip
commands (use pip3
to be specific for Python 3):
-
pip3 install <package_name>
:- Installs a package.
- Example:
(If in an active virtual environment, just
pip install requests
is fine).
-
pip3 install <package_name>==<version>
:- Installs a specific version of a package.
- Example:
-
pip3 uninstall <package_name>
:- Uninstalls a package.
- Example:
-
pip3 list
:- Lists installed packages and their versions.
-
pip3 freeze
:- Outputs installed packages in a format suitable for a
requirements.txt
file. This is useful for sharing your project's dependencies. - To create a requirements file:
- To install packages from a requirements file on another system (or in a new virtual environment):
- Outputs installed packages in a format suitable for a
-
pip3 search <query>
:- Searches PyPI for packages. (Note: This command's functionality on the command line has been deprecated for performance reasons by PyPI. It's better to search directly on the pypi.org website).
-
pip3 show <package_name>
:- Shows details about an installed package.
Importing Libraries in Python Scripts:
Once a library is installed, you use the import
statement in your Python script to use its functionality.
import time # Standard library, no need to pip install
import requests # Third-party, needs 'pip install requests'
from math import sqrt # Import a specific function from a module
# Using the imported modules/functions
time.sleep(2) # Pause for 2 seconds
response = requests.get("https://www.example.com")
print(response.status_code)
print(sqrt(16)) # Output: 4.0
When we start working with sensors, we will use pip
to install specific libraries like Adafruit_CircuitPython_DHT
and libraries for I2C communication.
Workshop Your First Python Script on the Pi
Objective:
To write, save, and run a simple Python script on the Raspberry Pi, demonstrating basic Python concepts and familiarizing yourself with the chosen Python development environment (Thonny or command line with nano). We will also practice creating and using a virtual environment.
Materials Needed:
- Your Raspberry Pi, booted up and logged in.
- Access to a terminal.
- (Optional) Thonny IDE if using the desktop version of Raspberry Pi OS.
Steps:
-
Create a Project Directory and Virtual Environment:
- Open a terminal.
- Create a directory for your Python projects (if you don't have one from the previous workshop, or create a new one):
- Create a virtual environment named
my_python_env
(or any name you prefer, e.g.,venv
): - Activate the virtual environment:
Your terminal prompt should now show
(my_python_env)
.
-
Write the Python Script:
-
Option A: Using
nano
(Command Line):- In the terminal (with the virtual environment active), type:
- Enter the following Python code into
nano
:# My first Python script on Raspberry Pi # Filename: hello_pi.py import time # Import the time module def main(): """Main function to demonstrate basic Python features.""" print("Hello from Raspberry Pi!") # Get user input user_name = input("What is your name? ") print(f"Nice to meet you, {user_name}!") # Simple loop print("\nCounting to 3:") for i in range(1, 4): # Counts 1, 2, 3 print(f"Count: {i}") time.sleep(0.5) # Pause for 0.5 seconds # Simple conditional try: age = int(input("\nHow old are you? ")) if age < 18: print("You are young!") elif age >= 18 and age < 65: print("You are in your prime working years!") else: print("You are experienced in life!") except ValueError: print("That doesn't look like a valid age number.") print("\nScript finished.") if __name__ == "__main__": main()
- The
if __name__ == "__main__":
block ensures thatmain()
is called only when the script is executed directly (not when imported as a module).
- The
- Save the file: Press
Ctrl + O
, then Enter. - Exit
nano
: PressCtrl + X
.
-
Option B: Using Thonny IDE (Desktop):
- Open Thonny IDE from the application menu.
- In the script editor (top pane), type or paste the same Python code as above.
- Click "File" -> "Save As...".
- Navigate to your
MyPythonScripts
directory (e.g.,/home/your_username/MyPythonScripts
). - Save the file as
hello_pi.py
. - Important for Thonny & Virtual Environments: By default, Thonny uses its own bundled Python interpreter or the system Python. To use your virtual environment:
- In Thonny, go to "Tools" -> "Options...".
- Go to the "Interpreter" tab.
- Select "Alternative Python 3 interpreter or virtual environment".
- In the "Path to the Python executable" field, browse to or type the path to the Python interpreter inside your virtual environment. This would be something like:
/home/your_username/MyPythonScripts/my_python_env/bin/python3
. - Click "OK". Thonny will restart its backend/shell using this interpreter.
-
-
Run the Python Script:
-
Option A: From the Command Line (if you used
nano
):- Make sure your virtual environment is still active (
(my_python_env)
in prompt). - Ensure you are in the
MyPythonScripts
directory wherehello_pi.py
is saved. - Execute the script:
(Or simply
python hello_pi.py
if the virtual environment correctly setspython
to point to its interpreter).
- Make sure your virtual environment is still active (
-
Option B: From Thonny IDE:
- Click the green "Run current script" button (or press F5).
- The script will execute, and output/prompts will appear in the "Shell" pane at the bottom.
-
-
Interact with the Script:
- The script will print "Hello from Raspberry Pi!".
- It will ask for your name. Type your name and press Enter.
- It will then count to 3.
- Finally, it will ask for your age. Type your age and press Enter.
- Observe the output based on your input. Try entering non-numeric text for age to see the error handling.
-
Experiment (Optional):
- Modify the script. For example:
- Change the messages.
- Make the loop count higher.
- Add another
elif
condition for age.
- Save your changes and run the script again to see the effects.
- Modify the script. For example:
-
Deactivate the Virtual Environment (when done with this session):
- If you are in the terminal: Your prompt will return to normal.
This workshop has given you a hands-on start with Python on your Raspberry Pi. You've learned how to create and run a script, use a virtual environment, and seen basic Python syntax in action. These skills are foundational for building our weather station.
5. Interfacing with Sensors The Heart of the Weather Station
Now that we have a foundational understanding of the Raspberry Pi, Linux, and Python, it's time to delve into the hardware aspect of our Mini Weather Station: the sensors. This section will introduce the Raspberry Pi's GPIO (General Purpose Input/Output) pins, discuss the types of sensors we'll be using (DHT22 for temperature/humidity and BME280 for temperature/pressure/humidity), and guide you through wiring them to the Pi.
Understanding GPIO Pins
The Raspberry Pi's GPIO header is a 40-pin connector that allows the Pi to communicate with external electronic devices like sensors, LEDs, motors, and more. These pins are a direct link to the Pi's processor (SoC - System on Chip) and can be controlled programmatically.
Key Characteristics of GPIO Pins:
- Digital Nature:
Most GPIO pins are digital, meaning they can be in one of two states: HIGH (typically 3.3 Volts) or LOW (0 Volts/Ground). They can be configured as:- Output:
To send a signal (e.g., turn an LED on/off). - Input:
To read a signal (e.g., detect a button press or read data from a sensor).
- Output:
- Voltage Level:
Raspberry Pi GPIO pins operate at 3.3V. This is crucial. Connecting a 5V device directly to a GPIO input pin can damage the Pi. Always ensure voltage level compatibility or use level shifters if interfacing with 5V logic devices. -
Numbering Schemes:
There are two main ways to refer to GPIO pins:- Board Pin Numbering (Physical Pin Numbering):
Refers to the physical pin number on the 40-pin header (1-40). Pin 1 is top-left (often marked with a square pad or "P1"), Pin 2 is next to it, Pin 3 below Pin 1, and so on. This is easy to identify on the board. - BCM Pin Numbering (Broadcom SoC Channel Numbering):
Refers to the GPIO channel number on the Broadcom SoC. This is the scheme often used by libraries likeRPi.GPIO
andgpiozero
. It's independent of the physical pin layout. - It's essential to know which numbering scheme your chosen library or tutorial uses. You can find pinout diagrams online by searching "Raspberry Pi 40 pin GPIO pinout".
A typical Raspberry Pi 40-pin GPIO header includes:
- Power Pins:
- Multiple 3.3V power pins.
- Multiple 5V power pins (can be used to power compatible external devices, but not for GPIO signal input).
- Multiple Ground (GND) pins.
- General Purpose I/O Pins:
The pins that can be used as inputs or outputs (e.g., GPIO2, GPIO3, ..., GPIO27). - Special Function Pins:
Some GPIO pins can also serve special functions like:- I2C (Inter-Integrated Circuit):
Pins GPIO2 (SDA) and GPIO3 (SCL) are commonly used for the I2C communication protocol. This is a two-wire serial bus used to connect to many sensors and devices (like our BME280). - SPI (Serial Peripheral Interface):
Another serial communication protocol (typically using GPIO9 (MISO), GPIO10 (MOSI), GPIO11 (SCLK), and GPIO7/GPIO8 (CE0/CE1)). - UART (Serial):
Pins GPIO14 (TXD) and GPIO15 (RXD) for serial communication. - PWM (Pulse Width Modulation):
Some pins support hardware PWM for controlling things like servo motors or LED brightness.
- I2C (Inter-Integrated Circuit):
- Board Pin Numbering (Physical Pin Numbering):
Safety Precautions with GPIO Pins:
- NEVER connect a 5V signal directly to a GPIO input pin. This can permanently damage your Raspberry Pi. GPIO inputs are 3.3V tolerant only.
- NEVER short a GPIO pin configured as output to Ground or to 3.3V/5V. This can draw too much current and damage the pin or the Pi.
- Be careful with static electricity. Handle the Pi and components in an anti-static environment if possible, or ground yourself before touching them.
- Double-check your wiring before powering on. Incorrect wiring is a common cause of problems.
- Current Limits: GPIO pins can source or sink a limited amount of current (typically a few mA per pin, with a total limit for all pins). For driving components that require more current (like motors or many LEDs), use a transistor or a dedicated driver board.
For our project, we'll primarily use:
- A digital GPIO input pin for the DHT22 sensor.
- The I2C pins (GPIO2/SDA and GPIO3/SCL) for the BME280 sensor.
- 3.3V power and GND pins to power the sensors.
Introduction to Digital Sensors (e.g., DHT11/DHT22 - Temperature & Humidity)
Digital sensors output data as a series of discrete high/low signals (digital bits) representing the measured value.
DHT11 and DHT22 (AM2302):
These are popular, low-cost sensors that measure both temperature and relative humidity.
-
DHT22 (or AM2302):
- More accurate and has a wider measurement range than the DHT11.
- Temperature Range: -40 to 80 °C (Accuracy: ±0.5 °C)
- Humidity Range: 0 to 100% RH (Accuracy: ±2-5%)
- Sampling Rate: ~0.5 Hz (once every 2 seconds)
- Operating Voltage: 3.3V to 5V (can be powered directly from Pi's 3.3V)
- Communication: Uses a proprietary 1-wire (single data line) digital protocol. This is not the same as the Dallas 1-Wire protocol.
-
DHT11:
- Cheaper but less precise.
- Temperature Range: 0 to 50 °C (Accuracy: ±2 °C)
- Humidity Range: 20 to 80% RH (Accuracy: ±5%)
- Sampling Rate: ~1 Hz (once every second)
- Operating Voltage: 3.3V to 5V
We will primarily focus on the DHT22 for better accuracy.
How DHT Sensors Work (Simplified):
The sensor contains a thermistor (for temperature) and a capacitive humidity sensor. An internal chip converts the analog readings from these components into a digital signal. When the Raspberry Pi wants a reading:
- The Pi pulls the data line LOW for a short period, then HIGH.
- The DHT sensor detects this, responds by pulling the line LOW, then HIGH.
- The sensor then sends out 40 bits of data: 16 bits for humidity, 16 bits for temperature, and 8 bits for a checksum (to verify data integrity).
- The Pi reads these bits by precisely timing the duration of HIGH pulses on the data line.
DHT Sensor Modules:
DHT sensors are often sold as modules mounted on a small PCB. These modules typically include:
- The DHT sensor itself.
- A pull-up resistor (usually 4.7kΩ or 10kΩ) on the data line. This resistor is important for stable communication. If you have a bare DHT sensor (just the 3 or 4-pin component), you'll likely need to add this resistor externally between the data line and VCC (3.3V). If your module has 3 pins, it usually includes the resistor. If it has 4 pins (one is NC - Not Connected), you might need to check if a resistor is present or add one.
- Pins for VCC (power), Data, and GND.
Pinout (Common for 3-pin modules):
- VCC (or +): Connect to 3.3V on Raspberry Pi.
- DATA (or OUT, S): Connect to a GPIO pin on Raspberry Pi (e.g., GPIO4, GPIO17).
- GND (or -): Connect to a Ground pin on Raspberry Pi.
Reading data from DHT sensors requires precise timing, so specific libraries are used in Python to handle the communication protocol.
Introduction to I2C Sensors (e.g., BMP280/BME280 - Temperature, Humidity, Pressure)
I2C (Inter-Integrated Circuit) Protocol:
I2C is a popular serial communication protocol used for short-distance communication between integrated circuits (ICs), microcontrollers, and sensors.
- Two Wires: It uses only two signal lines:
- SDA (Serial Data): Carries the actual data.
- SCL (Serial Clock): Synchronizes data transfer.
- Master-Slave Architecture: One device is the master (e.g., Raspberry Pi), and one or more devices are slaves (e.g., sensors). Each slave device has a unique I2C address (usually 7-bit).
- Pull-up Resistors: Both SDA and SCL lines require pull-up resistors (typically to 3.3V on the Pi). Many sensor modules come with these resistors built-in.
- Shared Bus: Multiple I2C slave devices can be connected to the same SDA/SCL lines, as long as each has a unique address.
Enabling I2C on Raspberry Pi:
We did this in a previous workshop using sudo raspi-config
-> Interface Options -> I2C -> Enable. This loads the necessary kernel modules. The I2C pins on the Pi are typically:
- GPIO2 (SDA1) - Physical Pin 3
- GPIO3 (SCL1) - Physical Pin 5
BMP280 and BME280 Sensors (Bosch Sensortec):
These are high-precision environmental sensors that communicate via I2C (or SPI, but I2C is more common for hobbyist modules).
- BMP280: Measures barometric pressure and temperature.
- BME280: Measures barometric pressure, temperature, AND humidity. This is an excellent choice for a comprehensive weather station due to its accuracy and combined sensing capabilities. We will focus on the BME280.
BME280 Key Features:
- Temperature Range: -40 to 85 °C (Accuracy: ±1.0 °C typical, can be better)
- Humidity Range: 0 to 100% RH (Accuracy: ±3% RH)
- Pressure Range: 300 to 1100 hPa (Accuracy: ±1 hPa)
- Operating Voltage: Typically 1.71V to 3.6V for the sensor chip. Modules often include a voltage regulator and level shifters, allowing them to be powered by 3.3V or 5V from the Pi. Always check your module's specifications. Most common BME280 modules are 3.3V compatible for VCC and logic.
- I2C Address: Usually
0x76
or0x77
. Some modules have a jumper or trace to select the address if you need to use multiple BME280s or resolve address conflicts.
BME280 Modules:
Like DHT sensors, BME280s are usually sold on breakout boards/modules for easy use. These modules typically include:
- The BME280 sensor chip.
- Pull-up resistors for I2C lines.
- Voltage regulator (if it supports 5V input).
- Pins for VCC, GND, SDA, and SCL. Some modules might have additional pins (like SDO for address selection, CS for SPI).
Pinout (Common for I2C BME280 modules):
- VIN or VCC: Power input (Connect to 3.3V on Raspberry Pi. Check module specs if 5V tolerant).
- GND: Ground (Connect to GND on Raspberry Pi).
- SDA: I2C Data (Connect to GPIO2/SDA on Raspberry Pi).
- SCL: I2C Clock (Connect to GPIO3/SCL on Raspberry Pi).
Python libraries (like smbus2
for low-level I2C and specific BME280 libraries) simplify communication with these sensors.
Wiring the Sensors to the Raspberry Pi
Important Reminders Before Wiring:
- POWER OFF YOUR RASPBERRY PI before connecting or disconnecting any components to/from the GPIO pins. Unplug the power supply.
- Use a Pinout Diagram: Have a Raspberry Pi GPIO pinout diagram handy. Search "Raspberry Pi 40 pin GPIO pinout" for your specific Pi model (though the first 26-40 pins are generally consistent).
- Breadboard: Using a solderless breadboard makes connections easy and temporary.
- Jumper Wires: Use male-to-female jumper wires if connecting directly from Pi to sensor module, or male-to-male if using a breadboard.
Wiring Diagram Philosophy:
We will connect both the DHT22 and BME280 sensors.
- Power (3.3V) and Ground (GND): Both sensors need power. We can use one 3.3V pin and one GND pin from the Pi and share them on the breadboard's power rails if needed.
- DHT22:
- VCC -> Raspberry Pi 3.3V
- DATA -> Raspberry Pi GPIO pin (e.g., GPIO4 - Physical Pin 7)
- GND -> Raspberry Pi GND
- (Pull-up resistor: If your DHT22 module doesn't have one, connect a 4.7kΩ-10kΩ resistor between the DATA pin and VCC/3.3V). Most 3-pin modules have this built-in.
- BME280 (I2C):
- VIN/VCC -> Raspberry Pi 3.3V
- GND -> Raspberry Pi GND
- SDA -> Raspberry Pi GPIO2 (SDA) - Physical Pin 3
- SCL -> Raspberry Pi GPIO3 (SCL) - Physical Pin 5
Visualizing the Raspberry Pi GPIO Header (First few pins):
Physical Pin Numbers & Common Functions:
(Processor side)
+---+-- GND ---+ +---- 5V ----+---+
| 1 | 3.3V PWR | | 5V PWR 2 | o |
| 3 | GPIO2 SDA| | 5V PWR 4 | o |
| 5 | GPIO3 SCL| | GND 6 | o |
| 7 | GPIO4 | | GPIO14 TXD 8 | o |
| 9 | GND | | GPIO15 RXD10| o |
|11 | GPIO17 | | GPIO18 12| o |
|13 | GPIO27 | | GND 14| o |
|15 | GPIO22 | | GPIO23 16| o |
... and so on up to pin 40
(Edge of board)
Let's Choose Specific Pins:
- 3.3V Power: Physical Pin 1 (3.3V)
- Ground: Physical Pin 9 (GND)
- DHT22 Data: GPIO4 (BCM numbering) - Physical Pin 7
- BME280 SDA: GPIO2 (BCM numbering) - Physical Pin 3 (SDA1)
- BME280 SCL: GPIO3 (BCM numbering) - Physical Pin 5 (SCL1)
Connection Steps (Using a Breadboard - Recommended):
- POWER OFF THE PI.
- Place Sensors on Breadboard: Insert the DHT22 module and BME280 module onto the breadboard, ensuring their pins are in different rows and not shorted.
-
Connect Power Rails (Breadboard):
- Connect Raspberry Pi Physical Pin 1 (3.3V) to the red (+) power rail on your breadboard using a jumper wire.
- Connect Raspberry Pi Physical Pin 9 (GND) to the blue (-) ground rail on your breadboard using a jumper wire.
-
Wire the DHT22 Sensor:
- Connect DHT22 VCC (or +) pin to the red (+) power rail on the breadboard.
- Connect DHT22 GND (or -) pin to the blue (-) ground rail on the breadboard.
- Connect DHT22 DATA (or OUT, S) pin to Raspberry Pi GPIO4 (Physical Pin 7).
- (Pull-up Resistor Check: Most DHT22 modules (3-pin versions) have this integrated. If you have a bare 4-pin sensor and one pin is NC, you likely need an external 4.7kΩ to 10kΩ resistor between the DATA pin and the VCC/3.3V rail.)
-
Wire the BME280 Sensor:
- Connect BME280 VIN (or VCC) pin to the red (+) power rail on the breadboard.
- Connect BME280 GND pin to the blue (-) ground rail on the breadboard.
- Connect BME280 SDA pin to Raspberry Pi GPIO2 (SDA) (Physical Pin 3).
- Connect BME280 SCL pin to Raspberry Pi GPIO3 (SCL) (Physical Pin 5).
-
Double-Check All Wiring:
- Carefully review every connection. Ensure:
- Power (3.3V) and Ground are correct for both sensors and the Pi.
- Data/SDA/SCL lines are connected to the correct GPIO pins on the Pi.
- No shorts (e.g., wires accidentally touching).
- Compare your wiring to a reliable pinout diagram and your sensor module's documentation.
- Carefully review every connection. Ensure:
This completes the physical wiring. In the next sections, we'll install the necessary Python libraries and write scripts to read data from these sensors.
Workshop Connecting and Testing a Temperature and Humidity Sensor (DHT22)
This workshop focuses on wiring the DHT22 sensor and performing an initial test to ensure it's communicating with the Raspberry Pi. We'll use a common Python library for the DHT22. We'll tackle the BME280 in a subsequent workshop after verifying this one.
Objective:
To correctly wire the DHT22 sensor to the Raspberry Pi and install the necessary software to read temperature and humidity data.
Materials Needed:
- Raspberry Pi (powered off initially)
- Breadboard
- DHT22 sensor module (preferably a 3-pin module with built-in pull-up resistor)
- Jumper wires (male-to-female if connecting directly, or male-to-male if using a breadboard extensively with Pi's pins brought to breadboard)
- Your Raspberry Pi GPIO pinout diagram for reference
- Access to your Raspberry Pi's terminal (either directly or via SSH)
- Working Python 3 environment (ideally with a virtual environment prepared).
Chosen Pins for DHT22:
- Raspberry Pi 3.3V Power (Physical Pin 1) -> DHT22 VCC
- Raspberry Pi GND (Physical Pin 9) -> DHT22 GND
- Raspberry Pi GPIO4 (BCM numbering, Physical Pin 7) -> DHT22 DATA
Steps:
-
POWER OFF YOUR RASPBERRY PI: Ensure the power supply is disconnected. This is critical before making any GPIO connections.
-
Wire the DHT22 Sensor:
- If using a breadboard:
- Connect a jumper wire from Physical Pin 1 (3.3V) on the Pi to the positive (red, +) rail of your breadboard.
- Connect a jumper wire from Physical Pin 9 (GND) on the Pi to the negative (blue, -) rail of your breadboard.
- Place the DHT22 module on the breadboard.
- Connect the DHT22 VCC (or +) pin to the positive rail on the breadboard.
- Connect the DHT22 GND (or -) pin to the negative rail on the breadboard.
- Connect the DHT22 DATA (or S, OUT) pin to Physical Pin 7 (GPIO4) on the Raspberry Pi.
- If connecting directly (less common for multiple sensors but possible for one):
- Use male-to-female jumper wires.
- Connect DHT22 VCC to Pi's Physical Pin 1 (3.3V).
- Connect DHT22 GND to Pi's Physical Pin 9 (GND).
- Connect DHT22 DATA to Pi's Physical Pin 7 (GPIO4).
- Double-check your wiring meticulously.
- If using a breadboard:
-
Power On and Prepare Software Environment:
- Once wiring is verified, power on your Raspberry Pi.
- Log in and open a terminal.
- If you created a virtual environment in the previous Python workshop (e.g.,
my_python_env
inMyPythonScripts
), activate it: If not using a virtual environment, skip activation, but installing packages globally is less ideal. We'll assume you are using one.
-
Install the Adafruit CircuitPython DHT Library:
- This library provides an easy way to read from DHT11, DHT22, and AM2302 sensors.
- In your terminal (with virtual environment active if using one), run:
- The library might also need access to system utilities to work with GPIOs at a low level. If you encounter issues later about
libgpiod
, you might need to install it system-wide: This system package provides tools and a library for interacting with GPIO character devices. The Python library often uses this underneath.
-
Create a Test Script for DHT22:
- Navigate to a directory where you want to save your script (e.g., inside
MyPythonScripts
). - Create a Python script, for example
test_dht22.py
, usingnano
or Thonny: - Enter the following Python code:
# Filename: test_dht22.py import time import board # Common CircuitPython library for board pin definitions import adafruit_dht # --- Configuration --- # Option 1: Using board.D<GPIO_NUMBER> (BCM Numbering) # This is the recommended way with Adafruit CircuitPython libraries. # GPIO4 corresponds to board.D4 DHT_SENSOR_PIN = board.D4 # Option 2: If you prefer RPi.GPIO style BCM numbers directly (less common with CircuitPython) # You would then initialize DHTDevice with just the number, e.g., adafruit_dht.DHT22(4) # But board.D4 is better as it's more abstract and works across CircuitPython boards. # Initialize the DHT sensor. # Note: use_pulseio=False is important on Raspberry Pi platforms # as pulseio is not supported directly on Linux user-space. # The library will fall back to a bitbang implementation. try: dht_device = adafruit_dht.DHT22(DHT_SENSOR_PIN, use_pulseio=False) print("DHT22 sensor initialized.") except RuntimeError as error: # Errors happen fairly often, DHTs are sensitive sensors. print(f"Failed to initialize DHT22: {error.args[0]}") print("Check wiring and try again. Exiting.") exit() except Exception as e: print(f"An unexpected error occurred during initialization: {e}") dht_device = None # Ensure it's None if initialization failed exit() print("\nAttempting to read data from DHT22 sensor (will try up to 5 times)...") attempts = 0 max_attempts = 5 while attempts < max_attempts: attempts += 1 try: if dht_device is None: # Check if initialization failed earlier print("DHT device not initialized. Cannot read.") break # Print the GDO_PIN the library is using to double check it is the correct one: print(f"Attempt {attempts}: Reading from pin {dht_device.pin}") temperature_c = dht_device.temperature humidity = dht_device.humidity if temperature_c is not None and humidity is not None: temperature_f = temperature_c * (9 / 5) + 32 print(f" Temperature: {temperature_c:.1f} °C / {temperature_f:.1f} °F") print(f" Humidity: {humidity:.1f} %") break # Successful read, exit loop else: print(f" Failed to retrieve a reading (data was None). Attempt {attempts}/{max_attempts}") except RuntimeError as error: # Errors happen fairly often, DHTs are sensitive sensors. print(f" RuntimeError during read: {error.args[0]}. Attempt {attempts}/{max_attempts}") except Exception as e: # Other unexpected errors print(f" An unexpected error occurred: {e}. Attempt {attempts}/{max_attempts}") if attempts < max_attempts: print(" Retrying in 2 seconds...") time.sleep(2.0) # DHT22 sampling rate is ~0.5Hz (once every 2s) else: print("\nMaximum attempts reached. Failed to get a consistent reading.") print("Troubleshooting tips:") print(" - Double-check your wiring (VCC, GND, Data to GPIO4/Pin 7).") print(" - Ensure a pull-up resistor is present (usually on 3-pin modules).") print(" - The sensor might be faulty or particularly sensitive.") print(" - Try running the script with 'sudo python3 test_dht22.py' if permission issues are suspected (though CircuitPython libraries usually handle this).") # Clean up (optional, but good practice for some libraries, though DHTDevice might not strictly need it here) if dht_device: try: dht_device.exit() # Some CircuitPython device drivers have an exit method print("\nDHT device resources released.") except AttributeError: pass # exit() method might not be present on all drivers print("Script finished.")
- Save the file (
Ctrl+O
, Enter) and exitnano
(Ctrl+X
).
- Navigate to a directory where you want to save your script (e.g., inside
-
Run the Test Script:
- In the terminal (virtual environment active, in the correct directory), run:
- Observe the Output:
- If successful, you should see temperature and humidity readings printed to the console.
- Troubleshooting:
- "Failed to initialize DHT22..." or "RuntimeError...": This is common.
- Check Wiring: Power off the Pi and meticulously re-check all connections. Ensure DATA is on GPIO4 (Physical Pin 7).
- Pull-up Resistor: If you have a bare 4-pin sensor, ensure you have a 4.7kΩ to 10kΩ pull-up resistor between the DATA line and 3.3V.
- Wait and Retry: DHT sensors can be finicky. Sometimes just waiting a bit and running the script again helps. The script itself has a retry loop.
- Permissions: Although
adafruit-circuitpython-dht
tries to handle permissions, sometimes direct GPIO access might still be an issue. Try running withsudo
:sudo python3 test_dht22.py
. If this works, it points to a permission issue with your user accessing GPIO. This can often be resolved by adding your user to thegpio
group (sudo usermod -a -G gpio your_username
then log out and back in), but CircuitPython libraries aim to avoid this need. - Faulty Sensor: The sensor itself might be damaged.
- Pin Numbering: Ensure
board.D4
correctly corresponds to the BCM GPIO pin 4 you've wired to. You can confirm BCM pin numbers withpinout
command in the terminal if it's installed (sudo apt install python3-gpiozero && pinout
).board.D4
from theboard
library is generally reliable.
- "ImportError: No module named 'board'" or "'adafruit_dht'": The library was not installed correctly or not in the active Python environment. Ensure your virtual environment is active and you used
pip3 install adafruit-circuitpython-dht
. - Readings are
None
or unrealistic: This can also indicate wiring problems or a faulty sensor.
- "Failed to initialize DHT22..." or "RuntimeError...": This is common.
-
Deactivate Virtual Environment (if used):
- When done:
If you successfully read data from the DHT22, congratulations! You've interfaced your first sensor. If not, carefully work through the troubleshooting steps. Getting hardware to talk to software for the first time often involves a bit of debugging. Once this is working, we'll move on to the BME280.
6. Reading Sensor Data with Python
Having successfully wired at least one sensor (the DHT22) and tested its basic functionality, we now move to more robust Python scripting for reading data from both the DHT22 and the BME280 (our I2C sensor). This section covers using the appropriate Python libraries, implementing error handling, and validating the data received from the sensors.
Using Python Libraries for DHT Sensors (e.g., Adafruit_DHT)
We've already had an introduction to the adafruit-circuitpython-dht
library in the previous workshop. Let's refine its usage and discuss common patterns.
Recap of adafruit-circuitpython-dht
:
- Installation:
pip3 install adafruit-circuitpython-dht
- Import necessary modules:
- Initialization:
- Specify the GPIO pin connected to the DHT sensor's data line using the
board
module (BCM numbering). For example, if connected to GPIO4: - Create a sensor object. For a DHT22:
# The use_pulseio=False argument is important for Raspberry Pi (Linux single-board computers) # as pulseio is typically for microcontrollers. The library falls back to a bit-banging method. try: dht_sensor = adafruit_dht.DHT22(DHT_PIN, use_pulseio=False) except RuntimeError as e: print(f"Failed to initialize DHT sensor: {e}") # Handle error, perhaps exit or retry later dht_sensor = None except Exception as e: # Catch any other unexpected error during init print(f"Unexpected error initializing DHT sensor: {e}") dht_sensor = None
- Specify the GPIO pin connected to the DHT sensor's data line using the
- Reading Data:
- Access the
temperature
andhumidity
attributes of the sensor object. - These readings can sometimes fail (return
None
) or raise aRuntimeError
due to timing sensitivity or checksum errors. It's crucial to implement retries and error handling.if dht_sensor: # Check if sensor was initialized successfully max_retries = 3 for attempt in range(max_retries): try: temperature_c = dht_sensor.temperature humidity = dht_sensor.humidity if temperature_c is not None and humidity is not None: print(f"DHT22 Temp: {temperature_c:.1f}°C, Humidity: {humidity:.1f}%") break # Successful read else: print("DHT22 read failed (got None), retrying...") except RuntimeError as e: print(f"DHT22 RuntimeError: {e}, retrying...") except Exception as e: print(f"DHT22 unexpected error: {e}, retrying...") time.sleep(2.0) # DHT22 needs at least 2 seconds between readings else: # This else clause executes if the loop completed without a 'break' print("Failed to get DHT22 reading after multiple attempts.")
- Access the
- Releasing Resources (Optional but Good Practice):
- Some CircuitPython device drivers have an
exit()
method to release underlying resources.
- Some CircuitPython device drivers have an
Key Considerations for DHT Sensors:
- Timing Sensitivity: DHT sensors rely on precise timing for their custom 1-wire protocol. Other system activity on the Raspberry Pi (a multitasking Linux system) can sometimes interfere, leading to failed reads. This is why retries are essential.
- Sampling Rate: Don't poll the sensor too frequently. The DHT22 has a minimum sampling interval of about 2 seconds. Polling faster can lead to errors or inaccurate readings.
- Pull-up Resistor: As mentioned before, a pull-up resistor (4.7kΩ to 10kΩ) on the data line to VCC is generally required. Most 3-pin modules include this.
Using Python Libraries for I2C Sensors (e.g., smbus2, adafruit-circuitpython-bme280)
For I2C sensors like the BME280, you typically need two layers of libraries:
- A low-level library for I2C communication on Linux (e.g.,
smbus2
). - A higher-level library specific to the sensor (e.g.,
adafruit-circuitpython-bme280
) that uses the low-level library to talk to the sensor and interpret its data.
1. Low-Level I2C Access (smbus2
):
smbus2
is a Python module that provides an interface to the Linux I2C SMBus (System Management Bus) functionality.- Installation:
- You usually don't interact with
smbus2
directly if you're using a sensor-specific CircuitPython library, as that library will handle it. However, it's good to know it's there.
2. Sensor-Specific Library (adafruit-circuitpython-bme280
):
- This library simplifies interacting with the BME280.
- Installation:
- Import necessary modules:
-
Initialization:
- First, get an I2C bus object. The
board
library provides a convenient way to get the default I2C bus (board.I2C()
typically refers toi2c-1
on Raspberry Pi, which uses GPIO2/SDA and GPIO3/SCL).try: i2c = board.I2C() # Uses board.SCL and board.SDA # For the basic library version: # bme280 = adafruit_bme280_basic.Adafruit_BME280_I2C(i2c) # For the full library version (often preferred): bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c) # You can optionally specify the I2C address if it's not the default 0x77 # bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76) print("BME280 sensor initialized.") except ValueError as e: print(f"Failed to initialize I2C bus or BME280 sensor: {e}") print("Ensure I2C is enabled in raspi-config and sensor is wired correctly.") bme280 = None except Exception as e: print(f"Unexpected error initializing BME280: {e}") bme280 = None
- Checking I2C Address: Before running Python code, you can check if the Pi detects the BME280 on the I2C bus.
- Install I2C tools:
sudo apt install i2c-tools -y
- Run detection (bus 1 is common for Pi's main I2C):
sudo i2cdetect -y 1
- You should see the BME280's address (e.g.,
76
or77
) in the output grid. If not, there's a wiring or enabling issue.
- Install I2C tools:
- First, get an I2C bus object. The
-
Reading Data:
- The library provides properties to access the sensor readings.
if bme280: # Check if sensor was initialized successfully try: temperature_c = bme280.temperature humidity = bme280.humidity # BME280 provides humidity pressure_hpa = bme280.pressure # altitude_m = bme280.altitude # Altitude is calculated from pressure and sea level pressure print(f"BME280 Temp: {temperature_c:.1f}°C") print(f"BME280 Humidity: {humidity:.1f}%") print(f"BME280 Pressure: {pressure_hpa:.1f} hPa") except Exception as e: print(f"Error reading from BME280: {e}")
- Sea Level Pressure: For accurate altitude calculations, you often need to set the current sea level pressure for your location.
For a weather station, reporting local station pressure (
# bme280.sea_level_pressure = 1013.25 # Default is 1013.25 hPa # You can set it to a more accurate local value if known # altitude = bme280.altitude # print(f"BME280 Altitude: {altitude:.2f} m")
bme280.pressure
) is usually sufficient unless you specifically need altitude or QNH/QFE conversions.
- The library provides properties to access the sensor readings.
Combining Sensor Readings:
You'll typically have initialization and reading blocks for each sensor in your main script.
Error Handling and Data Validation
When working with physical sensors, especially in a non-real-time environment like a Linux system, errors and invalid data are common. Robust code must anticipate and handle these issues.
Types of Errors:
- Initialization Errors: Sensor not found (wiring issue, I2C address wrong), library issues.
- Communication Errors (
RuntimeError
): For DHT sensors, this is common due to timing issues. For I2C, it could be a bus problem or sensor malfunction. - Invalid Data (
None
values): Libraries might returnNone
if a reading failed the sensor's internal checksum (DHT) or if a valid conversion couldn't be made. - Unrealistic Values: Sometimes a sensor might return a syntactically valid number that is physically impossible (e.g., temperature of -200°C or humidity of 150%).
Strategies:
-
try-except
Blocks:- Wrap sensor initialization and reading calls in
try-except
blocks to catch specific exceptions (e.g.,RuntimeError
,ValueError
) and genericException
.
- Wrap sensor initialization and reading calls in
-
Retries:
- Implement a loop to retry readings a few times if an error occurs or
None
is returned. Include a delay between retries.def read_sensor_with_retries(sensor_object, max_attempts=3, delay_seconds=2): for attempt in range(max_attempts): try: temp = sensor_object.temperature hum = sensor_object.humidity if temp is not None and hum is not None: return temp, hum # Success except RuntimeError: pass # Or log the error except Exception: pass # Or log the error time.sleep(delay_seconds) return None, None # Failed after all attempts
- Implement a loop to retry readings a few times if an error occurs or
-
Data Validation (Sanity Checks):
- After getting a reading, check if it's within a plausible range.
Define what "plausible" means for your environment and sensor specifications. For example, a BME280 temperature reading should ideally be within its spec range (-40°C to +85°C).
def is_valid_temperature(temp_c): # Example plausible range for environmental temperature return -40 <= temp_c <= 85 def is_valid_humidity(hum_percent): return 0 <= hum_percent <= 100 temp, hum = read_sensor_with_retries(dht_sensor) if temp is not None and not is_valid_temperature(temp): print(f"Warning: Unrealistic temperature: {temp}°C") temp = None # Discard unrealistic value if hum is not None and not is_valid_humidity(hum): print(f"Warning: Unrealistic humidity: {hum}%") hum = None # Discard unrealistic value
- After getting a reading, check if it's within a plausible range.
-
Default/Fallback Values:
- Decide what your application should do if a sensor reading fails persistently.
- Log an error and skip the reading for that cycle.
- Use the last known good reading (with a timestamp indicating its age).
- Use a special marker value (e.g.,
None
,-999
) to indicate missing data in logs.
- Decide what your application should do if a sensor reading fails persistently.
-
Logging:
- Use the
logging
module in Python to log errors, warnings, and successful readings. This is much better than justprint()
statements for long-running applications.
- Use the
By combining these techniques, you can create a more resilient weather station script that can handle the occasional hiccups from sensor communication and data.
Workshop Reading Temperature and Humidity with Python
Objective:
To write a Python script that reads data from both the DHT22 and BME280 sensors, incorporating error handling and retries. This workshop assumes you have already wired both sensors as per Section 5 and have enabled I2C.
Materials Needed:
- Raspberry Pi with DHT22 and BME280 wired correctly.
- I2C enabled on the Raspberry Pi.
- Access to the Pi's terminal.
- Python environment with a virtual environment activated (e.g.,
my_python_env
).
Chosen Pins (Recap):
- DHT22 Data: GPIO4 (BCM) /
board.D4
- BME280: I2C bus 1 (GPIO2/SDA, GPIO3/SCL) /
board.I2C()
Steps:
-
Verify Sensor Connections and I2C:
- POWER OFF Pi, double-check wiring for both DHT22 and BME280.
- Power ON Pi.
- Open a terminal.
- Check if the BME280 is detected on the I2C bus:
You should see
76
or77
(or whatever your BME280's address is) in the grid. If not, troubleshoot I2C wiring/enabling for the BME280.
-
Activate Virtual Environment and Install Libraries:
- Navigate to your project directory and activate your virtual environment:
- Install/ensure necessary libraries are present:
(
smbus2
is often a dependency ofadafruit-circuitpython-bme280
but installing it explicitly doesn't hurt). If you get an error related tolibgpiod
for the DHT sensor, ensure it's installed:
-
Create the Python Script (
read_all_sensors.py
):- Using
nano
or Thonny, create a new file namedread_all_sensors.py
. -
Enter the following code:
Note on DHT# Filename: read_all_sensors.py import time import board import adafruit_dht from adafruit_bme280 import basic as adafruit_bme280_basic # Using the basic BME280 library # --- Configuration --- DHT_SENSOR_PIN = board.D4 # GPIO4 (BCM) DHT_SENSOR_TYPE = adafruit_dht.DHT22 # Or adafruit_dht.DHT11 if using DHT11 # BME280 I2C address (typically 0x77 or 0x76) # If i2cdetect -y 1 shows a different address, change it here. BME280_I2C_ADDRESS = 0x77 # --- Sensor Initialization --- dht_sensor = None bme_sensor = None print("Initializing sensors...") # Initialize DHT22 try: dht_sensor = DHT_SENSOR_TYPE(DHT_SENSOR_PIN, use_pulseio=False) print("DHT sensor initialized.") except RuntimeError as e: print(f"Error initializing DHT sensor: {e.args[0]}") except Exception as e: print(f"Unexpected error initializing DHT sensor: {e}") # Initialize BME280 try: i2c = board.I2C() # Uses board.SCL and board.SDA bme_sensor = adafruit_bme280_basic.Adafruit_BME280_I2C(i2c, address=BME280_I2C_ADDRESS) # You can set sea level pressure for more accurate altitude if needed # bme_sensor.sea_level_pressure = 1013.25 print("BME280 sensor initialized.") except ValueError as e: print(f"Error initializing I2C or BME280 sensor: {e}") print("Ensure I2C is enabled (sudo raspi-config) and sensor is wired correctly.") except Exception as e: print(f"Unexpected error initializing BME280 sensor: {e}") # --- Helper function for data validation (example) --- def is_valid_temperature(temp_c, sensor_name="Sensor"): if temp_c is None: return False # Define plausible range, e.g., -40 to 85 Celsius if not (-40 <= temp_c <= 85): print(f"Warning: {sensor_name} temperature out of plausible range: {temp_c}°C") return False return True def is_valid_humidity(hum_percent, sensor_name="Sensor"): if hum_percent is None: return False if not (0 <= hum_percent <= 100): print(f"Warning: {sensor_name} humidity out of plausible range: {hum_percent}%") return False return True def is_valid_pressure(pressure_hpa, sensor_name="Sensor"): if pressure_hpa is None: return False # Define plausible range, e.g., 800 to 1100 hPa for typical conditions if not (800 <= pressure_hpa <= 1100): print(f"Warning: {sensor_name} pressure out of plausible range: {pressure_hpa} hPa") return False return True # --- Main Data Reading Loop --- print("\nStarting sensor readings (Press Ctrl+C to stop):") try: while True: print("-" * 30) current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) print(f"Time: {current_time}") # Read from DHT22 if dht_sensor: dht_temp_c = None dht_humidity = None # DHT sensors can be finicky, try a few times for _ in range(3): # Max 3 attempts try: dht_temp_c = dht_sensor.temperature dht_humidity = dht_sensor.humidity if dht_temp_c is not None and dht_humidity is not None: break # Got a good reading except RuntimeError as e: print(f"DHT Read Error: {e.args[0]}. Retrying...") except Exception as e: print(f"DHT Unexpected Error: {e}. Retrying...") time.sleep(dht_sensor.MIN_READ_ कसोटी) # Wait before retry (use sensor's defined min interval) if is_valid_temperature(dht_temp_c, "DHT22") and is_valid_humidity(dht_humidity, "DHT22"): print(f"DHT22: Temp={dht_temp_c:.1f}°C, Humidity={dht_humidity:.1f}%") else: print("DHT22: Failed to get valid reading.") else: print("DHT22: Not initialized.") # Read from BME280 if bme_sensor: try: bme_temp_c = bme_sensor.temperature bme_humidity = bme_sensor.humidity bme_pressure = bme_sensor.pressure # bme_altitude = bme_sensor.altitude # Optional valid_bme_temp = is_valid_temperature(bme_temp_c, "BME280") valid_bme_hum = is_valid_humidity(bme_humidity, "BME280") valid_bme_press = is_valid_pressure(bme_pressure, "BME280") if valid_bme_temp and valid_bme_hum and valid_bme_press: print(f"BME280: Temp={bme_temp_c:.1f}°C, Humidity={bme_humidity:.1f}%, Pressure={bme_pressure:.1f} hPa") # if bme_altitude is not None: print(f"BME280: Altitude={bme_altitude:.2f} m") else: print("BME280: One or more readings were invalid.") if not valid_bme_temp: print(f" Invalid BME280 Temp: {bme_temp_c}") if not valid_bme_hum: print(f" Invalid BME280 Humidity: {bme_humidity}") if not valid_bme_press: print(f" Invalid BME280 Pressure: {bme_pressure}") except Exception as e: print(f"BME280: Error during reading: {e}") else: print("BME280: Not initialized.") # Wait before next set of readings time.sleep(5) # Read every 5 seconds except KeyboardInterrupt: print("\nExiting script.") finally: # Clean up sensor objects if they have an exit method if dht_sensor: try: dht_sensor.exit() except AttributeError: pass # BME280 basic library usually doesn't require explicit exit/deinit for I2C object # if i2c object was created locally, it would go out of scope. # If board.I2C() holds resources, they are managed by the board module. print("Script finished.")
MIN_READ_INTERVAL
: Theadafruit_dht
library internally definesMIN_READ_INTERVAL
(e.g., 2 seconds for DHT22). It's good practice to respect this. I used a slightly more directtime.sleep(2.0)
in the earlier example, but referring todht_sensor.MIN_READ_INTERVAL
if available and documented for your library version is cleaner. For this script, a simpletime.sleep(2)
inside the retry and a longertime.sleep(5)
for the main loop is fine. Corrected the DHT retry sleep above.*
- Using
-
Run the Script:
- In the terminal (virtual environment active):
- Observe the Output:
- You should see initialization messages for both sensors.
- Then, in a loop, it will print the current time and readings from DHT22 and BME280.
- Look for any error messages or warnings about invalid data.
- Troubleshooting:
- DHT22 Issues: Refer to the previous workshop's troubleshooting. Common issues are wiring, missing pull-up (if not on module), or the sensor being finicky.
- BME280 Issues:
- "Error initializing I2C or BME280 sensor: [Errno 2] No such file or directory: '/dev/i2c-1'": I2C is likely not enabled. Run
sudo raspi-config
, go toInterface Options
, enableI2C
, and reboot. - "Error initializing I2C or BME280 sensor: Remote I/O error": This often means the sensor is not detected at the specified I2C address (
BME280_I2C_ADDRESS
).- Run
sudo i2cdetect -y 1
again. If the address is different (e.g.,76
instead of77
), updateBME280_I2C_ADDRESS
in your script. - If no address shows up, re-check BME280 wiring (VCC, GND, SDA to Pi's SDA, SCL to Pi's SCL). Ensure SDA and SCL are not swapped.
- Faulty sensor or module.
- Run
- Readings are wildly off or consistently
None
(after successful init): Could be a faulty sensor, or sometimes a very unstable power supply can affect I2C devices.
- "Error initializing I2C or BME280 sensor: [Errno 2] No such file or directory: '/dev/i2c-1'": I2C is likely not enabled. Run
- General Python errors (
ImportError
, etc.): Ensure libraries are installed in the active virtual environment.
-
Stop the Script: Press
Ctrl + C
in the terminal.
This workshop gives you a robust script to read from multiple types of sensors. You've practiced sensor initialization, data reading with retries, and basic data validation. This forms the core data acquisition part of your weather station. The next step will be to store this data.
7. Storing Weather Data
Collecting sensor data is one part of the puzzle; storing it effectively for later analysis, visualization, or long-term record-keeping is equally important. This section explores different strategies for data logging, focusing on two common methods: storing data in Comma-Separated Values (CSV) files and using a simple relational database, SQLite.
Data Logging Strategies
When deciding how to store your weather data, consider these factors:
- Simplicity vs. Power: How easy is it to implement and manage? How powerful are the query and analysis capabilities?
- Data Volume: How much data will be generated? Small amounts can fit in simple files; large volumes might need a database.
- Data Structure: Is the data simple (e.g., timestamp, temp, humidity, pressure) or more complex?
- Accessibility: How will you access the data later? Directly on the Pi? Copied to another machine? Via a web interface?
- Concurrency: Will multiple processes need to write or read data simultaneously? (Less of a concern for our simple weather station, but important for larger systems).
- SD Card Wear: Frequent writes to the SD card can reduce its lifespan. This is a key consideration for long-running Raspberry Pi projects.
Common Strategies:
-
Plain Text Files (e.g., CSV, TSV):
- Pros: Very simple to implement, human-readable, easily imported into spreadsheets (Excel, Google Sheets, LibreOffice Calc) and data analysis tools (Pandas in Python, R).
- Cons: Can become unwieldy for very large datasets. Querying specific data can be inefficient (requires reading and parsing the whole file or large parts of it). No built-in data integrity checks or schema enforcement.
- Best for: Smaller datasets, simple logging, easy export.
-
SQLite Database:
- Pros: Lightweight, serverless, file-based relational database. Stores data in a single file. Supports SQL for powerful querying and data manipulation. Provides data types, indexing (for faster lookups), and transactions (for data integrity). Good for structured data. Python has built-in support (
sqlite3
module). - Cons: While lightweight, it's more complex than plain text files. For extremely high write volumes, SD card wear can still be a concern, though SQLite has mechanisms to mitigate this (e.g., journaling modes, commit intervals).
- Best for: Structured data, projects requiring querying and filtering, moderate data volumes where a full-fledged database server (like PostgreSQL or MySQL) is overkill.
- Pros: Lightweight, serverless, file-based relational database. Stores data in a single file. Supports SQL for powerful querying and data manipulation. Provides data types, indexing (for faster lookups), and transactions (for data integrity). Good for structured data. Python has built-in support (
-
Time-Series Databases (e.g., InfluxDB, Prometheus):
- Pros: Optimized specifically for handling time-stamped data. Excellent for high-volume writes and complex time-based queries (e.g., "average temperature over the last hour," "max pressure yesterday"). Often used with visualization tools like Grafana.
- Cons: More complex to set up and manage than CSV or SQLite. Might be overkill for a very simple mini weather station but are the standard for more serious IoT data logging.
- Best for: Serious IoT applications, performance monitoring, when advanced time-series analysis is needed.
-
Cloud-Based IoT Platforms (e.g., AWS IoT, Google Cloud IoT Core, Adafruit IO, ThingSpeak):
- Pros: Offloads storage and often provides dashboards, analytics, and alert features. Scalable. Data accessible from anywhere.
- Cons: Requires internet connectivity. Usually involves subscription costs (though many have free tiers for limited use). Data is stored off-site, which might be a concern for privacy or control.
- Best for: Projects needing remote access, cloud-based dashboards, and integration with other cloud services.
For this workshop, we will focus on CSV files and SQLite as they provide a good balance of simplicity, utility, and learning value for a Raspberry Pi project.
Storing Data in CSV Files
CSV (Comma-Separated Values) is a text file format where data is organized in rows, with values in each row separated by commas. The first row often contains headers describing each column.
Python's csv
Module:
Python's built-in csv
module makes reading and writing CSV files easy.
Example Structure for our Weather Data CSV:
timestamp,dht_temperature_c,dht_humidity_percent,bme_temperature_c,bme_humidity_percent,bme_pressure_hpa
2023-10-27 10:00:00,22.5,45.3,22.7,44.9,1012.5
2023-10-27 10:05:00,22.6,45.1,22.8,44.8,1012.3
...
Writing to a CSV File:
import csv
import os
from datetime import datetime
DATA_FILE = "weather_log.csv"
FIELD_NAMES = [
"timestamp",
"dht_temperature_c", "dht_humidity_percent",
"bme_temperature_c", "bme_humidity_percent", "bme_pressure_hpa"
]
# Check if file exists to write headers only once
file_exists = os.path.isfile(DATA_FILE)
with open(DATA_FILE, mode='a', newline='', encoding='utf-8') as f:
csv_writer = csv.DictWriter(f, fieldnames=FIELD_NAMES)
if not file_exists:
csv_writer.writeheader() # Write header row if file is new
# Assume you have sensor readings:
# dht_temp, dht_hum = read_dht_sensor()
# bme_temp, bme_hum, bme_press = read_bme_sensor()
# Example dummy data for now
current_ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dht_temp_val = 22.5
dht_hum_val = 45.3
bme_temp_val = 22.7
bme_hum_val = 44.9
bme_press_val = 1012.5
row_data = {
"timestamp": current_ts,
"dht_temperature_c": dht_temp_val if dht_temp_val is not None else '', # Handle None from sensor
"dht_humidity_percent": dht_hum_val if dht_hum_val is not None else '',
"bme_temperature_c": bme_temp_val if bme_temp_val is not None else '',
"bme_humidity_percent": bme_hum_val if bme_hum_val is not None else '',
"bme_pressure_hpa": bme_press_val if bme_press_val is not None else ''
}
csv_writer.writerow(row_data)
print(f"Data logged to {DATA_FILE}")
Key Points for CSV Logging:
- Mode
'a'
(Append): Opens the file in append mode, so new data is added to the end. If the file doesn't exist, it's created. newline=''
: Important on some systems (like Windows) to prevent blank rows from being inserted.csv.DictWriter
: Convenient for writing rows from dictionaries, where keys matchfieldnames
.- Writing Headers: Check if the file already exists (e.g., using
os.path.isfile()
) before writing the header row to avoid duplicate headers. - Handling Missing Data: If a sensor fails to provide a reading (returns
None
), decide how to represent this in the CSV (e.g., an empty string''
, "N/A", or leave the field blank if yourDictWriter
handles it, though explicit empty string is safer). - File Locking (Advanced): If multiple scripts might try to write to the same CSV simultaneously (not typical for our simple case), file locking mechanisms would be needed to prevent corruption.
Introduction to SQLite Databases
SQLite is a C library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine. It's incredibly popular and widely used in applications, browsers, and embedded systems like the Raspberry Pi.
Why SQLite for the Pi?
- Lightweight: The entire database is stored in a single file on disk (e.g.,
weather.db
). No separate server process is needed. - Built-in Python Support: The
sqlite3
module is part of Python's standard library, so no extra installation is required to use it. - SQL Compliant: Supports a rich subset of SQL, allowing for structured data storage, complex queries, indexing for performance, and relationships between tables (though we'll use a single table for simplicity here).
- Transactional (ACID): Ensures that operations are Atomic, Consistent, Isolated, and Durable, which helps protect data integrity even if the program crashes or power is lost during a write (assuming the OS and hardware also cooperate).
- Portability: The database file can be easily copied and used on other systems that understand SQLite.
Basic SQLite Concepts:
- Database: The single file that holds all your data (e.g.,
weather_station.db
). - Table: Organizes data in rows and columns, similar to a spreadsheet. Each column has a specific data type.
- Example Table:
weather_readings
- Example Table:
- Columns (Fields): Define the attributes of your data (e.g.,
timestamp
,temperature
,humidity
).- Data Types in SQLite:
NULL
,INTEGER
,REAL
(floating-point),TEXT
,BLOB
(binary data).
- Data Types in SQLite:
- Rows (Records): Individual entries in a table (e.g., one set of sensor readings at a specific time).
- Primary Key: A unique identifier for each row in a table (e.g., an auto-incrementing integer ID, or the timestamp if guaranteed unique).
- SQL (Structured Query Language): The language used to interact with the database (create tables, insert data, query data, update data, delete data).
Setting up SQLite on the Pi
SQLite is usually pre-installed on Raspberry Pi OS as part of the standard system utilities and Python installation. You can interact with SQLite databases using:
- The
sqlite3
command-line shell:- Open a terminal and type
sqlite3 mydatabase.db
. This will open or createmydatabase.db
and give you ansqlite>
prompt. - You can then type SQL commands directly.
- Type
.help
for commands,.tables
to list tables,.schema tablename
to see table structure,.quit
to exit.
- Open a terminal and type
- Python's
sqlite3
module: This is what we'll primarily use.
Designing our weather_readings
Table:
Column Name | Data Type | Constraints | Description |
---|---|---|---|
id |
INTEGER |
PRIMARY KEY AUTOINCREMENT |
Unique ID for each reading |
timestamp |
TEXT |
NOT NULL UNIQUE |
ISO 8601 format timestamp (e.g., "YYYY-MM-DD HH:MM:SS") |
dht_temperature_c |
REAL |
Temperature from DHT22 (°C) | |
dht_humidity_percent |
REAL |
Humidity from DHT22 (%) | |
bme_temperature_c |
REAL |
Temperature from BME280 (°C) | |
bme_humidity_percent |
REAL |
Humidity from BME280 (%) | |
bme_pressure_hpa |
REAL |
Pressure from BME280 (hPa) |
PRIMARY KEY AUTOINCREMENT
forid
means SQLite will automatically assign a unique, increasing integer to each new row.timestamp
asTEXT
andNOT NULL UNIQUE
ensures we record when the reading was taken and try to avoid duplicate entries for the same exact second. SQLite also hasDATETIME
types, but storing as ISO8601 text is common and portable.REAL
is used for sensor values as they can have decimal points.- Columns for sensor readings can be
NULL
if a sensor fails to provide a reading.
Creating the Table and Inserting Data with Python:
import sqlite3
from datetime import datetime
DB_FILE = "weather_station.db"
def create_connection(db_file):
"""Create a database connection to the SQLite database specified by db_file"""
conn = None
try:
conn = sqlite3.connect(db_file)
# Set journal mode to WAL for potentially better concurrency and SD card wear
conn.execute("PRAGMA journal_mode=WAL;")
print(f"SQLite DB connection established to {db_file} (version: {sqlite3.sqlite_version})")
except sqlite3.Error as e:
print(e)
return conn
def create_table(conn):
"""Create the weather_readings table if it doesn't exist"""
sql_create_weather_table = """
CREATE TABLE IF NOT EXISTS weather_readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL UNIQUE,
dht_temperature_c REAL,
dht_humidity_percent REAL,
bme_temperature_c REAL,
bme_humidity_percent REAL,
bme_pressure_hpa REAL
);
"""
try:
cursor = conn.cursor()
cursor.execute(sql_create_weather_table)
conn.commit()
print("Table 'weather_readings' ensured to exist.")
except sqlite3.Error as e:
print(f"Error creating table: {e}")
def log_data_to_db(conn, data_tuple):
"""Log a new weather reading to the weather_readings table"""
sql = ''' INSERT INTO weather_readings(timestamp,
dht_temperature_c, dht_humidity_percent,
bme_temperature_c, bme_humidity_percent, bme_pressure_hpa)
VALUES(?,?,?,?,?,?) ''' # Placeholders for data
try:
cursor = conn.cursor()
cursor.execute(sql, data_tuple)
conn.commit() # Commit changes to the database file
return cursor.lastrowid # Returns the ID of the inserted row
except sqlite3.IntegrityError as e:
# This can happen if timestamp is not unique
print(f"SQLite IntegrityError (likely duplicate timestamp): {e}")
return None
except sqlite3.Error as e:
print(f"Error inserting data: {e}")
return None
# --- Main execution example ---
if __name__ == "__main__":
conn = create_connection(DB_FILE)
if conn is not None:
create_table(conn) # Ensure table exists
# Assume you have sensor readings (dummy data for this example)
current_ts_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
dht_temp = 23.1
dht_hum = 46.5
bme_temp = 23.3
bme_hum = 45.9
bme_press = 1011.2
# Prepare data as a tuple in the correct order for the SQL query
# Handle None values appropriately for database (SQLite stores them as NULL)
reading_data = (
current_ts_str,
dht_temp if dht_temp is not None else None,
dht_hum if dht_hum is not None else None,
bme_temp if bme_temp is not None else None,
bme_hum if bme_hum is not None else None,
bme_press if bme_press is not None else None
)
row_id = log_data_to_db(conn, reading_data)
if row_id:
print(f"Data logged to SQLite DB, row ID: {row_id}")
# Example: Querying last 5 entries
print("\nLast 5 readings from DB:")
cursor = conn.cursor()
cursor.execute("SELECT * FROM weather_readings ORDER BY timestamp DESC LIMIT 5")
rows = cursor.fetchall()
for row in rows:
print(row)
conn.close()
print("SQLite DB connection closed.")
else:
print("Error! Cannot create the database connection.")
Key sqlite3
Operations:
sqlite3.connect(DB_FILE)
: Connects to (or creates if not exists) the database file.PRAGMA journal_mode=WAL;
: Write-Ahead Logging (WAL) mode can improve performance and concurrency, and potentially reduce SD card wear compared to the default rollback journal, especially for applications with more writes than reads or concurrent access. It's a good practice for embedded systems.
conn.cursor()
: Creates a cursor object to execute SQL commands.cursor.execute(sql_string, [parameters_tuple])
: Executes an SQL command. Use?
placeholders for data to prevent SQL injection vulnerabilities.conn.commit()
: Saves changes (INSERT, UPDATE, DELETE, CREATE TABLE) to the database file. Without this, changes are not persisted.cursor.fetchall()
: Fetches all rows from a SELECT query result.cursor.fetchone()
: Fetches one row.conn.close()
: Closes the database connection. It's important to close connections when done. Using awith sqlite3.connect(...) as conn:
context manager also ensures the connection is closed properly.
Choosing between CSV and SQLite depends on your project's needs. CSV is simpler for basic logging and easy export, while SQLite offers more robust storage, querying, and data integrity for structured data. For a long-running weather station, SQLite is generally a better choice.
Workshop Logging Sensor Data to a CSV File
Objective:
Modify the read_all_sensors.py
script (or create a new one) to log the collected temperature, humidity, and pressure data into a CSV file.
Materials Needed:
- Your Raspberry Pi with sensors wired and Python environment set up.
- The sensor reading script from the previous workshop (or be ready to integrate its logic).
Steps:
-
Prepare Your Script:
- Open your existing sensor reading script (e.g.,
read_all_sensors.py
) or create a new file, saylog_to_csv.py
. - If creating a new file, copy over the sensor initialization and reading functions/logic from
read_all_sensors.py
. - Ensure your virtual environment is active.
- Open your existing sensor reading script (e.g.,
-
Add CSV Logging Functionality:
- Import necessary modules at the top of your script:
- Define the CSV filename and field names (column headers):
-
Modify your main loop (the
while True:
loop) to include CSV writing:Make sure to integrate your full sensor reading logic (with retries for DHT if needed) and the validation functions properly. The snippet above simplifies that part for focus on CSV. Also, ensure sensor objects (# ... (sensor initialization code from read_all_sensors.py) ... print("\nStarting sensor readings and logging to CSV (Press Ctrl+C to stop):") try: # Check if file exists to write headers only once # Do this *before* the loop if you open/close the file inside the loop, # or keep the file open across loop iterations. # For simplicity here, we open/close each time, so check is inside. while True: current_dt_object = datetime.now() current_ts_str = current_dt_object.strftime("%Y-%m-%d %H:%M:%S") print("-" * 30) print(f"Time: {current_ts_str}") # --- Read DHT22 Sensor Data --- dht_temp_c, dht_humidity = None, None # Initialize with None if dht_sensor: # ... (your DHT22 reading logic with retries) ... # For this workshop, let's assume you have: # dht_temp_c = ... # dht_humidity = ... # (Simplified for brevity here, refer to your full DHT reading code) try: dht_temp_c = dht_sensor.temperature dht_humidity = dht_sensor.humidity if not (is_valid_temperature(dht_temp_c, "DHT22") and is_valid_humidity(dht_humidity, "DHT22")): dht_temp_c, dht_humidity = None, None # Invalidate if checks fail else: print(f"DHT22: Temp={dht_temp_c:.1f}°C, Humidity={dht_humidity:.1f}%") except RuntimeError: # Add more specific error handling if needed print("DHT22: Failed to get reading.") dht_temp_c, dht_humidity = None, None else: print("DHT22: Not initialized.") # --- Read BME280 Sensor Data --- bme_temp_c, bme_humidity, bme_pressure = None, None, None # Initialize if bme_sensor: # ... (your BME280 reading logic) ... # (Simplified for brevity here, refer to your full BME280 reading code) try: bme_temp_c = bme_sensor.temperature bme_humidity = bme_sensor.humidity bme_pressure = bme_sensor.pressure if not (is_valid_temperature(bme_temp_c, "BME280") and \ is_valid_humidity(bme_humidity, "BME280") and \ is_valid_pressure(bme_pressure, "BME280")): bme_temp_c, bme_humidity, bme_pressure = None, None, None # Invalidate else: print(f"BME280: Temp={bme_temp_c:.1f}°C, Humidity={bme_humidity:.1f}%, Pressure={bme_pressure:.1f} hPa") except Exception: # Add more specific error handling if needed print("BME280: Failed to get reading.") bme_temp_c, bme_humidity, bme_pressure = None, None, None else: print("BME280: Not initialized.") # --- Prepare data for CSV --- row_data = { "timestamp": current_ts_str, "dht_temperature_c": f"{dht_temp_c:.1f}" if dht_temp_c is not None else '', "dht_humidity_percent": f"{dht_humidity:.1f}" if dht_humidity is not None else '', "bme_temperature_c": f"{bme_temp_c:.1f}" if bme_temp_c is not None else '', "bme_humidity_percent": f"{bme_humidity:.1f}" if bme_humidity is not None else '', "bme_pressure_hpa": f"{bme_pressure:.1f}" if bme_pressure is not None else '' } # --- Write to CSV File --- try: file_exists = os.path.isfile(CSV_FILE) with open(CSV_FILE, mode='a', newline='', encoding='utf-8') as f: csv_writer = csv.DictWriter(f, fieldnames=CSV_FIELD_NAMES) if not file_exists: csv_writer.writeheader() csv_writer.writerow(row_data) print(f"Data appended to {CSV_FILE}") except IOError as e: print(f"IOError writing to CSV: {e}") except Exception as e: print(f"Unexpected error writing to CSV: {e}") time.sleep(300) # Log data every 5 minutes (300 seconds) except KeyboardInterrupt: print("\nExiting script.") finally: # ... (sensor cleanup code dht_sensor.exit(), etc.) ... print("Script finished.") # --- (You'll also need the validation functions is_valid_temperature etc. from previous script) --- # Placeholder for validation functions (copy from read_all_sensors.py) def is_valid_temperature(temp_c, sensor_name="Sensor"): if temp_c is None: return False if not (-40 <= temp_c <= 85): return False return True def is_valid_humidity(hum_percent, sensor_name="Sensor"): if hum_percent is None: return False if not (0 <= hum_percent <= 100): return False return True def is_valid_pressure(pressure_hpa, sensor_name="Sensor"): if pressure_hpa is None: return False if not (800 <= pressure_hpa <= 1100): return False return True
dht_sensor
,bme_sensor
) are initialized before the loop.
-
Run the Script:
- Save your
log_to_csv.py
file. - Execute it from the terminal (virtual environment active):
- The script will start reading sensor data and printing it to the console. Every 5 minutes (or your chosen interval), it should also print "Data appended to weather_data_log.csv".
- Let it run for a few cycles (e.g., 10-15 minutes to get 2-3 entries).
- Press
Ctrl + C
to stop the script.
- Save your
-
Inspect the CSV File:
- In the terminal, list files to see
weather_data_log.csv
: - View its contents:
- You should see a header row followed by rows of data, each with a timestamp and sensor readings. If a sensor failed, its fields should be empty (or whatever you specified for
None
values).
- In the terminal, list files to see
-
(Optional) Open CSV in a Spreadsheet Program:
- If you have a desktop environment on your Pi, you can try opening it with LibreOffice Calc.
- Alternatively, copy the CSV file to your main computer (using
scp
if you have SSH access, or a USB drive) and open it with Excel, Google Sheets, etc.
You have now successfully logged sensor data to a CSV file! This is a simple yet effective way to store your weather station's measurements.
Workshop Logging Sensor Data to an SQLite Database
Objective:
Modify your sensor reading script to log data into an SQLite database, creating the table structure and inserting new readings.
Materials Needed:
- Your Raspberry Pi with sensors wired and Python environment set up.
- The sensor reading script.
Steps:
-
Prepare Your Script:
- Open your sensor reading script (e.g.,
read_all_sensors.py
orlog_to_csv.py
) or create a new file, saylog_to_sqlite.py
. - If creating a new file, copy over the sensor initialization and reading logic.
- Ensure your virtual environment is active.
- Open your sensor reading script (e.g.,
-
Add SQLite Functionality:
- Import necessary modules at the top:
-
Define the database filename and the SQLite helper functions (
create_connection
,create_table
,log_data_to_db
) as shown in the "Setting up SQLite on the Pi" theory section. Copy these functions into your script.DB_FILE = "weather_station_data.db" def create_connection(db_file): # ... (copy from theory section) conn = None try: conn = sqlite3.connect(db_file) conn.execute("PRAGMA journal_mode=WAL;") except sqlite3.Error as e: print(f"SQLite connection error: {e}") return conn def create_table(conn): # ... (copy from theory section) sql_create_weather_table = """ CREATE TABLE IF NOT EXISTS weather_readings ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL UNIQUE, dht_temperature_c REAL, dht_humidity_percent REAL, bme_temperature_c REAL, bme_humidity_percent REAL, bme_pressure_hpa REAL );""" try: cursor = conn.cursor() cursor.execute(sql_create_weather_table) conn.commit() except sqlite3.Error as e: print(f"SQLite table creation error: {e}") def log_data_to_db(conn, data_tuple): # ... (copy from theory section) sql = ''' INSERT INTO weather_readings(timestamp, dht_temperature_c, dht_humidity_percent, bme_temperature_c, bme_humidity_percent, bme_pressure_hpa) VALUES(?,?,?,?,?,?) ''' try: cursor = conn.cursor() cursor.execute(sql, data_tuple) conn.commit() return cursor.lastrowid except sqlite3.IntegrityError: # Likely duplicate timestamp print(f"SQLite IntegrityError: Failed to insert row for timestamp {data_tuple[0]}. Already exists?") return None except sqlite3.Error as e: print(f"SQLite insert error: {e}") return None
-
In your main part of the script (before the
while True:
loop):- Establish a database connection.
- Ensure the table is created.
Again, ensure full sensor reading logic and validation functions are integrated. Ensure sensor objects
# ... (sensor initialization code) ... # --- Initialize Database --- db_conn = create_connection(DB_FILE) if db_conn is None: print("CRITICAL: Could not connect to SQLite database. Exiting.") # Potentially exit script or handle differently # For this workshop, we'll allow it to proceed but log functions won't work else: create_table(db_conn) # Ensure table exists print("\nStarting sensor readings and logging to SQLite (Press Ctrl+C to stop):") try: while True: current_dt_object = datetime.now() current_ts_str = current_dt_object.strftime("%Y-%m-%d %H:%M:%S") print("-" * 30) print(f"Time: {current_ts_str}") # --- Read DHT22 Sensor Data --- dht_temp_c, dht_humidity = None, None # ... (Full DHT22 reading logic as in CSV workshop, assign to dht_temp_c, dht_humidity) ... if dht_sensor: try: dht_temp_c = dht_sensor.temperature dht_humidity = dht_sensor.humidity if not (is_valid_temperature(dht_temp_c, "DHT22") and is_valid_humidity(dht_humidity, "DHT22")): dht_temp_c, dht_humidity = None, None else: print(f"DHT22: Temp={dht_temp_c:.1f}°C, Humidity={dht_humidity:.1f}%") except RuntimeError: dht_temp_c, dht_humidity = None, None # --- Read BME280 Sensor Data --- bme_temp_c, bme_humidity, bme_pressure = None, None, None # ... (Full BME280 reading logic as in CSV workshop, assign to bme_temp_c, etc.) ... if bme_sensor: try: bme_temp_c = bme_sensor.temperature bme_humidity = bme_sensor.humidity bme_pressure = bme_sensor.pressure if not (is_valid_temperature(bme_temp_c, "BME280") and \ is_valid_humidity(bme_humidity, "BME280") and \ is_valid_pressure(bme_pressure, "BME280")): bme_temp_c, bme_humidity, bme_pressure = None, None, None else: print(f"BME280: Temp={bme_temp_c:.1f}°C, Humidity={bme_humidity:.1f}%, Pressure={bme_pressure:.1f} hPa") except Exception: bme_temp_c, bme_humidity, bme_pressure = None, None, None # --- Prepare data for SQLite --- # Tuple order must match the VALUES(?,?,?,?,?,?) placeholders in log_data_to_db reading_data_tuple = ( current_ts_str, dht_temp_c, dht_humidity, # Python None is stored as SQL NULL bme_temp_c, bme_humidity, bme_pressure ) # --- Log to SQLite Database --- if db_conn: # Only attempt if connection was successful row_id = log_data_to_db(db_conn, reading_data_tuple) if row_id: print(f"Data logged to SQLite, row ID: {row_id}") # else: (Error message already printed in log_data_to_db) else: print("SQLite connection not available. Cannot log data.") time.sleep(300) # Log data every 5 minutes except KeyboardInterrupt: print("\nExiting script.") finally: # ... (sensor cleanup code) ... if db_conn: db_conn.close() print("SQLite DB connection closed.") print("Script finished.") # --- (You'll also need the validation functions is_valid_temperature etc.) --- # Placeholder for validation functions (copy from read_all_sensors.py) def is_valid_temperature(temp_c, sensor_name="Sensor"): if temp_c is None: return False if not (-40 <= temp_c <= 85): return False return True def is_valid_humidity(hum_percent, sensor_name="Sensor"): if hum_percent is None: return False if not (0 <= hum_percent <= 100): return False return True def is_valid_pressure(pressure_hpa, sensor_name="Sensor"): if pressure_hpa is None: return False if not (800 <= pressure_hpa <= 1100): return False return True
dht_sensor
,bme_sensor
are initialized before the loop.
-
Run the Script:
- Save your
log_to_sqlite.py
file. - Execute it from the terminal:
- The script will connect to/create the database, ensure the table exists, and then start logging data every 5 minutes (or your interval).
- Watch for "Data logged to SQLite, row ID: ..." messages.
- Let it run for a few cycles.
- Press
Ctrl + C
to stop.
- Save your
-
Inspect the SQLite Database:
- Using
sqlite3
command-line tool:- In the terminal, type:
- At the
sqlite>
prompt:- List tables:
.tables
(should showweather_readings
) - Show table schema:
.schema weather_readings
- Select all data:
SELECT * FROM weather_readings;
- Select last 3 entries:
SELECT * FROM weather_readings ORDER BY timestamp DESC LIMIT 3;
- Count rows:
SELECT COUNT(*) FROM weather_readings;
- Exit:
.quit
- List tables:
- Using a GUI SQLite Browser (Optional):
- If you have a desktop on your Pi, you can install a tool like
DB Browser for SQLite
(sudo apt install sqlitebrowser -y
). - Or, copy
weather_station_data.db
to your main computer and use a similar tool there.
- If you have a desktop on your Pi, you can install a tool like
- Using
You've now successfully logged your weather data to a robust SQLite database. This provides a more structured and queryable way to store your information compared to CSV, especially for long-term data collection.
8. Automating Data Collection
Manually running your Python script every time you want to collect data is not practical for a weather station that's supposed to operate continuously. We need a way to automate the execution of our data logging script at regular intervals, even if we're not logged in, and ensure it starts automatically if the Raspberry Pi reboots. cron
is a time-based job scheduler in Unix-like operating systems (including Raspberry Pi OS) that is perfect for this task.
Using cron for Scheduled Tasks
cron
is a daemon (a background process) that runs commands or scripts at pre-scheduled times or intervals. Each user on the system can have their own crontab
(cron table), which is a file containing the schedule of jobs to be run.
Understanding Crontab Syntax:
A crontab entry consists of two parts:
- Schedule Expression (5 or 6 fields): Defines when the command should run.
- Command to be Executed: The actual command or script path.
The schedule expression has the following fields (separated by spaces):
* * * * * command_to_execute
┬ ┬ ┬ ┬ ┬
│ │ │ │ │
│ │ │ │ └───── day of the week (0 - 7) (Sunday is 0 or 7)
│ │ │ └─────────── month (1 - 12)
│ │ └───────────────── day of the month (1 - 31)
│ └─────────────────────── hour (0 - 23)
└───────────────────────────── minute (0 - 59)
Special Characters:
*
(asterisk): Matches any value (e.g.,*
in the hour field means "every hour").,
(comma): Specifies a list of values (e.g.,0,15,30,45
in the minute field means "at 0, 15, 30, and 45 minutes past the hour").-
(hyphen): Specifies a range of values (e.g.,9-17
in the hour field means "from 9 AM to 5 PM")./
(slash): Specifies a step value (e.g.,*/15
in the minute field means "every 15 minutes").
Examples:
0 * * * * /path/to/command
: Run command at minute 0 of every hour (i.e., hourly on the hour).*/10 * * * * /path/to/command
: Run command every 10 minutes.0 6 * * * /path/to/command
: Run command at 6:00 AM every day.30 14 * * 1-5 /path/to/command
: Run command at 2:30 PM on Monday to Friday.@reboot /path/to/command
: Run command once at startup (after reboot).
Editing Your Crontab:
- To edit your user's crontab, open a terminal and type:
- The first time you run this, it might ask you to choose a text editor (e.g., nano). Select your preferred editor.
- Add your cron job entries at the end of the file, one per line.
-
Save and exit the editor (Ctrl+O, Enter, Ctrl+X if using nano).
cron
will automatically read the changes. -
To list your current crontab entries:
- To remove all your crontab entries (use with caution!):
Important Considerations for Cron Jobs:
-
Full Paths:
Always use full absolute paths for commands and scripts in your crontab, as cron jobs run with a minimal environment and may not know thePATH
to your executables or scripts.- Find the full path to Python 3:
which python3
(e.g.,/usr/bin/python3
) - Find the full path to your script: e.g.,
/home/student/MyPythonScripts/log_to_sqlite.py
- Find the full path to Python 3:
-
Environment:
Cron jobs run with a very limited environment. They don't inherit your usual shell environment variables (likePATH
, or variables set in.bashrc
or.profile
).- If your script relies on a virtual environment, you must activate it within the cron command or have your script explicitly use the Python interpreter from the virtual environment.
- Example using venv's Python:
/home/student/MyPythonScripts/my_python_env/bin/python3 /home/student/MyPythonScripts/log_to_sqlite.py
-
Permissions:
(You'll need to log out and back in for these group changes to take effect).
The cron job runs with the permissions of the user whose crontab it is in. If your script needssudo
privileges (e.g., for certain hardware access, though well-written Python libraries for GPIO/I2C often handle this via user groups likegpio
,i2c
), you'd typically configure the script to not require interactive password entry or run it from the root user's crontab (sudo crontab -e
), which is generally less secure and should be avoided if possible. For sensor access, adding your user to thegpio
andi2c
groups is usually the preferred way: -
Output and Errors:
By default, any output (STDOUT) or errors (STDERR) from a cron job are emailed to the user. If you don't have local mail configured, this can fill up logs or go unnoticed. It's good practice to redirect output:>/dev/null 2>&1
: Redirects both STDOUT and STDERR to/dev/null
(silences all output).>> /path/to/logfile.log 2>&1
: Appends both STDOUT and STDERR to a specified log file. This is highly recommended for debugging.
-
Working Directory:
Cron jobs often start in the user's home directory. If your script expects to run from a specific directory (e.g., to find relative file paths), you shouldcd
to that directory as part of the cron command:*/5 * * * * cd /home/student/MyPythonScripts && /usr/bin/python3 log_to_sqlite.py >> /home/student/cron_weather.log 2>&1
- Or, better, make your Python script use absolute paths for any files it reads/writes, or construct paths relative to the script's own location.
Writing a Robust Data Collection Script
Before automating your script with cron
, ensure it's robust enough to run unattended:
- Error Handling:
Implement comprehensivetry-except
blocks for all sensor interactions, file/database operations, and any other part that might fail. Your script should not crash on a single sensor failure. - Logging:
Use Python'slogging
module to write status messages, sensor readings, and errors to a dedicated log file. This is crucial for diagnosing problems when the script runs in the background.# In your Python script import logging LOG_FILE = "/home/student/MyPythonScripts/weather_station.log" # Absolute path logging.basicConfig(filename=LOG_FILE, level=logging.INFO, # Or logging.DEBUG for more detail format='%(asctime)s - %(levelname)s - %(message)s') # ... later in script ... logging.info("Script started.") try: # temp = sensor.read_temp() logging.info(f"Temperature read: {temp}") except Exception as e: logging.error(f"Failed to read temperature: {e}", exc_info=True) # exc_info=True adds traceback
- Absolute Paths:
Ensure your script uses absolute paths for any files it accesses (e.g., SQLite database file, CSV file, its own log file), or constructs paths reliably (e.g., relative to the script's own location). - Resource Management:
Ensure files and database connections are properly closed (e.g., usingwith open(...)
orconn.close()
). - Configuration:
If you have configurable parameters (like sensor pins, logging interval), consider putting them at the top of the script or in a separate configuration file for easy modification. - Single Instance (Optional, Advanced):
For some applications, you might want to ensure only one instance of your script runs at a time (e.g., using a PID lock file). For a simple polling script run by cron, this is often not strictly necessary if the script execution time is much shorter than the cron interval.
Modified Script Example (Conceptual - for log_to_sqlite.py
):
Make sure paths in your actual log_to_sqlite.py
script (or whatever script you are automating) are absolute or correctly relative to the script's location if cron
changes the working directory.
# At the top of your log_to_sqlite.py or similar script:
import os
import logging
# --- Configuration for Robustness ---
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) # Gets the directory where the script itself is located
DB_FILE = os.path.join(SCRIPT_DIR, "weather_station_data.db") # Path to DB relative to script
LOG_FILE = os.path.join(SCRIPT_DIR, "weather_station_script.log") # Path to log file relative to script
# --- Setup Logging ---
# %(pathname)s gives full path of logging call, %(module)s just the filename
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO, # Change to logging.DEBUG for more verbose output
format='%(asctime)s - %(levelname)s - %(module)s - %(funcName)s - Line:%(lineno)d - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# --- Make sure your functions use these paths, e.g., create_connection(DB_FILE) ---
# ... rest of your script (sensor initialization, reading functions, DB functions) ...
# In your main execution block:
if __name__ == "__main__":
logging.info("Weather station script started.")
# Your sensor initialization with logging
try:
# ... initialize dht_sensor ...
if dht_sensor: logging.info("DHT sensor initialized successfully.")
else: logging.warning("DHT sensor failed to initialize.")
# ... initialize bme_sensor ...
if bme_sensor: logging.info("BME sensor initialized successfully.")
else: logging.warning("BME sensor failed to initialize.")
except Exception as e:
logging.critical(f"Fatal error during sensor initialization: {e}", exc_info=True)
# exit(1) # Or handle gracefully
db_conn = create_connection(DB_FILE) # create_connection should now use the global DB_FILE
if db_conn is None:
logging.critical("CRITICAL: Could not connect to SQLite database. Script will not log data.")
else:
create_table(db_conn)
logging.info("Database connection established and table ensured.")
# --- This part is different for cron. We don't need a while True loop if cron calls it periodically.
# --- The script will run once, log data, and then exit. Cron will call it again later.
current_dt_object = datetime.now()
current_ts_str = current_dt_object.strftime("%Y-%m-%d %H:%M:%S")
logging.info(f"Attempting readings at: {current_ts_str}")
# ... (Your sensor reading logic for dht_temp_c, dht_humidity, etc.) ...
# ... (Make sure to add logging within these reading attempts) ...
# Example for one sensor reading:
dht_temp_c, dht_humidity = None, None
if dht_sensor:
try:
dht_temp_c = dht_sensor.temperature
dht_humidity = dht_sensor.humidity
if dht_temp_c is not None and dht_humidity is not None:
logging.info(f"DHT22 Read: Temp={dht_temp_c:.1f}C, Hum={dht_humidity:.1f}%")
if not (is_valid_temperature(dht_temp_c, "DHT22") and is_valid_humidity(dht_humidity, "DHT22")):
logging.warning("DHT22 readings marked invalid after validation.")
dht_temp_c, dht_humidity = None, None
else:
logging.warning("DHT22 read returned None values.")
except RuntimeError as e:
logging.error(f"DHT22 RuntimeError during read: {e.args[0]}", exc_info=True)
except Exception as e:
logging.error(f"DHT22 Unexpected error during read: {e}", exc_info=True)
else:
logging.warning("DHT22 sensor not initialized, skipping read.")
# ... (Similar detailed logging for BME280 readings) ...
bme_temp_c, bme_humidity, bme_pressure = None, None, None
# ... (BME280 reading logic with logging) ...
reading_data_tuple = (
current_ts_str,
dht_temp_c, dht_humidity,
bme_temp_c, bme_humidity, bme_pressure
)
if db_conn:
row_id = log_data_to_db(db_conn, reading_data_tuple) # log_data_to_db should also log success/failure
if row_id:
logging.info(f"Data logged to SQLite, row ID: {row_id}")
else:
logging.error(f"Failed to log data to SQLite for timestamp {current_ts_str}.")
db_conn.close()
logging.info("Database connection closed.")
else:
logging.warning("SQLite connection not available. Data not logged.")
# Cleanup sensors
if dht_sensor:
try: dht_sensor.exit()
except AttributeError: pass
# BME280 does not typically need an exit call with the basic library
logging.info("Weather station script finished one cycle.")
cron
will be responsible for calling it repeatedly.
Workshop Automating Data Logging with cron
Objective:
To create a cron job that automatically runs your SQLite data logging script at a regular interval (e.g., every 5 minutes) and ensures it starts on boot.
Materials Needed:
- Your Raspberry Pi with the robust
log_to_sqlite.py
script (or your equivalent) ready. This script should:- Use Python's
logging
module to write to a file. - Use absolute paths or paths relative to the script's location for database and log files.
- Be designed to run once and exit (not an infinite loop).
- Have execute permissions (
chmod +x /path/to/your/script.py
). (This is mainly if you plan to run it directly like./script.py
. If you call it withpython3 script.py
, it's not strictly needed for the.py
file itself).
- Use Python's
Steps:
-
Prepare Your Python Script:
- Ensure your
log_to_sqlite.py
(or equivalent) is finalized and tested. - Note the absolute path to your Python 3 interpreter. In the terminal:
(e.g.,
/usr/bin/python3
) - Note the absolute path to your Python script. For example, if your username is
student
and the script is inMyPythonScripts
:/home/student/MyPythonScripts/log_to_sqlite.py
- If using a virtual environment, note the path to its Python interpreter:
/home/student/MyPythonScripts/my_python_env/bin/python3
- Ensure your
-
Ensure User is in
gpio
andi2c
groups (if not already done):- This allows your script, when run by your user via cron, to access hardware without
sudo
. - You must log out and log back in for these group changes to take effect. A reboot also works. If you don't, cron jobs might fail to access hardware.
- This allows your script, when run by your user via cron, to access hardware without
-
Open Your User's Crontab for Editing:
If prompted, choosenano
as the editor. -
Add Cron Job Entries:
- Go to the end of the file.
-
Job 1: Run the script every 5 minutes.
- Syntax:
minute hour day_of_month month day_of_week command
- To run every 5 minutes, the minute field will be
*/5
. - If NOT using a virtual environment:
- If USING a virtual environment (replace paths with your actual paths):
- Explanation of the line:
*/5 * * * *
: Run at any minute that is a multiple of 5 (0, 5, 10, ..., 55), every hour, every day, etc./path/to/python3_interpreter
: Absolute path to the Python interpreter./path/to/your/script.py
: Absolute path to your data logging script.>> /home/student/MyPythonScripts/cron_weather_job.log 2>&1
: This is important!>>
: Appends STDOUT (standard output) to the log file./home/student/MyPythonScripts/cron_weather_job.log
: The file where cron job output will be logged. Use an absolute path.2>&1
: Redirects STDERR (standard error) to the same place as STDOUT.- This
cron_weather_job.log
is for anyprint()
statements your script might still have or general cron execution issues. Your Python script's ownlogging
module will write toweather_station_script.log
(or whatever you named it). Both logs are useful.
- Syntax:
-
Job 2: Run the script on boot (optional, but good for a weather station).
- Add another line for the
@reboot
directive: - If NOT using a virtual environment:
- If USING a virtual environment:
Note: For
# Weather Station Data Logger - Runs on boot (with venv) @reboot /home/student/MyPythonScripts/my_python_env/bin/python3 /home/student/MyPythonScripts/log_to_sqlite.py >> /home/student/MyPythonScripts/cron_weather_job_reboot.log 2>&1
@reboot
jobs, sometimes there's a delay before network or all hardware is fully ready. A common practice is to add asleep
at the beginning of the cron command or within the script if it depends on network immediately at boot:@reboot sleep 60 && /path/to/python3 ...
(waits 60 seconds after boot before running). For local sensor reading, this might not be strictly necessary but doesn't hurt.
- Add another line for the
-
Save and Exit Crontab:
- If using
nano
:Ctrl + O
(WriteOut), Enter (to confirm filename),Ctrl + X
(Exit). - You should see a message like
crontab: installing new crontab
.
- If using
-
Verify Crontab:
You should see the lines you added. -
Monitor and Debug:
- Wait for the next 5-minute interval (e.g., if it's 10:02, wait until 10:05).
- Check your Python script's log file (e.g.,
/home/student/MyPythonScripts/weather_station_script.log
). You should see entries indicating the script ran and logged data. - Check the cron job's log file (e.g.,
/home/student/MyPythonScripts/cron_weather_job.log
). This will capture anyprint
statements from your script or errors if cron couldn't execute the script (e.g., path not found, permission denied for the script itself if you tried to execute it directly withoutpython3
and it didn't have+x
). - Check the SQLite database using the
sqlite3
command-line tool to see if new rows are being added. -
Check system logs for cron activity (advanced):
This will show entries whencron
starts jobs. -
Troubleshooting:
- Script not running:
- Double-check all paths in
crontab -l
. - Ensure your user is in
gpio
andi2c
groups AND you've logged out/in. - Check
cron_weather_job.log
for errors. - Is the Python interpreter path correct? Is the script path correct?
- Double-check all paths in
- Script runs but data not logged/errors in script log:
- Examine
weather_station_script.log
for Python errors (e.g., sensor read failures, DB write failures). - The issue is likely within your Python script's logic or its interaction with hardware/DB.
- Test the script manually again:
/path/to/python3_interpreter /path/to/your/script.py
. Does it work manually? If not, fix it there first.
- Examine
@reboot
job not running:- Reboot your Pi:
sudo reboot
. - After it comes back up, wait a couple of minutes, then check the logs (
cron_weather_job_reboot.log
andweather_station_script.log
) and the database. @reboot
jobs can be tricky if they depend on services that haven't started yet (like networking, though not an issue for local sensors). Thesleep 60 &&
trick can help.
- Reboot your Pi:
- Script not running:
-
(Optional) Test the
@reboot
functionality:- Comment out the 5-minute cron job temporarily (add
#
at the beginning of the line incrontab -e
) to avoid too many writes while testing reboot. - Reboot your Pi:
sudo reboot
. - After it boots up and you log in, wait a minute or two (especially if you added a
sleep
to the@reboot
command). - Check your script's log file (
weather_station_script.log
) and the database to see if a new entry was added. - Check the cron reboot log (
cron_weather_job_reboot.log
). - If it works, you can re-enable the 5-minute job in
crontab -e
.
- Comment out the 5-minute cron job temporarily (add
You have now successfully automated your data collection script using cron
. Your Raspberry Pi Mini Weather Station will diligently log data at regular intervals and after every reboot, operating autonomously!
9. Basic Data Visualization
Collecting and storing weather data is useful, but visualizing it can provide much quicker insights and make the data more engaging. While the Raspberry Pi can host complex web dashboards, this section will focus on a simpler approach: generating basic plots using the Matplotlib library in Python. These plots could then be viewed directly on the Pi's desktop, saved as image files, or even incorporated into a very simple web page served by Flask (though the Flask part will be a more advanced extension discussed briefly).
Introduction to Data Plotting with Matplotlib
Matplotlib is a comprehensive and powerful open-source plotting library for Python. It provides a wide variety of static, animated, and interactive visualizations in Python. Matplotlib makes it easy to generate plots, histograms, power spectra, bar charts, error charts, scatterplots, and much more, with just a few lines of code. It's highly versatile and is one of the cornerstones of the scientific Python ecosystem.
Why Matplotlib?
- Publication Quality Plots:
Capable of producing high-quality figures in various hardcopy formats and interactive environments. - Wide Range of Plot Types:
Supports numerous types of 2D and some 3D plots. - Customization:
Offers extensive control over every aspect of a figure, including colors, line styles, fonts, labels, and annotations. - Integration:
Works well with other scientific Python libraries like NumPy (for numerical data) and Pandas (for data analysis). - Community and Documentation:
Has a large, active community and excellent documentation, making it easy to find examples and help.
Installation:
If you are working within a Python virtual environment (which is highly recommended), ensure it is activated before running the installation command.
To install Matplotlib, you use pip
:
Matplotlib has several dependencies, such as NumPy, dateutil, and others, which pip
will typically handle and install automatically. For displaying plots in a graphical window (interactively), Matplotlib relies on backend GUI toolkits. Common ones include TkAgg (using Tkinter), Qt5Agg (using PyQt5 or PySide2), WXAgg (using wxPython), etc.
- On Raspberry Pi OS with a desktop, Tkinter (
python3-tk
) is usually available or easily installable: This allows Matplotlib to use the TkAgg backend to display plots in a window. - If you are running headless (e.g., Raspberry Pi OS Lite or via SSH without X11 forwarding) and only intend to save plots to files, you might not need a GUI toolkit. Matplotlib can use non-interactive backends like 'Agg' (Anti-Grain Geometry), which is excellent for rendering to PNG files, 'PDF' for PDF files, or 'SVG' for Scalable Vector Graphics. You can explicitly set a backend in your script if needed: However, for this workshop, we'll assume you might want to see the plot interactively first if possible.
Core Concepts and matplotlib.pyplot
:
The most common interface to Matplotlib is matplotlib.pyplot
, which is a state-based interface that provides a convenient way to create plots and visualizations, similar to MATLAB's plotting functions. It's typically imported as plt
:
Basic Plotting Workflow:
- Import
pyplot
: As shown above. - Prepare Data:
Your data for plotting should typically be in Python lists, NumPy arrays, or Pandas Series/DataFrames. For our weather station, we'll fetch data (e.g., timestamps and temperature readings) from our SQLite database. -
Create a Figure and Axes (Optional but Recommended for Control):
- A Figure is the top-level container for all plot elements.
- Axes (plural of Axis, not to be confused with x-axis/y-axis) are the actual plotting areas within a Figure. A Figure can contain one or more Axes objects (subplots).
You can create a figure and one or more axes explicitly:
Alternatively,
fig, ax = plt.subplots() # Creates a figure and a single set of axes # For multiple subplots: fig, axs = plt.subplots(2, 1) # 2 rows, 1 column
pyplot
functions often create them implicitly if they don't exist.
-
Plot Data:
Use functions likeax.plot()
,ax.scatter()
,ax.bar()
, etc., to draw data onto the Axes.# x_values and y_values are lists or NumPy arrays ax.plot(x_values, y_values, label="My Data Series", color="green", marker="x", linestyle="--")
label
: Used for the legend.color
: Sets the color of the line/markers.marker
: Specifies the marker style (e.g., 'o' for circle, 'x' for x, '.' for point).linestyle
: Specifies the line style (e.g., '-' for solid, '--' for dashed, ':' for dotted).
-
Customize the Plot: Add titles, labels, legends, grid lines, and adjust ticks.
ax.set_title("My Awesome Plot") ax.set_xlabel("Time (Hours)") ax.set_ylabel("Temperature (°C)") ax.legend() # Displays the legend (needs 'label' in plot calls) ax.grid(True) # Adds a grid # For customizing x-axis ticks, especially for dates: import matplotlib.dates as mdates ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M')) ax.xaxis.set_major_locator(mdates.HourLocator(interval=6)) # Tick every 6 hours plt.xticks(rotation=45, ha="right") # Rotate labels and align them
-
Display or Save the Plot:
plt.show()
:
If you're in an environment with a GUI backend (like a desktop session), this command will open a window displaying your plot. Execution of the script pauses until the plot window is closed.plt.savefig("filename.png")
:
Saves the current figure to a file. You can specify various formats (e.g., "plot.pdf", "plot.svg").dpi=300
: Can be added for higher resolution images.bbox_inches='tight'
: Often useful to ensure everything (labels, titles) fits in the saved image without being cropped.
plt.close(fig)
orplt.close('all')
:
If you are generating many plots in a loop, it's important to close figures after they are shown or saved to free up memory.plt.close(fig)
closes a specific figure object, whileplt.close('all')
closes all open figures.
Example: Plotting Simple Data
import matplotlib.pyplot as plt
# Sample data
x_coords = [1, 2, 3, 4, 5]
y_coords = [2, 3, 5, 7, 11] # Some prime numbers
# Create a figure and an axes
fig, ax = plt.subplots(figsize=(8, 5)) # figsize is width, height in inches
# Plot the data
ax.plot(x_coords, y_coords, marker='o', linestyle='-', color='blue', label='Prime Numbers')
# Customize the plot
ax.set_title('Simple Line Plot')
ax.set_xlabel('X-axis')
ax.set_ylabel('Y-axis')
ax.legend()
ax.grid(True)
# Save the plot
try:
plt.savefig('simple_plot.png', bbox_inches='tight')
print("Plot saved as simple_plot.png")
except Exception as e:
print(f"Error saving plot: {e}")
# Show the plot (uncomment if you have a display and want to see it interactively)
# plt.show()
# Close the figure
plt.close(fig)
This script, when run, would generate a file named simple_plot.png
in the same directory. If plt.show()
were uncommented and a GUI environment were available, it would also display the plot in a window. For our weather station, we will retrieve data from the SQLite database and plot time series data like temperature, humidity, or pressure against time.
Workshop Generating a Temperature Plot
Objective:
To create a Python script that fetches weather data from the SQLite database and uses Matplotlib to generate a line plot of temperature readings over time, saving the plot as an image file.
Materials Needed:
- Your Raspberry Pi with the SQLite database (
weather_station_data.db
or similar) populated with some data from previous logging. - Python environment with Matplotlib installed (and
python3-tk
if you want toplt.show()
). - Your activated virtual environment if you're using one.
Steps:
-
Prepare Your Environment:
- Ensure Matplotlib is installed:
- If you want to use
plt.show()
and are on a desktop environment, ensurepython3-tk
is installed:
-
Create a New Python Script (
plot_weather_data.py
):- In your project directory (e.g.,
MyPythonScripts
), create a new file namedplot_weather_data.py
. - Use
nano
or your preferred editor.
- In your project directory (e.g.,
-
Write the Python Script:
- Start by importing necessary modules:
# Filename: plot_weather_data.py import sqlite3 from datetime import datetime import matplotlib # Try to use a non-interactive backend if no display is available, # otherwise Matplotlib might try to use TkAgg and fail if python3-tk is not there or X11 not available. # This is especially important if you plan to run this script from cron later. try: # Check if a display is available (rudimentary check) import os os.environ['DISPLAY'] except KeyError: matplotlib.use('Agg') # Use 'Agg' backend for writing to file without GUI import matplotlib.pyplot as plt import matplotlib.dates as mdates # For formatting dates on the x-axis import os # For path joining
- Define the database file path (make sure this matches your actual database file):
# --- Configuration --- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) DB_FILE = os.path.join(SCRIPT_DIR, "weather_station_data.db") # Assumes DB is in the same directory as script PLOT_FILENAME = os.path.join(SCRIPT_DIR, "temperature_trend.png") NUM_RECORDS_TO_PLOT = 144 # e.g., for 12 hours of data if logging every 5 mins (12*12) # Adjust as needed, e.g., 288 for 24 hours
- Add the function to fetch data from SQLite (similar to the one in the theory section):
def fetch_sensor_data(db_path, sensor_column_name, limit): """ Fetches the most recent 'limit' records for a specific sensor from the SQLite database. Returns a list of dictionaries: [{'timestamp': datetime_obj, 'value': float_val}] """ conn = None records = [] print(f"Fetching data for {sensor_column_name} from {db_path}") try: conn = sqlite3.connect(db_path) cursor = conn.cursor() # Construct query safely (though sensor_column_name is from our own code here) if sensor_column_name not in ["dht_temperature_c", "dht_humidity_percent", "bme_temperature_c", "bme_humidity_percent", "bme_pressure_hpa"]: raise ValueError("Invalid sensor column name") query = f""" SELECT timestamp, {sensor_column_name} FROM weather_readings WHERE {sensor_column_name} IS NOT NULL ORDER BY timestamp DESC LIMIT ? """ cursor.execute(query, (limit,)) raw_records = cursor.fetchall() # Convert to more usable format, reverse to get chronological order for plotting for row in reversed(raw_records): try: ts_str, val_str = row[0], row[1] ts = datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S") val = float(val_str) records.append({'timestamp': ts, 'value': val}) except (ValueError, TypeError, IndexError) as e: print(f"Warning: Skipping row due to data conversion error: {row} - {e}") print(f"Fetched {len(records)} valid records for {sensor_column_name}.") except sqlite3.Error as e: print(f"SQLite error fetching data for {sensor_column_name}: {e}") except ValueError as e: print(f"Configuration error: {e}") finally: if conn: conn.close() return records
- Add the main plotting logic:
def main(): print(f"Attempting to generate plot: {PLOT_FILENAME}") # Fetch data for BME280 temperature temperature_data = fetch_sensor_data(DB_FILE, "bme_temperature_c", NUM_RECORDS_TO_PLOT) if not temperature_data: print("No temperature data found to plot.") return timestamps = [record['timestamp'] for record in temperature_data] temperatures = [record['value'] for record in temperature_data] # --- Create the Plot --- fig, ax = plt.subplots(figsize=(12, 6)) # Figure size: 12 inches wide, 6 inches high ax.plot(timestamps, temperatures, marker='.', linestyle='-', color='crimson', label="BME280 Temperature") # --- Customize the Plot --- ax.set_title(f'Temperature Trend (Last {len(temperatures)} Readings)') ax.set_xlabel('Time') ax.set_ylabel('Temperature (°C)') ax.legend() # Show legend ax.grid(True) # Add a grid # Format the x-axis to show dates/times nicely # AutoDateLocator will try to pick sensible tick locations # DateFormatter will format the tick labels ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M\n%Y-%m-%d')) # Hour:Minute on new line, then Date ax.xaxis.set_major_locator(mdates.AutoDateLocator(minticks=5, maxticks=10)) # Adjust number of ticks fig.autofmt_xdate(rotation=30, ha='right') # Automatically format x-dates: rotate labels, align right plt.tight_layout() # Adjust plot to ensure everything fits without overlapping # --- Save the Plot --- try: plt.savefig(PLOT_FILENAME, dpi=100) # dpi for resolution print(f"Plot successfully saved as {PLOT_FILENAME}") except Exception as e: print(f"Error saving plot: {e}") # --- Optionally, show the plot if a display is available --- # Check if backend is not 'Agg' before trying to show # if matplotlib.get_backend().lower() != 'agg': # print("Displaying plot...") # plt.show() # else: # print("Plot generated. To view, open the image file. (Display not available or 'Agg' backend used).") plt.close(fig) # Close the figure to free memory if __name__ == "__main__": main()
- Start by importing necessary modules:
-
Run the Script:
- Save the
plot_weather_data.py
file. - Make sure your database (
weather_station_data.db
) has some data. If not, run your data logging script for a while. - Execute the plotting script from the terminal (ensure virtual environment is active if used):
- Observe the Output:
- You should see messages about fetching data and saving the plot.
- If you uncommented
plt.show()
and have a desktop environment withpython3-tk
, a plot window should appear. Close it to let the script finish. - If using the 'Agg' backend (or no display), it will just save the file.
- Save the
-
Check the Output Image:
- In your project directory, you should now find an image file named
temperature_trend.png
. - If you have a desktop environment on your Pi, you can open it with an image viewer (e.g., right-click -> Open with Image Viewer).
- Alternatively, transfer the image file to your main computer (using
scp
, a USB drive, or a shared folder) and view it there. - The plot should show temperature readings against time, with labels, a title, and a grid.
- In your project directory, you should now find an image file named
-
Experiment (Optional):
- Modify
NUM_RECORDS_TO_PLOT
to see different time windows. - Try plotting other data:
- Change
sensor_column_name
infetch_sensor_data
call to"dht_temperature_c"
,"bme_humidity_percent"
, or"bme_pressure_hpa"
. - Adjust
ax.set_ylabel()
andax.set_title()
accordingly. - Save with a different
PLOT_FILENAME
.
- Change
- Multiple Lines on One Plot:
To plot both DHT and BME temperature on the same graph:
# In main(): dht_temp_data = fetch_sensor_data(DB_FILE, "dht_temperature_c", NUM_RECORDS_TO_PLOT) bme_temp_data = fetch_sensor_data(DB_FILE, "bme_temperature_c", NUM_RECORDS_TO_PLOT) if dht_temp_data: dht_timestamps = [r['timestamp'] for r in dht_temp_data] dht_temps = [r['value'] for r in dht_temp_data] ax.plot(dht_timestamps, dht_temps, marker='x', linestyle=':', color='dodgerblue', label="DHT22 Temperature") if bme_temp_data: bme_timestamps = [r['timestamp'] for r in bme_temp_data] bme_temps = [r['value'] for r in bme_temp_data] ax.plot(bme_timestamps, bme_temps, marker='.', linestyle='-', color='crimson', label="BME280 Temperature") # Ensure legend is called after all plot commands ax.legend() # ... rest of the plotting code ...
- Subplots:
To create separate plots for temperature and humidity in the same figure:
# In main(): # fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), sharex=True) # 2 rows, 1 col, shared x-axis # temp_data = fetch_sensor_data(DB_FILE, "bme_temperature_c", NUM_RECORDS_TO_PLOT) # hum_data = fetch_sensor_data(DB_FILE, "bme_humidity_percent", NUM_RECORDS_TO_PLOT) # if temp_data: # ax1.plot([r['timestamp'] for r in temp_data], [r['value'] for r in temp_data], color='crimson', label="Temperature") # ax1.set_ylabel('Temperature (°C)') # ax1.legend() # ax1.grid(True) # if hum_data: # ax2.plot([r['timestamp'] for r in hum_data], [r['value'] for r in hum_data], color='mediumblue', label="Humidity") # ax2.set_ylabel('Humidity (%)') # ax2.legend() # ax2.grid(True) # ax2.set_xlabel('Time') # X-label only on the bottom plot due to sharex # fig.suptitle('Weather Conditions', fontsize=16) # Overall title for the figure # ... formatting for x-axis on ax2 (since it's shared) ... # ... savefig, show, close ...
- Modify
You have now successfully generated a visual representation of your weather data! This script can be run manually whenever you want an updated plot, or you could even add a cron job to regenerate the plot image periodically if you plan to serve it via a simple web page later.