Raspberry Zero 2W – BME68x – DS18B20 – ILI9341 TFT: Display Show Software

Teil 5: Auslesen von Sensorsignalen, Generierung von Grafiken und Anzeige auf dem TFT

Nach erfolgreicher Einrichtung und Inbetriebnahme Ihrer Hardware ist es nun an der Zeit, die Sensoren zu konfigurieren, um Werte zu lesen und alle fünf Minuten in Ihrer Datenbank zu speichern. Sie haben die Flexibilität, die Lesefrequenz nach Ihren Vorlieben anzupassen, sei es alle 15 Minuten, stündlich, viermal täglich oder in einem anderen Intervall. Beachten Sie, dass die Größe der Datenbank mit häufigerer Datenspeicherung zunimmt. Persönlich habe ich mich für ein Intervall von 5 Minuten entschieden, unter Berücksichtigung von periodischen Datenausgaben und der Verkleinerung der Datenbank, wenn die Leistung nachlässt, um einen Neustart zu ermöglichen.

Der nächste Schritt besteht darin, die Software regelmäßig auszuführen, um Sensoren auszulesen und die Daten zu speichern. In der Regel erstelle ich einen Systemdienst, der im Hintergrund mit systemd läuft. Alternativ können Sie auch einen Cronjob verwenden, um ein Skript oder ein Programm zu festgelegten Intervallen – Minuten, Stunden oder Tagen – auszuführen.

Ich neige zu Systemdiensten aufgrund ihrer einfachen Initialisierung, Neustart, Überwachung und ihrer eingebauten Protokollierungsfunktionen. Auf einem bescheidenen Raspberry Pi mit nur 512 MB RAM ist es vorteilhaft, Prozesse im Speicher zu minimieren. Die Sensorkripte laufen einige Sekunden und stoppen dann, was einen unkomplizierten und relativ stabilen Prozess darstellt, der keine ständige Aufmerksamkeit erfordert. Da die Bildgenerierung und die Diashow kontinuierlich laufen, richte ich sie als Dienste ein.

Das Skript für das Auslesen der Sensoren befindet sich unter /home/axel/dev/bme688screen als die Datei bme688_ds18b20.py. Um dieses Skript alle 5 Minuten auszuführen, fügen Sie einen Cronjob mit folgendem Befehl hinzu:

crontab -e

Fügen Sie die folgende Zeile (angepasst an Ihre Änderungen und Verzeichnisse oder Dateinamen) am Ende der Datei hinzu:

*/5 * * * * python /home/axel/dev/bme688screen/bme688_ds18b20.py > /dev/null 2>&1

Speichern und verlassen Sie die Crontab mit CTRL-O, CTRL-X.

Diese Zeile weist den Cron-Daemon an, Ihr Skript alle 5 Minuten mit Python auszuführen. Das “/dev/null 2>&1” am Ende unterdrückt alle Ausgaben, die das Programm generieren würde. Da der Cron-Daemon diese Ausgabe nicht benötigt und sie Ihren System-E-Mail-Posteingang überfluten würde.

Jetzt lesen wir die Werte und generieren Bilder für die Anzeige.

Nachdem wir die Temperaturen und andere Werte in unserer Datenbank gespeichert haben, um sie innerhalb unseres Netzwerks zugänglich zu machen, rufen wir sie ab, um eine Reihe von Bildern für eine schöne Diashow auf dem kompakten TFT zu erstellen. Für das 2,8-Zoll-ILI9341-Display müssen die Bilder eine Auflösung von 320 mal 240 haben. Bestimmte Werte werden in einem Farbschema angezeigt, um eine schnelle Erfassung zu ermöglichen. Temperaturen werden beispielsweise in Blau, Hellblau, Hellgrün, Grün, Orange und Rot angezeigt und geben den aktuellen Bereich der Innen- und Außentemperaturen an.

Zuerst installieren Sie eine Grafikbibliothek für Ihr Python-Programm, indem Sie Folgendes eingeben:

pip install Pillow

Erstellen Sie nun eine Datei namens makeimages_temp.py:

nano makeimages_temp.py

Kopieren und fügen Sie den folgenden Code in diese Datei ein:


import os
import logging
from PIL import Image, ImageDraw, ImageFont
import mysql.connector
from mysql.connector import Error
import datetime
 
logging.basicConfig(filename='/var/log/makeimages2.py.log', level=logging.DEBUG)
logging.info('Das Skript wurde gestartet')
 
# Verbindung zur Datenbank herstellen
try:
    connection = mysql.connector.connect(
        host="192.168.x.y",
        user="dbuser",
        password="password",
        database="sensor_data"
    )
    if connection is not None and connection.is_connected():
        cursor = connection.cursor(dictionary=True)
        cursor.execute("SELECT * FROM sensor_data2 ORDER BY date2 DESC, time2 DESC LIMIT 1")
        row = cursor.fetchone()
 
        if row:
            last_temperature = row["Innentemperatur2"]
            last_pressure = row["Luftdruck2"]
            last_humidity = row["Feuchtigkeit2"]
            air_quality = row["AirQuality2"]
            air_quality_scale = row["AirQualityScale2"]
            date_time = f"{row['date2']} {row['time2']}"
 
            # Schriftart und Größe festlegen
            font = ImageFont.truetype("/home/axel/dev/bme688screen/fonts/Roboto-Regular.ttf", 24)
            font_big = ImageFont.truetype("/home/axel/dev/bme688screen/fonts/Roboto-Regular.ttf", 60)
            font_large = ImageFont.truetype("/home/axel/dev/bme688screen/fonts/Roboto-Bold.ttf", 96)
            font_small = ImageFont.truetype("/home/axel/dev/bme688screen/fonts/Roboto-Regular.ttf", 18)
            font_units = ImageFont.truetype("/home/axel/dev/bme688screen/fonts/Roboto-Thin.ttf", 16)  # Schrift für Einheiten (kleiner)
 
            # Erstes Bild mit Wochentag, Datum und Uhrzeit erstellen
            img = Image.new('RGB', (320, 240), color='black')
            draw = ImageDraw.Draw(img)
            current_datetime = datetime.datetime.now()
            day_of_week = current_datetime.strftime("%A")
            date = current_datetime.strftime("%d.%m.%Y")
            time = current_datetime.strftime("%H:%M")
 
            draw.text((10, 10), day_of_week, fill='white', font=font_big)
            draw.text((10, 90), date, fill='white', font=font_small)
            draw.text((10, 140), time, fill='white', font=font_large)
            img.save("/home/axel/dev/bme688screen/images/image_4.png")
 
            # Luftqualitätsgrafik erstellen
            img = Image.new('RGB', (320, 240), color='black')
            draw = ImageDraw.Draw(img)
 
            # Farben für die Luftqualität definieren
            luftqualitaet_farben = {
                "Sehr gute Luftqualität": (0, 255, 0),  # Grün
                "Gute Luftqualität": (144, 238, 144),  # Hellgrün
                "Moderate Luftqualität": (255, 255, 0),  # Gelb
                "Schlechte Luftqualität": (255, 165, 0),  # Orange
                "Sehr schlechte Luftqualität": (255, 0, 0)  # Rot
            }
 
            # Hintergrund und Textfarbe basierend auf Luftqualität setzen
            background_color = (0, 0, 0)  # Schwarz
            text_color = luftqualitaet_farben.get(air_quality_scale, (255, 255, 255))  # Weiß als Standard
 
            # Texte für Luftqualität erstellen
            air_quality_text_1 = "Luftqualität:"
            air_quality_text_2 = air_quality_scale
            air_quality_text_3 = f"{int(air_quality)}"
            unit_text = ""
 
            # Texte auf Bild zeichnen
            draw.rectangle([(0, 0), (320, 240)], fill=background_color)
            draw.text((10, 10), air_quality_text_1, fill='white', font=font)
            draw.text((10, 40), air_quality_text_2, fill=text_color, font=font)
            draw.text((10, 80), air_quality_text_3, fill=text_color, font=font_large)
            img.save("/home/axel/dev/bme688screen/images/image_air_quality.png")
 
            # Luftfeuchtigkeitsgrafik erstellen
            img = Image.new('RGB', (320, 240), color='black')
            draw = ImageDraw.Draw(img)
            # Aussentemperatur abrufen
            external_temperature = row["Aussentemperatur2"]
 
            # Grafik für Aussentemperatur erstellen
            img = Image.new('RGB', (320, 240), color='black')
            draw = ImageDraw.Draw(img)
 
            temperature_ranges = [(-100, -15), (-15, -5), (-5, 0), (0, 5), (5, 10), (10, 15), (15, 20), (20, 25), (25, 30),
                                  (30, 35), (35, 100)]
            colors = [(0, 0, 208), (0, 0, 255), (173, 216, 230), (173, 216, 230), (255, 255, 255), (0, 255, 60),
                      (60, 218, 0), (255, 185, 0), (255, 99, 0), (255, 60, 0), (139, 0, 0)]
 
            temperature_color = (0, 0, 0)  # Default-Farbe (schwarz)
            for temp_range, color in zip(temperature_ranges, colors):
                if temp_range[0] <= external_temperature <= temp_range[1]:
                    temperature_color = color
                    break
 
            temperature_text = "Aussentemperatur: "
            unit = "°C"
 
            draw.text((10, 10), temperature_text, fill='white', font=font)
            value_text = f"{external_temperature:.2f}"
            unit_text = f"{unit}"
            value_bbox = draw.textbbox((10, 80), value_text, font=font_large)
            unit_bbox = draw.textbbox((value_bbox[2], 80), unit_text, font=font_units)
            total_width = unit_bbox[2]
            draw.text((10 + (320 - total_width) / 2, 80), value_text, fill=temperature_color, font=font_large)
            draw.text((10 + (320 - total_width) / 2 + value_bbox[2], 80), unit_text, fill='white', font=font_units)
            img.save("/home/axel/dev/bme688screen/images/image_external_temperature.png")
 
            # Weitere Bilder wie zuvor erstellen
            for i, (label, value, unit) in enumerate([
                ("Temperatur:", last_temperature, "°C"),
                ("Luftdruck:", last_pressure, "hPa"),
                ("Feuchtigkeit:", last_humidity, "%")
            ]):
                img = Image.new('RGB', (320, 240), color='black')
                draw = ImageDraw.Draw(img)
 
                if label == "Temperatur:":
                    temperature_ranges = [(-100, -15), (-15, -5), (-5, 0), (0, 5), (5, 10), (10, 15), (15, 20), (20, 25),
                                          (25, 30), (30, 35), (35, 100)]
                    colors = [(0, 0, 208), (0, 0, 255), (173, 216, 230), (173, 216, 230), (255, 255, 255), (0, 255, 60),
                              (60, 218, 0), (255, 185, 0), (255, 99, 0), (255, 60, 0), (139, 0, 0)]
 
                    temperature_color = (0, 0, 0)  # Default-Farbe (schwarz)
                    for temp_range, color in zip(temperature_ranges, colors):
                        if temp_range[0] <= value <= temp_range[1]:
                            temperature_color = color
                            break
 
                    temperature_text = f"Temperatur: "
                    unit = "°C"
 
                    draw.text((10, 10), label, fill='white', font=font)
                    value_text = f"{value:.2f}"
                    unit_text = f"{unit}"
                    value_bbox = draw.textbbox((10, 80), value_text, font=font_large)
                    unit_bbox = draw.textbbox((value_bbox[2], 80), unit_text, font=font_units)
                    total_width = unit_bbox[2]
                    draw.text((10 + (320 - total_width) / 2, 80), value_text, fill=temperature_color, font=font_large)
                    draw.text((10 + (320 - total_width) / 2 + value_bbox[2], 80), unit_text, fill='white',
                              font=font_units)
                    img.save(f"/home/axel/dev/bme688screen/images/image_{i + 1}.png")
 
                else:
                    draw.text((10, 10), label, fill='white', font=font)
                    value_text = f"{value:.2f}"
                    unit_text = f"{unit}"
                    value_bbox = draw.textbbox((10, 80), value_text, font=font_large)
                    unit_bbox = draw.textbbox((value_bbox[2], 80), unit_text, font=font_units)
                    total_width = unit_bbox[2]
                    draw.text((10 + (320 - total_width) / 2, 80), value_text, fill='white', font=font_large)
                    draw.text((10 + (320 - total_width) / 2 + value_bbox[2], 80), unit_text, fill='white',
                              font=font_units)
                    img.save(f"/home/axel/dev/bme688screen/images/image_{i + 1}.png")
 
except Error as e:
    print(f"Fehler bei der Datenbankverbindung: {e}")
finally:
    if connection.is_connected():
        cursor.close()
        connection.close()


Speichern und beenden Sie die Datei mit STRG-O, STRG-X. Dieses kompakte Programm generiert nun fünf Bilder mit den Namen image_1.png bis image_5.png, die jeweils relevante Sensorwerte aus der Datenbank enthalten. Passen Sie Pfade, Verzeichnisse, Texte und Farben in diesem Skript an Ihre Vorlieben und Sprache an.

Erstellen Sie einen dedizierten Ordner, um die Bilder zu speichern:

mkdir images

Zusätzlich möchten Sie möglicherweise Schriftarten für Ihr Programm herunterladen. Platzieren Sie sie in einem Ordner mit dem Namen “fonts” und passen Sie den Schriftpfad und die Dateinamen im Skript entsprechend an. Ansprechende Schriftarten finden Sie auf Google Fonts: https://fonts.google.com/

Testen Sie das Programm nun, indem Sie folgenden Befehl ausführen:

python ./makeimages_temp.py

Als Bonus hole ich aktuelle Wetterbedingungen und Vorhersagen für die nächsten 5 Tage im 3-Stunden-Intervall ein. Ein kleines Wetter-Icon-Bild wird ebenfalls generiert und in die Diashow aufgenommen. Dies wird jedoch in einem nachfolgenden Leitfaden behandelt.

Showtime – Jetzt holen wir uns etwas Code für eine einfache JPEG-Diashow auf dem TFT.

Als vorläufigen Schritt installieren Sie pigpio und die entsprechenden Python-Bibliotheken für die Dimmsteuerung. Da sich ein Terminal im Schlafzimmer und ein weiteres im Flur befindet, dimme ich die Displays nachts und abends und morgens, um einen ungestörten Schlaf zu gewährleisten. Geben Sie dazu Folgendes in Ihre Konsole ein:

pip install pigpio

Dies installiert die pigpio Python-Bibliothek, die es unserem Anzeigeprogramm ermöglicht, den Backlight-Pin mit PWM-Signalen für die Helligkeitsanpassung zwischen 1 und 100 % zu steuern.

Als nächstes benötigen wir ein weiteres Programm als Unterprozess. “fbi” ist ein Bildbetrachter für den Framebuffer, der mit apt installiert werden kann:

apt install fbi

Erstellen Sie eine neue Datei:

nano framebuffer_show_dim.py

Kopieren und fügen Sie den folgenden Text ein:


import os
import subprocess
import time
import pigpio
from datetime import datetime
 
# GPIO-Pin für PWM-Steuerung des Backlights
backlight_pin = 13
 
# PWM-Objekt erstellen
pi = pigpio.pi()
 
def get_files_in_directory(directory):
    files = []
    for f in os.listdir(directory):
        file_path = os.path.join(directory, f)
        if os.path.isfile(file_path):
            files.append(file_path)
    return files
 
def file_exists(file_path):
    return os.path.exists(file_path)
 
def get_oldest_file(directory):
    files = get_files_in_directory(directory)
    if not files:
        return None
    return min(files, key=lambda f: os.path.getctime(os.path.join(directory, f)))
 
def display_image(image_path, framebuffer_device):
    try:
        print(f"Anzeige von Bild: {image_path}")
 
        # Überprüfen, ob die Datei existiert, bevor sie angezeigt wird
        if file_exists(image_path):
            subprocess.Popen(['fbi', '-d', framebuffer_device, '-T', '1', '-noverbose', '-a', image_path])
            print("Anzeige erfolgreich.")
        else:
            print("Die Datei existiert nicht.")
 
    except Exception as e:
        print(f"Fehler beim Anzeigen des Bildes: {e}")
 
def set_pwm_duty_cycle(current_hour):
    # Zeitsteuerung für PWM-Duty-Cycle
    if 0 <= current_hour < 6:
        pi.set_PWM_dutycycle(backlight_pin, 5)
    elif 6 <= current_hour < 7:
        pi.set_PWM_dutycycle(backlight_pin, 20)
    elif 7 <= current_hour < 9:
        pi.set_PWM_dutycycle(backlight_pin, 50)
    elif 9 <= current_hour < 18:
        pi.set_PWM_dutycycle(backlight_pin, 100)
    elif 18 <= current_hour < 21:
        pi.set_PWM_dutycycle(backlight_pin, 70)
    elif 21 <= current_hour < 23:
        pi.set_PWM_dutycycle(backlight_pin, 20)
    else:
        pi.set_PWM_dutycycle(backlight_pin, 5)
 
def main(image_directory, framebuffer_device):
    displayed_images = set()
 
    try:
        while True:
            files = get_files_in_directory(image_directory)
 
            if files:
                for file in files:
                    image_path = os.path.join(image_directory, file)
 
                    # Überprüfen, ob das Bild bereits angezeigt wurde
                    if image_path not in displayed_images and time.time() - os.path.getctime(image_path) > 2:
                        # Dimmzeit für die aktuelle Stunde überprüfen
                        current_hour = datetime.now().hour
                        set_pwm_duty_cycle(current_hour)
 
                        # Bild anzeigen, wenn es existiert
                        if file_exists(image_path):
                            display_image(image_path, framebuffer_device)
 
                            # Angezeigtes Bild und PWM-Duty-Cycle speichern
                            displayed_images.add(image_path)
 
                            # Warten
                            time.sleep(3)
                        else:
                            print(f"Die Datei {image_path} existiert nicht.")
 
            else:
                print("Keine Bilder im Verzeichnis gefunden.")
                time.sleep(1)
 
            # Beende alle laufenden fbi-Prozesse
            subprocess.run(['pkill', 'fbi'])
 
            # Warten und dann die Liste der angezeigten Bilder zurücksetzen
            time.sleep(1)
            displayed_images.clear()
 
    except KeyboardInterrupt:
        print("\nProgramm wurde beendet.")
        # Beende alle laufenden fbi-Prozesse
        subprocess.run(['pkill', 'fbi'])
 
        # PWM beenden
        pi.set_PWM_dutycycle(backlight_pin, 0)
        pi.stop()
 
if __name__ == "__main__":
    image_directory = "/home/axel/dev/bme688screen/images/"
    framebuffer_device = "/dev/fb1"
 
    main(image_directory, framebuffer_device)


In diesem Code gibt es auch den Zeitplan für die Display-Dimmung in einer 24-Stunden-Anzeige. Passen Sie ihn Ihren Bedürfnissen an:

def set_pwm_duty_cycle(current_hour):
    # Zeitsteuerung für PWM-Duty-Cycle
    if 0 <= current_hour < 6:
        pi.set_PWM_dutycycle(backlight_pin, 5)
    elif 6 <= current_hour < 7:
        pi.set_PWM_dutycycle(backlight_pin, 20)
    elif 7 <= current_hour < 9:
        pi.set_PWM_dutycycle(backlight_pin, 50)
    elif 9 <= current_hour < 18:
        pi.set_PWM_dutycycle(backlight_pin, 100)
    elif 18 <= current_hour < 21:
        pi.set_PWM_dutycycle(backlight_pin, 70)
    elif 21 <= current_hour < 23:
        pi.set_PWM_dutycycle(backlight_pin, 20)
    else:
        pi.set_PWM_dutycycle(backlight_pin, 5)
 

Speichern und beenden Sie die Datei mit STRG-O, STRG-X.

Angenommen, alles ist richtig konfiguriert und Bilder wurden erstellt, sind wir jetzt bereit, eine beeindruckende Diashow auf dem winzigen TFT-Display zu starten. Testen Sie die Framebuffer-Diashow mit dem folgenden Befehl:

python framebuffer_show_dim.py

Wenn Ihr Display Text und Werte zeigt, sind wir fast fertig! Brechen Sie das Programm mit STRG-C ab und erstellen wir nun zwei Systemdienste, die die Bildgenerierung jede Minute durchführen. Da wir auch Datum und Uhrzeit auf einem der Bilder anzeigen, macht es Sinn sicherzustellen, dass die richtige Minute angezeigt wird. Die Framebuffer-Diashow ist eine kontinuierliche Schleife, die Bilder anzeigt, den Bildordner nach neuen Bildern durchsucht und diese nur anzeigt, wenn sie nicht mehr geschrieben werden.

Ich habe ein Bash-Skript erstellt, um eine bessere Kontrolle über die einzelnen Bildgenerierungen (Sensorwerte und Vorhersagebilder) auf dem Raspberry zu haben. Für jetzt ruft dieses Bash-Skript nur einen Prozess auf. Falls Sie sich wundern, warum es nur eine Ausführung darin gibt, werde ich die Icon-Generierung mit der Wettervorhersage in einem späteren Leitfaden behandeln.

Geben Sie in Ihrer Konsole ein:

nano bash_imagemake.sh

und fügen Sie die folgenden Zeilen hinzu:

#!/bin/bash
while true
do
sleep 57
python /home/axel/dev/bme688screen/makeimages_temp.py
sleep 3
done

Speichern und beenden Sie mit STRG-O, STRG-X.

Geben Sie in Ihrer Konsole ein:

cd /lib/systemd/system

und fügen Sie eine Datei hinzu:

nano weatherimage.service

Kopieren/Einfügen Sie dies:

[Unit]
Description=Wetterbild-Erzeuger
After=stream.service
[Service]
ExecStartPre=/bin/sleep 5
ExecStart=/home/axel/dev/bme688screen/bash_imagemake.sh
Nice=15
Restart=always
User=root
WorkingDirectory=/home/axel/dev/bme688screen
[Install]
WantedBy=multi-user.target

Erstellen Sie nun eine weitere Datei:

nano weatherscreen.service

Fügen Sie den folgenden Inhalt hinzu:

[Unit]
Description=Wetterscreen
After=weatherimage.service
[Service]
ExecStartPre=/bin/sleep 10
ExecStart=/usr/bin/python3 /home/axel/dev/bme688screen/framebuffer_show_dim.py
Nice=15
Restart=always
User=root
WorkingDirectory=/home/axel/dev/bme688screen
[Install]
WantedBy=multi-user.target

Speichern und beenden Sie mit STRG-O, STRG-X.

Starten und aktivieren Sie die Dienste während des Systemstarts:

systemctl enable weatherimages.service
systemctl enable weatherscreen.service
systemctl start weatherimages.service
systemctl start weatherscreen.service

Und da haben Sie es! Sie sollten jetzt einen voll funktionsfähigen Raspberry Zero 2W mit Sensorauslesung, Datenbank-Speicherung, Bildgenerierung und Diashow-Anzeige auf einem 2,8-Zoll-TFT-Display haben. Es kann mit etwas Kabelchaos, ratlosen Blicken Ihres Ehepartners und neugierigen Fragen Ihrer Kinder einhergehen, während es an Ihrer Wand in der Nähe eines Fensters hängt. 🙂

In einigen weiteren Anleitungen werde ich meine Codes teilen, wie Sie all diese Daten auf Ihrer persönlichen Website präsentieren können. Ich werde erklären, wie Sie Wetterdaten von openweathermap.org hinzufügen, einen Bildtranscoder einbinden, der verschiedene Quellformate unterstützt und sie auf 320×240 für Ihre ILI9341-Diashow verkleinert. Außerdem werde ich Sie darüber informieren, wie Sie bequem EQ3 Bluetooth-Thermostate für Ihre Heizbatterien steuern können, indem Sie einen benutzerfreundlichen Zeitplan verwenden, den Sie direkt auf einer Website bearbeiten können. Freuen Sie sich auf weitere spannende Einblicke!