Skip to content
Author Nejat Hakan
eMail 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).
  • 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:

  1. Raspberry Pi Board:
    The specific model you've chosen (e.g., Raspberry Pi 4 Model B, Raspberry Pi 3 Model B+).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. (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.
  8. (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).
  9. (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.

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:

  1. 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.
  2. (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.
  3. 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.
  4. 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).
  5. 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.
  6. 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.
  7. 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.

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:

  1. 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).
  2. 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.
  3. 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.

  1. 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 (like config.txt and cmdline.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.
    • Why it's used for /boot:
      Its simplicity and cross-platform compatibility are key for the initial boot phase.
  2. 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.
    • Why it's used for / (root):
      Its robustness, support for Linux permissions, and overall performance make it ideal for the main operating system partition.

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:

  1. 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.
  2. Second Stage Bootloader (bootcode.bin on SD Card):
    The SoC Boot ROM looks for a FAT32 formatted partition on the SD card and loads bootcode.bin. This second-stage bootloader is more capable and initializes some basic hardware like SDRAM.
  3. 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 like config.txt (for hardware parameters) and cmdline.txt (for kernel boot arguments).
  4. Linux Kernel (kernel.img or kernel7.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 like systemd or SysVinit).

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:

  1. 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.
      sudo dphys-swapfile swapoff
      sudo dphys-swapfile uninstall
      sudo systemctl disable dphys-swapfile
      
    • Monitor RAM usage to ensure you're not running out of memory.
  2. 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.
  3. 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 as tmpfs (stored in RAM) or overlaid with a writable layer using overlayfs.
    • 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 like overlayroot or custom scripts can help manage a read-only root setup. Raspberry Pi OS has an option in raspi-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.
  4. 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.
  5. 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:

  1. 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.
  2. 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.
  3. 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 is raspberrypi.
      • Enable SSH:
        Crucial for headless access. Select "Enable SSH" and "Use password authentication."
      • Set username and password:
        The default username is pi. It's highly recommended to change this for security. You can set a new username (e.g., weatheradmin) and a strong password. If you keep pi, 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.

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.

  1. 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).
  2. 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.
  3. 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.
  4. 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 password raspberry 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.
  5. 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 (or pi/raspberry for very old images, though this is unlikely now).
  6. 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 about raspi-config.
    • Open a terminal and type:
      sudo raspi-config
      
    • 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.
    • After making changes in raspi-config, you'll often be prompted to reboot for them to take effect. Select <Finish> to exit.
  7. Update Your System:

    • It's always good practice to ensure your system is up-to-date. Open a terminal and run:
      sudo apt update
      sudo apt full-upgrade -y
      
    • 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:

  1. 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.
  2. 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.
  3. 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".
    • 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.
  4. 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.
  5. 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:
      sudo apt update
      
      (Wait for it to finish)
      sudo apt full-upgrade -y
      
      (This might take a while. The -y automatically confirms prompts.)
    • If it asks about restarting services, it's usually safe to allow it.
  6. Configure Interfaces using raspi-config:

    • In the terminal, type:
      sudo raspi-config
      
    • 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.
  7. Verify Network Connection and SSH (Optional but good practice):

    • After rebooting and logging in, open a terminal.
    • Find your Pi's IP address:
      hostname -I
      
      or
      ip addr show
      
      (Look for wlan0 if on Wi-Fi, or eth0 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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.
  10. 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:
      pwd
      
      Output might be /home/student (if your username is student). The / at the beginning indicates the root of the file system.
  • ls (List):

    • Lists the files and directories in the current working directory.
    • Example:
      ls
      
    • 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:
      cd Documents
      
      (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 just cd: 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 and Documents is inside, cd Documents (relative path).
  • mkdir (Make Directory):

    • Creates a new directory.
    • Example:
      mkdir MyProject
      
      (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:
      rmdir OldProject
      
  • cp (Copy):

    • Copies files or directories.
    • Syntax: cp [options] source destination
    • Example (copying a file):
      cp myfile.txt mybackup.txt
      
      (Copies myfile.txt to mybackup.txt in the current directory.)
      cp myfile.txt Documents/
      
      (Copies myfile.txt into the Documents directory.)
    • Common option (for directories):
      • cp -r SourceDirectory DestinationDirectory: Recursively copies a directory and its contents.
        cp -r MyProject MyProject_backup
        
  • mv (Move):

    • Moves files or directories. It's also used to rename files or directories.
    • Syntax: mv [options] source destination
    • Example (moving a file):
      mv report.docx FinalReports/
      
      (Moves report.docx into the FinalReports directory.)
    • Example (renaming a file):
      mv oldname.txt newname.txt
      
      (Renames oldname.txt to newname.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):
      rm unwantedfile.txt
      
    • 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 as rm -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.
  • 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:
      sudo apt update
      
      (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 the sudoers 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.

Other Useful Commands:

  • man command_name: Displays the manual page for a command (e.g., man ls). Press q 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 than cat 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 or htop: Displays running processes and system resource usage (CPU, memory). htop is more user-friendly and may need to be installed (sudo apt install htop). Press q 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):

  1. 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 before upgrade or install.
  2. 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.
  3. sudo apt full-upgrade (or sudo 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.
  4. sudo apt install <package_name>

    • Installs a new package.
    • Example:
      sudo apt install htop
      
      (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.
  5. sudo apt remove <package_name>

    • Removes a package but leaves its configuration files on the system.
    • Example:
      sudo apt remove htop
      
  6. sudo apt purge <package_name>

    • Removes a package AND its configuration files.
    • Example:
      sudo apt purge htop
      
  7. 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.
  8. apt search <keyword>

    • Searches the package lists for packages matching the keyword.
    • Example:
      apt search "text editor"
      
  9. apt show <package_name>

    • Shows detailed information about a package, such as its version, size, dependencies, and description.
    • Example:
      apt show python3
      

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:

  1. Opening or Creating a File:

    • To open an existing file or create a new one, type:
      nano filename.txt
      
    • 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:
      sudo nano /etc/some_config_file.conf
      
  2. 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) or M- (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).
  3. Basic Editing:

    • Use arrow keys to navigate.
    • Type to insert text.
    • Backspace/Delete work as expected.
  4. 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.
  5. 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.

Example: Creating a simple text file:

  1. In your terminal, type:
    cd ~  # Go to your home directory
    nano mysample.txt
    
  2. Type a few lines of text, e.g.:
    Hello from nano!
    This is a test file.
    Learning Linux commands.
    
  3. Press Ctrl + O. The filename mysample.txt should be displayed. Press Enter to save.
  4. Press Ctrl + X to exit nano.
  5. You can view the file content using cat:
    cat mysample.txt
    

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:

  1. 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.
  2. Explore Your Home Directory:

    • Confirm your current location:
      pwd
      
      (It should be something like /home/your_username)
    • List files and directories:
      ls
      
    • List in long format with hidden files:
      ls -lah
      
      (Notice files like .bashrc, .profile.)
  3. Create and Navigate Directories:

    • Create a new directory for this workshop:
      mkdir WeatherStationProject
      
    • Verify it was created:
      ls
      
    • Navigate into the new directory:
      cd WeatherStationProject
      
    • Confirm your new location:
      pwd
      
      (Should now be /home/your_username/WeatherStationProject)
    • Create a subdirectory for sensor scripts:
      mkdir SensorScripts
      
    • Create another for data logs:
      mkdir DataLogs
      
    • List the contents of WeatherStationProject:
      ls
      
      (You should see SensorScripts and DataLogs)
  4. Create a Sample File with nano:

    • Navigate into SensorScripts:
      cd SensorScripts
      
    • Create a new text file named test_script.txt:
      nano test_script.txt
      
    • In nano, type the following:
      # This is a placeholder for a future sensor script.
      # We will learn Python next!
      
    • Save the file: Press Ctrl + O, then Enter.
    • Exit nano: Press Ctrl + X.
    • Verify the file exists:
      ls
      
  5. Copy and Rename Files:

    • Copy test_script.txt to a backup file:
      cp test_script.txt test_script.txt.bak
      
    • List files to see both:
      ls
      
    • Rename the backup file:
      mv test_script.txt.bak test_script_backup_v1.txt
      
    • List files again to see the change:
      ls
      
  6. Move Files:

    • Navigate back to the parent directory (WeatherStationProject):
      cd ..
      
    • Create a dummy log file in the current directory:
      nano dummy_log.csv
      
      Type Date,Temperature,Humidity then save (Ctrl+O, Enter) and exit (Ctrl+X).
    • Move dummy_log.csv into the DataLogs directory:
      mv dummy_log.csv DataLogs/
      
    • Verify it's moved:
      ls DataLogs/
      
      (You should see dummy_log.csv there.)
  7. Install, Check, and Remove a Package:

    • Update your package list:
      sudo apt update
      
    • Install htop (an interactive process viewer):
      sudo apt install htop -y
      
    • Run htop:
      htop
      
      (Observe the processes, CPU/memory usage. Press q to quit htop.)
    • Remove htop:
      sudo apt remove htop -y
      
    • Try running htop again. It should say "command not found".
    • (Optional) Reinstall it if you like it: sudo apt install htop -y.
  8. Clean Up (Deleting Files and Directories):

    • Navigate back to your home directory:
      cd ~
      
      (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 and ls WeatherStationProject/SensorScripts and ls WeatherStationProject/DataLogs.
    • Remove the entire directory:
      rm -r WeatherStationProject
      
      Self-correction/Alternative: A safer way, especially when learning, is to delete contents first:
      # cd WeatherStationProject/SensorScripts
      # rm *
      # cd ../DataLogs
      # rm *
      # cd ..
      # rmdir SensorScripts
      # rmdir DataLogs
      # cd ..
      # rmdir WeatherStationProject
      
      However, rm -r is common. Just always double-check the path before using rm -r.
    • Verify it's gone:
      ls
      
      (The WeatherStationProject directory should no longer be listed.)

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:

  1. 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.

    # Example: Python code is often very intuitive
    if temperature > 25:
        print("It's warm!")
    

  2. 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 like RPi.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 like socket, http.client, and third-party libraries like requests (for HTTP requests), Flask or Django (for web servers) simplify network communication.
    • Data Processing:
      Libraries like NumPy for numerical operations, Pandas for data analysis, and Matplotlib 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.
  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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:

python3 --version
You might also see python (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.

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):

  1. Open Thonny from the Raspberry Pi's application menu (usually under "Programming").
  2. You'll see a script area (top) and a shell area (bottom).
  3. 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 (using pip freeze > requirements.txt).

How to use venv:

Python 3 includes the venv module for creating virtual environments.

  1. Install venv (if not already installed, though usually it is):

    sudo apt update
    sudo apt install python3-venv -y
    

  2. Create a virtual environment:

    • Navigate to your project directory (e.g., the WeatherStationProject directory we might create later).
    • Run:
      cd ~/WeatherStationProject  # Or wherever your project will live
      python3 -m venv myenv
      
      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.
  3. Activate the virtual environment:

    • On Linux/macOS:
      source myenv/bin/activate
      
    • Your terminal prompt will usually change to show the active environment's name (e.g., (myenv) user@hostname:~$).
    • While the environment is active, python and pip commands will refer to the versions within myenv, and packages will be installed into myenv/lib/pythonX.Y/site-packages/.
  4. Install packages into the virtual environment:

    • With the environment active:
      pip install somepackage
      pip install adafruit-circuitpython-dht  # Example for a sensor
      
  5. Deactivate the virtual environment:

    • When you're done working on the project, simply type:
      deactivate
      
    • 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.
      message = "Hello, Raspberry Pi!"
      temperature = 23.5
      pin_number = 17
      is_raining = False
      
  • 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}).
  • Comments:

    • Lines starting with # are comments and are ignored by the interpreter.
      # This is a single-line comment
      x = 5  # This is an inline comment
      
  • Input and Output:

    • print(): Displays output to the console.
      print("Current temperature:", temperature)
      
    • input(): Reads input from the user (always returns a string).
      name = input("Enter your name: ")
      print("Hello,", name)
      
  • Operators:

    • Arithmetic: +, -, *, / (division), // (floor division), % (modulo), ** (exponentiation).
    • Comparison: == (equal), != (not equal), >, <, >=, <=.
    • Logical: and, or, not.
  • Control Flow:

    • if-elif-else statements: Used for conditional execution.

      if temperature > 30:
          print("It's hot!")
      elif temperature > 20:
          print("It's warm.")
      else:
          print("It's cool or cold.")
      
      Indentation (usually 4 spaces) is crucial in Python; it defines code blocks.

    • for loops: Iterate over a sequence (like a list, string, or range).

      fruits = ["apple", "banana", "cherry"]
      for fruit in fruits:
          print(fruit)
      
      for i in range(5):  # range(5) generates numbers 0, 1, 2, 3, 4
          print(i)
      

    • while loops: Repeat as long as a condition is true.

      count = 0
      while count < 3:
          print("Count is:", count)
          count = count + 1  # Or count += 1
      

  • Functions:

    • Reusable blocks of code. Defined using def.
      def greet(name):
          """This function greets the person passed in as a parameter.""" # Docstring
          print("Hello, " + name + ". Good morning!")
      
      greet("Alice")  # Calling the function
      greet("Bob")
      
      def add_numbers(x, y):
          return x + y
      
      result = add_numbers(5, 3)
      print("Sum:", result)
      
  • Error Handling (try-except):

    • Used to catch and handle exceptions (errors) that might occur during execution.
      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
      
      This is particularly important when dealing with hardware, as sensor readings can fail.

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:
      pip3 install requests
      
      (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 install requests==2.25.0
      
  • pip3 uninstall <package_name>:

    • Uninstalls a package.
    • Example:
      pip3 uninstall requests
      
  • 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:
      pip3 freeze > requirements.txt
      
    • To install packages from a requirements file on another system (or in a new virtual environment):
      pip3 install -r requirements.txt
      
  • 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:

  1. 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):
      cd ~  # Go to home directory
      mkdir MyPythonScripts
      cd MyPythonScripts
      
    • Create a virtual environment named my_python_env (or any name you prefer, e.g., venv):
      python3 -m venv my_python_env
      
    • Activate the virtual environment:
      source my_python_env/bin/activate
      
      Your terminal prompt should now show (my_python_env).
  2. Write the Python Script:

    • Option A: Using nano (Command Line):

      • In the terminal (with the virtual environment active), type:
        nano hello_pi.py
        
      • 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 that main() is called only when the script is executed directly (not when imported as a module).
      • Save the file: Press Ctrl + O, then Enter.
      • Exit nano: Press Ctrl + 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:
        1. In Thonny, go to "Tools" -> "Options...".
        2. Go to the "Interpreter" tab.
        3. Select "Alternative Python 3 interpreter or virtual environment".
        4. 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.
        5. Click "OK". Thonny will restart its backend/shell using this interpreter.
  3. 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 where hello_pi.py is saved.
      • Execute the script:
        python3 hello_pi.py
        
        (Or simply python hello_pi.py if the virtual environment correctly sets python to point to its interpreter).
    • 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.
  4. 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.
  5. 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.
  6. Deactivate the Virtual Environment (when done with this session):

    • If you are in the terminal:
      deactivate
      
      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).
  • 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:

    1. 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.
    2. 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 like RPi.GPIO and gpiozero. It's independent of the physical pin layout.
    3. 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.

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:

  1. The Pi pulls the data line LOW for a short period, then HIGH.
  2. The DHT sensor detects this, responds by pulling the line LOW, then HIGH.
  3. 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).
  4. 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 or 0x77. 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)
Always double-check with an accurate diagram for your specific Pi model.

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):

  1. POWER OFF THE PI.
  2. Place Sensors on Breadboard: Insert the DHT22 module and BME280 module onto the breadboard, ensuring their pins are in different rows and not shorted.
  3. 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.
  4. 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.)
  5. 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).
  6. 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.

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:

  1. POWER OFF YOUR RASPBERRY PI: Ensure the power supply is disconnected. This is critical before making any GPIO connections.

  2. 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.
  3. 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 in MyPythonScripts), activate it:
      cd ~/MyPythonScripts  # Or wherever your project directory is
      source my_python_env/bin/activate
      
      If not using a virtual environment, skip activation, but installing packages globally is less ideal. We'll assume you are using one.
  4. 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:
      pip3 install adafruit-circuitpython-dht
      
    • 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:
      sudo apt update
      sudo apt install libgpiod2 -y
      
      This system package provides tools and a library for interacting with GPIO character devices. The Python library often uses this underneath.
  5. 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, using nano or Thonny:
      nano test_dht22.py
      
    • 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 exit nano (Ctrl+X).
  6. Run the Test Script:

    • In the terminal (virtual environment active, in the correct directory), run:
      python3 test_dht22.py
      
    • 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 with sudo: 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 the gpio 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 with pinout command in the terminal if it's installed (sudo apt install python3-gpiozero && pinout). board.D4 from the board 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.
  7. Deactivate Virtual Environment (if used):

    • When done:
      deactivate
      

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:
    import board  # For platform-agnostic pin definitions
    import adafruit_dht
    import time   # For delays
    
  • 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:
      DHT_PIN = board.D4  # board.D4 refers to BCM pin 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
      
  • Reading Data:
    • Access the temperature and humidity attributes of the sensor object.
    • These readings can sometimes fail (return None) or raise a RuntimeError 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.")
      
  • Releasing Resources (Optional but Good Practice):
    • Some CircuitPython device drivers have an exit() method to release underlying resources.
      if dht_sensor:
          try:
              dht_sensor.exit()
          except AttributeError:
              pass # Not all drivers might have it
      

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:

  1. A low-level library for I2C communication on Linux (e.g., smbus2).
  2. 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:
    pip3 install smbus2
    
  • 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:
    pip3 install adafruit-circuitpython-bme280
    
  • Import necessary modules:
    import board
    import adafruit_bme280 # Can use basic or advanced version
    # For the basic version using board.I2C():
    # from adafruit_bme280 import basic as adafruit_bme280_basic 
    
  • Initialization:

    • First, get an I2C bus object. The board library provides a convenient way to get the default I2C bus (board.I2C() typically refers to i2c-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 or 77) in the output grid. If not, there's a wiring or enabling issue.
  • 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.
      # 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")
      
      For a weather station, reporting local station pressure (bme280.pressure) is usually sufficient unless you specifically need altitude or QNH/QFE conversions.

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 return None 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:

  1. try-except Blocks:

    • Wrap sensor initialization and reading calls in try-except blocks to catch specific exceptions (e.g., RuntimeError, ValueError) and generic Exception.
      try:
          # Code that might raise an error
          value = sensor.read_value()
      except RuntimeError as e:
          print(f"Sensor read error: {e}")
          value = None # Or some other default/error indicator
      except Exception as e:
          print(f"An unexpected error: {e}")
          value = None
      
  2. 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
      
  3. Data Validation (Sanity Checks):

    • After getting a reading, check if it's within a plausible range.
      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
      
      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).
  4. 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.
  5. Logging:

    • Use the logging module in Python to log errors, warnings, and successful readings. This is much better than just print() statements for long-running applications.
      import logging
      logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
      # ...
      try:
          # sensor reading
          logging.info("Sensor read successfully.")
      except RuntimeError as e:
          logging.error(f"Sensor read error: {e}")
      

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:

  1. 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:
      sudo i2cdetect -y 1
      
      You should see 76 or 77 (or whatever your BME280's address is) in the grid. If not, troubleshoot I2C wiring/enabling for the BME280.
  2. Activate Virtual Environment and Install Libraries:

    • Navigate to your project directory and activate your virtual environment:
      cd ~/MyPythonScripts  # Or your project path
      source my_python_env/bin/activate
      
    • Install/ensure necessary libraries are present:
      pip3 install adafruit-circuitpython-dht adafruit-circuitpython-bme280 smbus2
      
      (smbus2 is often a dependency of adafruit-circuitpython-bme280 but installing it explicitly doesn't hurt). If you get an error related to libgpiod for the DHT sensor, ensure it's installed:
      sudo apt update
      sudo apt install libgpiod2 -y
      
  3. Create the Python Script (read_all_sensors.py):

    • Using nano or Thonny, create a new file named read_all_sensors.py.
    • Enter the following code:

      # 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.")
      
      Note on DHT MIN_READ_INTERVAL: The adafruit_dht library internally defines MIN_READ_INTERVAL (e.g., 2 seconds for DHT22). It's good practice to respect this. I used a slightly more direct time.sleep(2.0) in the earlier example, but referring to dht_sensor.MIN_READ_INTERVAL if available and documented for your library version is cleaner. For this script, a simple time.sleep(2) inside the retry and a longer time.sleep(5) for the main loop is fine. Corrected the DHT retry sleep above.*

  4. Run the Script:

    • In the terminal (virtual environment active):
      python3 read_all_sensors.py
      
    • 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 to Interface Options, enable I2C, 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 of 77), update BME280_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.
        • 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.
      • General Python errors (ImportError, etc.): Ensure libraries are installed in the active virtual environment.
  5. 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 match fieldnames.
  • 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 your DictWriter 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
  • 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).
  • 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:

  1. The sqlite3 command-line shell:
    • Open a terminal and type sqlite3 mydatabase.db. This will open or create mydatabase.db and give you an sqlite> 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.
  2. 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 for id means SQLite will automatically assign a unique, increasing integer to each new row.
  • timestamp as TEXT and NOT NULL UNIQUE ensures we record when the reading was taken and try to avoid duplicate entries for the same exact second. SQLite also has DATETIME 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 a with 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:

  1. Prepare Your Script:

    • Open your existing sensor reading script (e.g., read_all_sensors.py) or create a new file, say log_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.
  2. Add CSV Logging Functionality:

    • Import necessary modules at the top of your script:
      import csv
      import os
      from datetime import datetime
      # ... other imports like time, board, adafruit_dht, adafruit_bme280_basic
      
    • Define the CSV filename and field names (column headers):
      CSV_FILE = "weather_data_log.csv"
      CSV_FIELD_NAMES = [
          "timestamp",
          "dht_temperature_c", "dht_humidity_percent",
          "bme_temperature_c", "bme_humidity_percent", "bme_pressure_hpa"
      ]
      
    • Modify your main loop (the while True: loop) to include CSV writing:

      # ... (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
      
      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 (dht_sensor, bme_sensor) are initialized before the loop.

  3. Run the Script:

    • Save your log_to_csv.py file.
    • Execute it from the terminal (virtual environment active):
      python3 log_to_csv.py
      
    • 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.
  4. Inspect the CSV File:

    • In the terminal, list files to see weather_data_log.csv:
      ls -l weather_data_log.csv
      
    • View its contents:
      cat weather_data_log.csv
      
    • 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).
  5. (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:

  1. Prepare Your Script:

    • Open your sensor reading script (e.g., read_all_sensors.py or log_to_csv.py) or create a new file, say log_to_sqlite.py.
    • If creating a new file, copy over the sensor initialization and reading logic.
    • Ensure your virtual environment is active.
  2. Add SQLite Functionality:

    • Import necessary modules at the top:
      import sqlite3
      from datetime import datetime
      # ... other imports like time, os, board, adafruit_dht, adafruit_bme280_basic
      
    • 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.
        # ... (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
        
        Again, ensure full sensor reading logic and validation functions are integrated. Ensure sensor objects dht_sensor, bme_sensor are initialized before the loop.
  3. Run the Script:

    • Save your log_to_sqlite.py file.
    • Execute it from the terminal:
      python3 log_to_sqlite.py
      
    • 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.
  4. Inspect the SQLite Database:

    • Using sqlite3 command-line tool:
      • In the terminal, type:
        sqlite3 weather_station_data.db
        
      • At the sqlite> prompt:
        • List tables: .tables (should show weather_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
    • 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.

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:

  1. Schedule Expression (5 or 6 fields): Defines when the command should run.
  2. 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:
    crontab -e
    
  • 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:

    crontab -l
    

  • To remove all your crontab entries (use with caution!):
    crontab -r
    

Important Considerations for Cron Jobs:

  1. 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 the PATH 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
  2. Environment:
    Cron jobs run with a very limited environment. They don't inherit your usual shell environment variables (like PATH, 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
  3. Permissions:
    The cron job runs with the permissions of the user whose crontab it is in. If your script needs sudo privileges (e.g., for certain hardware access, though well-written Python libraries for GPIO/I2C often handle this via user groups like gpio, 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 the gpio and i2c groups is usually the preferred way:

    sudo usermod -aG gpio your_username
    sudo usermod -aG i2c your_username
    
    (You'll need to log out and back in for these group changes to take effect).

  4. 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.
  5. 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 should cd 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:

  1. Error Handling:
    Implement comprehensive try-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.
  2. Logging:
    Use Python's logging 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
    
  3. 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).
    import os
    SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
    DB_FILE = os.path.join(SCRIPT_DIR, "weather_station_data.db")
    # Now DB_FILE will always be in the same directory as the script.
    
  4. Resource Management:
    Ensure files and database connections are properly closed (e.g., using with open(...) or conn.close()).
  5. 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.
  6. 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.")
This script is now designed to run once per invocation, log thoroughly, and then exit. 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 with python3 script.py, it's not strictly needed for the .py file itself).

Steps:

  1. 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:
      which python3
      
      (e.g., /usr/bin/python3)
    • Note the absolute path to your Python script. For example, if your username is student and the script is in MyPythonScripts: /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
  2. Ensure User is in gpio and i2c groups (if not already done):

    • This allows your script, when run by your user via cron, to access hardware without sudo.
      sudo usermod -aG gpio $(whoami)
      sudo usermod -aG i2c $(whoami)
      
    • 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.
  3. Open Your User's Crontab for Editing:

    crontab -e
    
    If prompted, choose nano as the editor.

  4. 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:
        # Weather Station Data Logger - Runs every 5 minutes
        */5 * * * * /usr/bin/python3 /home/student/MyPythonScripts/log_to_sqlite.py >> /home/student/MyPythonScripts/cron_weather_job.log 2>&1
        
      • If USING a virtual environment (replace paths with your actual paths):
        # Weather Station Data Logger - Runs every 5 minutes (with venv)
        */5 * * * * /home/student/MyPythonScripts/my_python_env/bin/python3 /home/student/MyPythonScripts/log_to_sqlite.py >> /home/student/MyPythonScripts/cron_weather_job.log 2>&1
        
      • 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 any print() statements your script might still have or general cron execution issues. Your Python script's own logging module will write to weather_station_script.log (or whatever you named it). Both logs are useful.
    • 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:
        # Weather Station Data Logger - Runs on boot
        @reboot /usr/bin/python3 /home/student/MyPythonScripts/log_to_sqlite.py >> /home/student/MyPythonScripts/cron_weather_job_reboot.log 2>&1
        
      • If USING a virtual environment:
        # 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
        
        Note: For @reboot jobs, sometimes there's a delay before network or all hardware is fully ready. A common practice is to add a sleep 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.
  5. 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.
  6. Verify Crontab:

    crontab -l
    
    You should see the lines you added.

  7. 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 any print 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 without python3 and it didn't have +x).
    • Check the SQLite database using the sqlite3 command-line tool to see if new rows are being added.
      sqlite3 /home/student/MyPythonScripts/weather_station_data.db "SELECT * FROM weather_readings ORDER BY timestamp DESC LIMIT 5;"
      
    • Check system logs for cron activity (advanced):

      grep CRON /var/log/syslog
      
      This will show entries when cron starts jobs.

    • Troubleshooting:

      • Script not running:
        • Double-check all paths in crontab -l.
        • Ensure your user is in gpio and i2c 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?
      • 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.
      • @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 and weather_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). The sleep 60 && trick can help.
  8. (Optional) Test the @reboot functionality:

    • Comment out the 5-minute cron job temporarily (add # at the beginning of the line in crontab -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.

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:

pip3 install matplotlib

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:
    sudo apt update
    sudo apt install python3-tk -y
    
    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:
    import matplotlib
    matplotlib.use('Agg') # Set this before importing pyplot
    import matplotlib.pyplot as plt
    
    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:

import matplotlib.pyplot as plt

Basic Plotting Workflow:

  1. Import pyplot: As shown above.
  2. 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.
  3. 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:
      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
      
      Alternatively, pyplot functions often create them implicitly if they don't exist.
  4. Plot Data:
    Use functions like ax.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).
  5. 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
    

  6. 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) or plt.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, while plt.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 to plt.show()).
  • Your activated virtual environment if you're using one.

Steps:

  1. Prepare Your Environment:

    • Ensure Matplotlib is installed:
      # Activate your virtual environment first if you use one
      # source path/to/your/venv/bin/activate
      pip3 install matplotlib
      
    • If you want to use plt.show() and are on a desktop environment, ensure python3-tk is installed:
      sudo apt install python3-tk -y
      
  2. Create a New Python Script (plot_weather_data.py):

    • In your project directory (e.g., MyPythonScripts), create a new file named plot_weather_data.py.
    • Use nano or your preferred editor.
  3. 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()
      
  4. 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):
      python3 plot_weather_data.py
      
    • Observe the Output:
      • You should see messages about fetching data and saving the plot.
      • If you uncommented plt.show() and have a desktop environment with python3-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.
  5. 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.
  6. Experiment (Optional):

    • Modify NUM_RECORDS_TO_PLOT to see different time windows.
    • Try plotting other data:
      • Change sensor_column_name in fetch_sensor_data call to "dht_temperature_c", "bme_humidity_percent", or "bme_pressure_hpa".
      • Adjust ax.set_ylabel() and ax.set_title() accordingly.
      • Save with a different PLOT_FILENAME.
    • 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 ...
      

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.