Temperaturregelung mit Raspi

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
Antworten
mayr_2112
User
Beiträge: 5
Registriert: Donnerstag 2. April 2020, 13:48

Hallo erstmal.

Ich bin gerade am tüfteln, wie ich denn am einfachsten meinem Python Programm Variablen übergeben kann?
Ausgangssituation:
Ich möchte eine Temperaturregelung bauen, um meinen Ofen auf ca 200°C zu halten. +-5°C sind egal.

Meine Hardware:
Raspberry Pi 3 B+ (mit Raspian)
MAX6675 Thermomodul (Messwerte über SPI)
OLED Display 128x64 (i2C)
Relaykarte zum An/Ausschalten der Heizspiralen.

Meine Frage ist nun, wie ich am besten per Smartphone einen Sollwert, Hysterese usw übergeben kann, und gleichzeitig den Istwert des Sensors auslesen kann?
Habe mich durch viele Foren gelesen, aber die Lösung mit dem QT5 Designer scheint mir zu kompliziert, da weiß ich nicht wie ich den Code am besten implementieren soll.
Geht es mit php über einen Webbrowser? Oder über VNC direkt auf den Raspi zugreifen auf eine graphische Oberfläche? (Welche??)

Mein Programmcode:

Code: Alles auswählen


import time
import time
import pigpio

import Adafruit_GPIO.SPI as SPI
import Adafruit_SSD1306

import RPi.GPIO as gpio

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
import subprocess

##Gpio Einstellungen
Taster10_up = 23
Taster10_down = 24
Taster01_up = 25
Taster01_down = 26
Relay1 = 5

gpio.setwarnings(False)
  
gpio.setmode(gpio.BCM)
gpio.setup(Taster10_up, gpio.IN) #Taster10_up
gpio.setup(Taster10_down, gpio.IN) #Taster10_down
gpio.setup(Taster01_up, gpio.IN) #Taster01_up
gpio.setup(Taster01_down, gpio.IN) #Taster01_down
gpio.setup(Relay1, gpio.OUT)  #Relay1 für Heizspule

##Sensor einlesen    
pi = pigpio.pi()
sensor = pi.spi_open(0, 1000000, 0)
if not pi.connected:
    exit(0)
       
# Raspberry Pi pin configuration:
RST = None     
DC = 23
SPI_PORT = 0
SPI_DEVICE = 0

# 128x64 display with hardware I2C:
disp = Adafruit_SSD1306.SSD1306_128_64(rst=RST)
disp.begin()
# Clear display.
disp.clear()
disp.display()
# Create blank image for drawing.
width = disp.width
height = disp.height
image = Image.new('1', (width, height))
# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)
# Draw a black filled box to clear the image.
draw.rectangle((0,0,width,height), outline=0, fill=0)
# First define some constants to allow easy resizing of shapes.
padding = -2
top = padding
bottom = height-padding
# Move left to right keeping track of the current x position for drawing shapes.
x = 0
# Load default font.
font = ImageFont.load_default()
# Alternatively load a TTF font.  Make sure the .ttf font file is in the same directory as the python script!
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
# font = ImageFont.truetype('Minecraftia.ttf', 8)
def LCD_Temperatur():
        # Draw a black filled box to clear the image.
    draw.rectangle((0,0,width,height), outline=0, fill=0)
        # Shell scripts for system monitoring from here : https://unix.stackexchange.com/questions/119126/command-to-display-memory-usage-disk-usage-and-cpu-load
    cmd = "hostname -I | cut -d\' \' -f1"
    IP = subprocess.check_output(cmd, shell = True )
    cmd = "top -bn1 | grep load | awk '{printf \"CPU Load: %.2f\", $(NF-2)}'"
        

    draw.text((x, top),       "IP: " + str(IP),  font=font, fill=255)
    draw.text((x, top+16),     "Isttemp = " + str(isttemp) + " °C", font=font, fill=255)
    draw.text((x, top+32),     "Solltemp = " + str(solltemp) + " °C", font=font, fill=255)
    draw.text((x, top+48),     "Hysterese = +-" + str(hysterese) + " °C", font=font, fill=255)
        # Display image.
    disp.image(image)
    disp.display()
    #time.sleep(.1)


def getSensorTemp():
    c, d = pi.spi_read(sensor, 2)
    if c == 2:
        word = (d[0]<<8) | d[1]    #shift bits
        if (word & 0x8006) == 0: # Bits 15, 2, und 1 müssen 0 sein (lt. Datenblatt).
            t = (word >> 3)/4.0
            Istwert = t
            print("{:.2f}".format(t))
        else:
            print("bad reading {:b}".format(word))
            Istwert = 987
    return Istwert   

    pi.spi_close(sensor)

    pi.stop()


solltemp = float(input("Sollwert: "))
hysterese = float(input("Hysterese: "))
                 
while True:     ##Hier sollte eigentlich eine Abfrage eines Starttasters stattfinden
    Tempmax = (solltemp+hysterese)
    Tempmin = (solltemp-hysterese)
            
    isttemp = getSensorTemp()
    LCD_Temperatur()
    time.sleep(0.5)

    if isttemp < Tempmin:
        #print("relay an")
        gpio.output(Relay1, gpio.HIGH)
    elif isttemp > Tempmax:
        #print ("relay aus")
        gpio.output(Relay1, gpio.LOW)

__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Es gibt da Moeglichkeiten wie Sand am Meer, eine der besseren: benutz ein Python Web-Framework (bottle oder Flask), und lass deinen Code oben in einem extra Thread in der Webanwendung laufen. Dazu gehoert der gehoerig aufgeraeumt, mindestens in Funktionen, besser gar eine Klasse.
Sirius3
User
Beiträge: 17787
Registriert: Sonntag 21. Oktober 2012, 17:20

Du benutzt pigpio und RPi.GPIO, und dann noch Adafruit_GPIO, entscheide Dich für eins.
Strings stückelt man nicht mit + zusammen, und statt .format benutzt man seit Python 3.6 f-Strings.
Statt externer Programme für Host und Load aufzurufen, benutze psutils.
In `getSensorTemp` stehen Befehle, die nach `return` stehen.
Variablennamen und Funktionen schreibt man klein_mit_unterstrich. Keine Abkürzungen verwenden. Tempmax -> maximum_temperature, oder ist_temp -> current_temperature.
Und für Fernsteuerung per Smartphone bietet sich ein Webserver an, den man komplett in Python schreiben kann, z.B. mit Flask.
mayr_2112
User
Beiträge: 5
Registriert: Donnerstag 2. April 2020, 13:48

Ok danke für eure Antworten.

Denke ich schaffe es nicht, einen Webserver selber zu schreiben. Gibt es nicht eine einfache Möglichkeit, einen Wert einzugeben, und die Isttemperatur anzuzeigen? Das Programm läuft ja...könnte man die Variablen nicht einfach an ein externes Programm übergeben?

Bitte um eure Hilfe!
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das IST der einfache Weg. Wenn ein Programm laeuft, dann muss man nunmal einen Weg finden, wie man es zur Laufzeit mit Daten versorgt, und welche rauskriegt. Webserver sind die einfachste, weil in vielen Beziehungen bereits best dokumentiert und ausgetretene Art und Weise.

Ander sind sockets in verschiedenen Formen, Messagebusse wie nanomsg, nng, zeromq ohne Persistenz, oder mit wie MQTT, RabbitMQ und co. Wenn du schon an einem Webserver scheiterst, dann werden die nicht einfacher sein.
mayr_2112
User
Beiträge: 5
Registriert: Donnerstag 2. April 2020, 13:48

Geht es mit tKinter? Habe versucht da was auf die Reihe zu bringen, allerdings weiß kenne ich mich mit Klassen etc nicht wirklich aus. Da gibts ja eine Mainloop() und da weiß ich nicht wo man den restlichen Programmcode (von oben) einfügen soll? get_Solltemperatur() wird nur aufgerufen, wenn der Button gedrückt wurde. Ich möchte also den aktuellen Wert der Eingabe >Solltemperatur_entry.get()< herausschreiben auf eine globale Variable. Wie mache ich das? Wo müsste ich den Programmcode einfügen, sodass ständig alle Werte aktualisiert werden (Isttemperatur)?

Code: Alles auswählen


import tkinter as tk
import requests

HEIGHT = 500
WIDTH = 900
Solltemperatur = 0

def get_Solltemperatur(entry):
    print(entry)
    
root = tk.Tk()

canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()

##Ausgabe Temperatur1 (Isttemperatur)
#Rahmen erstellen
frameA = tk.Frame(root, bg='#A0A0A0', height=100, width=50)
frameA.place(relx=0.5, rely=0.1, relwidth=0.75, relheight=0.1, anchor='n')



##Eingabe Solltemperatur
#Rahmen erstellen
frameB = tk.Frame(root, bg='#A0A0A0', height=100, width=50)
frameB.place(relx=0.5, rely=0.25, relwidth=0.75, relheight=0.1, anchor='n')

#Label erstellen
Solltemperatur_label = tk.Label(frameB, bg='#A0A0A0', text='Solltemperatur')
Solltemperatur_label.place(rely=0.1, relwidth=0.25, relheight=0.8)

#Eingabefeld erstellen
Solltemperatur_entry = tk.Entry(frameB, font=40)
Solltemperatur_entry.place(relx=0.3,rely=0.1, relwidth=0.25, relheight=0.8)

#Button erstellen
OK_btn = tk.Button(frameB, bg='#ff8000', text='OK', bd=2, command=lambda: get_Solltemperatur(Solltemperatur_entry.get()))
OK_btn.place(relx=0.7,rely=0.1, relheight=0.8, relwidth=0.25)


root.mainloop()

__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nein, das ist nicht zielfuehrend. Denn da fehlt die Moeglichkeit, die Oberflaeche vom Smartphone aus zu benutzen.
mayr_2112
User
Beiträge: 5
Registriert: Donnerstag 2. April 2020, 13:48

Ich kann ja über VNC-Viewer drauf zugreifen?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ok, das waere das letzte, was ich machen wuerde, weil es langsam und nicht der Umgebung des Smartphones angepasst ist (Aufloesung, Groesser der Schaltelemente, Fensterdekorationen etc), aber wenn du dir den Schmerz geben willst - bitteschoen.

Dann musst du deine Regelung in einen Thread packen, und mit der GUI ueber eine Queue kommunizieren, und in der GUI mit der after-Methode pruefen, ob Daten in der Queue sind, und darauf reagieren. Das wird hier des oefteren diskutiert, und auch gelegentlich mal mit Bespielen unterfuettert. Dazu mal im tkinter Forum stoebern, bewaffnet mit den gerade genannten Stichworten. Ich empfehle da google site search statt die Forumssuche, weil die ziemlich oede ist leider.
mayr_2112
User
Beiträge: 5
Registriert: Donnerstag 2. April 2020, 13:48

Danke!
Sirius3
User
Beiträge: 17787
Registriert: Sonntag 21. Oktober 2012, 17:20

Ob Du jetzt lernst, wie man ordentliche GUIs programmiert, oder Dich frisch in etwas HTML und ein Framework wie Flask einarbeitest, um eine einfache und saubere Lösung zu bekommen, ist Dir ja überlassen.
Hier im Forum gehen die Empfehlungen ganz klar in eine Richtung.

Was hast Du denn versucht und an welcher Stelle scheiterst Du beim Programmieren eines "Webservers"?
Benutzeravatar
__blackjack__
User
Beiträge: 13179
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@mayr_2112: Weitere Anmerkungen zum Quelltext im ersten Beitrag:

`time` zweimal importiert. In direkt aufeinanderfolgenden Zeilen.

``as`` beim Import ist zum Umbennenen da, `SPI` aus `Adafruit_GPIO` wird aber gar nicht umbenannt. Wobei das Modul im Code auch überhaupt nicht verwendet wird‽

Konstanten werden komplett in Grossbuchstaben benannt.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Daraus folgt dann auch, dass Funktionen und Methoden alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. `LCD_Temperatur()` bekommt dann allerdings 11 Argumente, was ein Zeichen dafür ist das in dieser Funktion zu viel unterschiedliches gemacht wird, oder das man anfangen sollte Werte sinnvoll zu Objekten zusammenzufassen.

Warnungen sollte man nicht ignorieren, sondern deren Ursache beseitigen. Beim `GPIO`-Modul ist es wichtig sicherzustellen das am Ende des Programmablaufs, egal warum das beendet wird, die `cleanup()`-Funktion aufgerufen wird.

Warum steht bei dem aufsetzen der Taster-Pins noch mal der Variablenname aus dem Code als Kommentar in der Zeile?

Statt eines nichstsagenden `Relay` an das unsinnigerweise auch noch eine ebenfalls nichtssagende 1 angehängt wurde, und einem Kommentar, dass das der Pin für die Heizspule ist, sollte man da einen Namen wählen der das aussagt und den Kommentar weglassen.

Der Kommentar ``Sensor einlesen`` mach inhaltlich überhaupt keinen Sinn.

`exit()` gibt es eigentlich gar nicht. Das ist undokumentiert. Wenn man die Funktion braucht, dann sollte man sie explizit aus dem `sys`-Modul nehmen. Und man sollte sie nur verwenden wenn man zumindest potentiell auch einen anderen Rückgabecode als 0 übermitteln möchte. Möchte man in diesem Fall sogar, denn 0 bedeutet, dass das Programm ohne Probleme durchlief, was ja nicht der Fall ist wenn `pi` nicht mit dem Daemon verbunden ist. Das sollte man testen *bevor* man `spi_open()` aufruft. Danach macht es wenig Sinn.

Den `sensor` könnte man auch `temperature_sensor` nennen, damit der Leser weiss was das für ein Sensor ist. Und auch hier sollte man wieder sicherstellen, dass am Programmende diese Ressourcen ordentlich aufgeräumt werden. Sowohl das `pi`-Objekt als auch die SPI-Verbindung.

Wenn man `display` meint, sollte man nicht nur `disp` schreiben.

Faustregel für Kommentare: Die beschreiben nicht *was* der Code macht, denn das steht da ja bereits als Code, sondern warum der das so macht. Sofern das nicht offensichtlich ist. Da sind so einige Kommentare überflüssiger Ballast.

Das gefüllte Rechteck zeichnen im Hauptprogramm kann man sich sparen, weil `Image.new()` bereits ein leeres Bild liefert.

`bottom` wird definiert, aber nirgends verwendet. `x` ist immer 0, der Kommentar darüber suggiert verwirrenderweise etwas anderes. Wenn man das `x` rauswirft, hat `LCD_Temperatur()` schon mal ein Argument weniger. Da die Funktion auch das Bildobjekt übergeben bekommt, braucht man ihr auch `width` und `height` gar nicht übergeben, denn diese Informationen kann man vom Bildobjekt abfragen. Wieder zwei Argumente eingespart.

Die beiden Temperaturgrenzen werden in der Schleife immer neu berechnet obwohl sich die Werte davon nie ändern.

`getSensorTemp()` ist fehlerhaft. Wenn *nicht* zwei Bytes gelesen werden konnten, ist `Istwert` undefiniert und führt beim Versuch das zurückzugeben zu einem `NameError`.

`c`, `d`, und `t` sind keine guten Namen. `t` ist auch überflüssig. 987°C ist ein extrem komischer und willkürlicher Wert für eine Temperatur die gar nicht gelesen werden konnte. Wenn man schon keine Ausnahme auslösen möchte und das unbedingt über einen Wert regeln möchte, dann sollte der auch ”sinnvoll” sein. Also beispielweise NaN wenn es vom Typ `float` sein soll oder `None`.

Eine Funktion die einen Sensor abfragt sollte keine Ausgaben mit `print()` machen. Für so etwas eignet sich Logging weil man das an- und abschalten kann, je nach dem ob man diese Infos haben möchte oder nicht. Und man kann sie auch in eine Datei oder woanders hin schicken wenn man sie dauerhaft sichern und auswerten möchte.

`LCD_Temperatur()` ist nicht nur von der Schreibweise her falsch für eine Funktion, sondern auch inhaltlich. Funktionen werden üblicherweise nach ihrer Tätigkeit benannt. Die Funktion macht auch zu viel verschiedenes. Die Ermittelt eine IP mit einem externen Programm, formatiert einzelne Werte in Zeichenketten, und aktualisiert das LCD. Die sollte einfach nur alles notwendige zum Anzeigen von mehreren Zeichenketten auf dem Display übergeben bekommen und nichts davon wissen müssen was das für Werte sind und wo die her kommen. Dann werden das auch gleich wieder weniger Argumente, denn die Zeichenketten die angezeigt werden sollen übergibt man sinnvollerweise als Liste. Dann lässt sich das Anzeigen auch mit einer Schleife mit weniger Code-Wiederholungen schreiben. Und es sind wieder 3 Argumente weniger.

Die ersten vier Argumente beschreiben zusammen den Zustand der Anzeige und verändern sich nicht. Die könnte man also sinnvollerweise in einem Objekt zusammenfassen. Dann bleiben von den 11 Argumenten am Ende nur noch zwei übrig.

Die zweite Zuweisung an `cmd` ist überflüssig, weil der Wert nirgends verwendet wird. In der ersten Zuweisung sind die \ in der Zeichenkette überflüssig. Der Wert kommt auch überhaupt nicht von der Webseite die im Kommentar davor genau das behauptet.

Zum aufrufen von externen Programmen: ``shell=True`` sollte man nur verwenden wenn man tatsächlich eine Shell braucht. Die wird bei der IP nur wegen der Pipe in das ``cut`` benutzt, aber das was ``cut`` da macht, kann man problemlos in Python lösen. Und man möchte an der Stelle auch Text und keine Bytes zurückbekommen.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import subprocess
import sys
import time
from collections import namedtuple

import Adafruit_SSD1306
import pigpio
from loguru import logger
from PIL import Image, ImageDraw, ImageFont
from RPi import GPIO as gpio

TEN_UP_BUTTON_PIN = 23
TEN_DOWN_BUTTON_PIN = 24
ONE_UP_BUTTON_PIN = 25
ONE_DOWN_BUTTON_PIN = 26
HEATING_COIL_PIN = 5

DISPLAY_RST = None

Display = namedtuple("Display", "display image draw font x_offset y_offset")


def create_display(rst, x_offset=0, y_offset=0):
    display = Adafruit_SSD1306.SSD1306_128_64(rst)
    display.begin()
    display.clear()
    display.display()
    image = Image.new("1", (display.width, display.height))
    return Display(
        display,
        image,
        ImageDraw.Draw(image),
        ImageFont.load_default(),
        x_offset,
        y_offset,
    )


def update_display(display, lines):
    display.draw.rectangle(
        (0, 0, display.image.width, display.image.height), outline=0, fill=0
    )
    for i, line in enumerate(lines):
        display.draw.text(
            (display.x_offset, display.y_offset + i * 16),
            line,
            font=display.font,
            fill=255,
        )
    display.display.image(display.image)
    display.display.display()


def get_temperature(pi, sensor_handle):
    temperature = float("nan")
    byte_count, data = pi.spi_read(sensor_handle, 2)
    if byte_count == 2:
        word = (data[0] << 8) | data[1]
        #
        # Bits 15, 2, und 1 müssen 0 sein, laut Datenblatt.
        #
        if (word & 0b1000_0000_0000_0110) == 0:
            temperature = (word >> 3) / 4
            logger.debug("{:.2f}°C", temperature)
        else:
            logger.error("bad reading {:b}", word)
    else:
        logger.error("wrong byte count: {}", byte_count)

    return temperature


#
# TODO Die Funktion ist zu lang.  Sinnvoll in Funktionen aufteilen.
#
@logger.catch
def main():
    try:
        gpio.setmode(gpio.BCM)
        gpio.setup(
            [
                TEN_UP_BUTTON_PIN,
                TEN_DOWN_BUTTON_PIN,
                ONE_UP_BUTTON_PIN,
                ONE_DOWN_BUTTON_PIN,
            ],
            gpio.IN,
        )
        gpio.setup(HEATING_COIL_PIN, gpio.OUT)

        pi = pigpio.pi()
        try:
            if not pi.connected:
                sys.exit(1)
            temperature_sensor_handle = pi.spi_open(0, 1_000_000)
            try:
                display = create_display(DISPLAY_RST, 0, -2)
                target_temperature = float(input("Sollwert: "))
                hysteresis = float(input("Hysterese: "))
                min_temperature = target_temperature - hysteresis
                max_temperature = target_temperature + hysteresis
                #
                # TODO Hier sollte eigentlich eine Abfrage eines Starttasters
                # stattfinden.
                #
                while True:
                    #
                    # TODO `psutil` statt eines externen Programms verwenden.
                    # Und dann möchte man auch die Netzwerkschnittstelle angeben
                    # von der man die IP haben möchte und nicht einfach
                    # irgendeine der Möglichkeiten zufällig auswählen.
                    #
                    ip_addresses = subprocess.check_output(
                        "hostname --all-ip-addresses", universal_newlines=True
                    ).split()
                    temperature = get_temperature(
                        pi, temperature_sensor_handle
                    )
                    update_display(
                        display,
                        [
                            "IP: {}".format(ip_addresses[0]),
                            "Isttemp. = {:.2f} °C".format(temperature),
                            "Solltemp. = {:.2f} °C".format(target_temperature),
                            "Hysterese = +-{:2f} °C".format(hysteresis),
                        ],
                    )
                    if temperature < min_temperature:
                        logger.debug("relay an")
                        gpio.output(HEATING_COIL_PIN, gpio.HIGH)
                    elif temperature > max_temperature:
                        logger.debug("relay aus")
                        gpio.output(HEATING_COIL_PIN, gpio.LOW)

                    time.sleep(0.5)
            finally:
                pi.spi_close(temperature_sensor_handle)
        finally:
            pi.stop()
    finally:
        gpio.cleanup()


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Antworten