Einen PIR-Bewegungsmelder (HC-SR501) mit dem Raspberry Pi ansteuern
Ich möchte mir, aufbauend auf meinem letzten Projekt LCD-Ansteuerung ein Always-On-Display zusammenbauen, dass mir jederzeit die für mich wichtigen Infos auf mehreren durchschaltbaren Screens anzeigt. Das Durchschalten (oder auch die Direktanwahl einer der bis zu 9 Screens) hatte ich mir per Infrarot-Fernbedienung vorgestellt. Angezeigt werden sollen später einmal- natürlich Datum, Wochentag, Uhrzeit
- Innentemperatur und weitere Innensensoren
- Raspi-Status (CPU-LAST, Temperatur, RAM etc.
- Webserver-Überwachung mit Ping-Zeit, HTTP-Test, Alarm bei Offline etc.
- aktuelle Wetterdaten aus dem Internet
- Wetterdaten für morgen
- evtl. die nächsten anstehenden Termine
- Fernsehsendungen des Tages und was mir noch so einfällt
Den ersten Screen habe ich schon zusammen gezimmert. In der ersten Zeile stehen Wochentag, Datum und Uhrzeit.
In der dritten nach einem CPU-Symbol die CPU-Auslastung des Raspberrys, die Taktfrequenz in Gigahertz und die aktuelle CPU-Temperator.
In der vierten Zeile ein RAM-Symbol, dahinter, wieviel MB Speicher noch frei sind und dann noch ein SD-Karten-Symbol und der freie Speicherplatz darauf.
Die zweite Zeile habe ich erstmal frei gelassen für Temperatur, Luftfeuchtigkeit und was mir an Sensoren noch so in die Hänge fällt.
Übrigens ist inzwischen mein Cobbler-Board angekommen, um die Anschlüsse des Raspis über ein 40 poliges Flachbandkabel auf das Breadboard zu bringen. "GPIO Extension Board" haben die Chinesen noch richtig geschrieben bekommen, aber was soll GNL sein? Oder der Pin Nr. 28. Der kann sich anscheinend nicht zwischen ID_SC und GPIO12 entscheiden, steht doch beides dran.
Tolle Hilfe: Pinbeschriftungen, auf die man sich nicht verlassen kann. Der Hohn dabei ist ja, dass auf dem Produktfoto bei eBay alles korrekt gedruckt war.
Ich habe darum meine eigenen Beschriftungen mit Excel gestaltet und ausgedruckt (was für eine Fitzel-Arbeit, das genau passend zu kriegen!) und dann drübergeklebt.
Und ich habe den Cobbler gleich eingesetzt. Dadurch musste ich die Schaltung neu aufbauen und da die Pins nicht mehr von rechts nach links verlaufen, sondern "richtig" herum, haben sich auch die Pins für die Datenleitungen geändert. Im neuen Programm also diese Änderungen berücksichtigen.
Die neue Verdrahtung des LCD sieht nun so aus:
LCD Raspi
VSS Masse
VDD 3.3V oder 5V, je nach Modell
V0 über 5 kΩ-Poti an GND, für Kontrast
RS an BCM 9, bestimmt ob Kommando oder Zeichen
RW GND (wir wollen nur schreiben, also immer Low)
E an BCM 11 (Enable-Leitung, Pulse, wenn Sendung fertig)
D4 an BCM 5 (Datenleitung 1)
D5 an BCM 6 (Datenleitung 2)
D6 an BCM 13 (Datenleitung 3)
D7 an BCM 19 (Datenleitung 4)
A an BCM 26 über 470 Ω-Poti (Backlight an/aus, Helligkeit)
K GND
Der LCD-Anschluss über ein 16-poliges Flachbandkabel an mein 20x4-Character-Display ist so nah mit möglich an den Cobbler rangerückt, damit rechts noch Platz für weitere Sensoren bleibt. Ist der auch verbraucht, muss halt mit weiteren Boards oben erweitert werden.
Die einpoligen Seiten der Potis habe ich gleich in die Anschlüsse V0 und A gesteckt, so ist der Platz oberhalb nicht verschwendet und bei der Länge der Potis müssen diese eh beide Boardhälfte (oben und unten) berühren.
Aber die Schaltung, auch wenn die Kabel und Bauteile ein wenig den Platz gewechselt haben, kennen wir ja so bereits aus meinem letzten Projekt.
Heute hinzukommen soll ein sogenannter PIR (für Passive InfraRed)-Sensor, zu deutsch: Infrarot-Bewegungsmelder. Das sind die Dinger, die registrieren, ob jemand an ihnen vorbeiläuft und dann automatisch das Licht anschalten. Die Dinger stecken auch in Alarmanlagen.
Technisch funktioniert das Bauteil so, dass es die abgegeben Wärmestrahlung (die von jedem Menschen, aber auch von Hund und Katze, abgegeben wird) erfasst und dabei Änderungen feststellt. Das signalisiert es durch ein High an OUT-Pin.
Mit dem linken der zwei Potis (gesehen mit Blick auf die Unterseite, wenn der gelbe Jumper links ist) kann man die Empfindlichkeit des Bauteils einstellen: Je weiter rechts, desto unempfindlicher reagiert der Sensor auf Bewegungen. Aber wie groß muss die zum Auslösen nötige Bewegung sein? Auf einen Käfer soll es wohl eher nicht reagieren. Aber wir wollen ja auch nicht vor dem Sensor stehen wollen und wild mit den Armen wedeln, bis er mal registriert, dass sich da etwas bewegt hat.
Mit dem rechten Poti regelt man die Haltezeit, also die Zeitspanne, wie lange das Signal nach einer Bewegungserkennung auf HIGH geht. Ist das Poti links am Anschlag, dann beträgt sie eins bis zwei Sekunden, rechts am Anschlag dauert es 5 Minuten, bis der PIR wieder auf LOW geht. Die Haltezeit ist z. B. dafür gut, das Flurlicht so und so lange anzulassen, wenn das schaltene Relais sich am High des PIR orientiert. Nach ein paar Sekunden oder Minuten geht es dann wieder automatisch aus.
Mit dem Jumper (soweit er denn vorhanden ist) kann man schließlich einstellen, obwohl ein langes, einzelnes (Stellung L) oder ein gepulstes (Stellung H) HIGH haben will. Das gepulste HIGH macht nur Sinn für Schaltungen, die auf Flankenveränderungen prüfen. Da wir direkt auf den Zustand prüfen (also ob LOW oder HIGH) ist L für uns die richtige Stellung.
Auf der Unterseite der Platine befindet sich auch der Header zum Anschluss des PIR-Sensors an den Raspi. Mehr als Versorgunsspannung, Masse und einem Kabel, dass an OUT angeschlossen wird, braucht es nicht.
Die technischen Daten lauten:
Versorgungsspannung: 5-20 V Gleichstrom
Stromaufnahme: 65 mA
Output Level High: 3.3V, Low: 0V
Haltezeit: 5 ... 300 Sekunden
Betriebstemperatur: -15 ... +70 °C
Operationswinkel: 120 °, 7 Meter
Da der Spannung, die an OUT anliegt, auf 3.3V begrenzt ist, können wir den PIR auch bedenkenlos an +5V betreiben. Damit funktionieren sie sicher. Manche meiner Modelle haben aber auch klaglos mit 3.3V Versorgungsspannung funktioniert.Das OUT-Kabel schließen wir an BCM 10 des Raspi an, GND dürfte klar sein.
BCM 10 wird dann natürlich als Eingang definiert. Außerdem schalten wir den internen PullDown-Widerstand dazu, um immer einen klar definierten Zustand zu haben.
So sieht der PIR-Sensor übrigens ohne die Plastikhaube aus. Das kleine rechteckige Fenster wertet die eintreffende Infrarotstrahlung aus.
Wozu die Plastikhaube genau gut ist und warum sie genau diese Form hat, weiß ich auch nicht genau.
Ich schätze mal, damit kann man einen größeren Betrachtungswinkel realisieren. Vielleicht wird damit auch zusätzlich ungewollte Strahlung gefiltert.
Der Aufbau funktioniert wunderbar. Ich habe das Timing so eingestellt, dass das Display ausgeht, wenn ich mich länger als 2 Minuten davor nicht bewegt habe, was eigentlich nie der Fall ist, wenn ich vorm Computer sitze.
Und wenn ich den Platz verlasse, geht der Bildschirm automatisch nach etwa 2 Minuten aus und schont die LED der Hintergrundbeleuchtung (und spart ein wenig Strom).
Sobald ich wiederkomme, geht die Hintergrundbeleuchtung wieder an und das Display ist wieder gut ablesbar.
Hier der erweiterte und angepasste Code aus dem letzten Projekt:
lcd-2004a-pir.py (klicken, um diesen Abschnitt aufzuklappen)
# -*- encoding: utf-8 -*-
# (C) 2018 by Oliver Kuhlemann
# Bei Verwendung freue ich mich über Namensnennung,
# Quellenangabe und Verlinkung
# Quelle: http://cool-web.de/raspberry/
import RPi.GPIO as GPIO # Funktionen für die GPIO-Ansteuerung laden
from time import sleep # damit müssen wir nur noch sleep() statt time.sleep schreiben
import time
import datetime
from sys import exit # um das Programm ggf. vorzeitg zu beenden
import os
from subprocess import PIPE, Popen
GPIO.setmode(GPIO.BCM) # die GPIO-Pins im BCM-Modus ansprechen
pinLcdRS=9 # OUT LCD Command / Character Switch Leitung
pinLcdE=11 # OUT LCD Enable-Leitung
pinLcdData=[5,6,13,19] # OUT wir benutzen den 4-Pin-Data-Mode für das LCD
pinLcdBL=26 # OUT LCD lcdBacklight
pinPIR=10 # IN PIR-Bewegungsmelder
# GPIO-Ports initialisieren
GPIO.setup(pinLcdRS, GPIO.OUT)
GPIO.setup(pinLcdE, GPIO.OUT)
GPIO.setup(pinLcdBL, GPIO.OUT)
for pin in pinLcdData:
GPIO.setup(pin, GPIO.OUT)
GPIO.setup(pinPIR, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
lcdPulseDur = 0.001
lcdDelayDur = 0.001
# Defnitionen für ein 1602-Display
#lcdWidth = 16
#lcdHeight= 2
#addrLines=[0x00,0x40]
# Defnitionen für ein 2004-Display
lcdWidth = 20
lcdHeight= 4
addrLines=[0x00,0x40,0x14,0x54]
# Dictionary der Kommandos
cmds={"clear":0x01, # löscht alle Zeichen
"home":0x02, # springt an den Anfang (line 0, col 0)
"off":0b1000, # schaltet LCD aus (merkt sich aber den Text)
"on":0b1100, # schaltet LCD wieder ein mit Text (praktisch zum blinken)
"curon":0b1111, # cursor an
"curoff":0b1100 # cursor wieder aus
}
# Folgende ASCII-Zeichen werden auf das richtig LCD-Zeichen übersetzt
zeichen="°äöüÄÖÜßµ€"
ersatz=[223,225,239,245,225,239,245,226,228,227]
def lcdBacklight(onOff): # 1 schaltet die Hintergrundbeleuchtung an
GPIO.output(pinLcdBL, onOff)
def pulseEnable(): # Einen Pulse auf die Enable-Leitung schicken
sleep(lcdDelayDur)
GPIO.output(pinLcdE, 1)
sleep(lcdPulseDur)
GPIO.output(pinLcdE, 0)
sleep(lcdDelayDur)
def lcdByte(byte): # ein Byte im 4-Bit-Mode senden
# höherwertiges Halbbyte senden
for bnr in range (4,8):
bit=(byte & 2**bnr) / 2**bnr
GPIO.output(pinLcdData[bnr-4], bit)
pulseEnable()
# niederwertiges Halbbyte senden
for bnr in range (0,4):
bit=(byte & 2**bnr) / 2**bnr
GPIO.output(pinLcdData[bnr], bit)
pulseEnable()
def lcdCmd(cmd): # ein Kommando ans LCD senden
try:
byte=cmds[cmd]
except KeyError:
print "Das Kommando " + char + " ist nicht definiert."
return
lcdCmdByte(byte)
def lcdCmdByte(byte): # Kommando als Byte-Wert senden
GPIO.output(pinLcdRS, 0) # 0=command, 1=character
lcdByte(byte)
def lcdMsg(line, col, msg): # line ist die x. Zeile (gezählt ab 0)
# col die Spalte (ab 0)
lcdCmdByte(addrLines[line]+0x80+col) # Speicheradresse für Line/Col adressieren
GPIO.output(pinLcdRS, 1) # 0=command, 1=character
za=col+1
for char in msg:
byte=(ord(char)) # ord gibt ASCII-Code eines Zeichens zurück
p=zeichen.find(char) # einige zeichen sind nicht ASCII-konform, diese übersetzen
if p >-1:
byte=ersatz[p]
lcdByte(byte)
if za % lcdWidth == 0:
line+=1 # Zeile zu lang, Zeilenumbruch und in nächster Zeile weiter
try:
lcdCmdByte(addrLines[line]+0x80)
GPIO.output(pinLcdRS, 1) # und wieder zurück in den Zeichenmodus
except IndexError: # Display voll
break
za+=1
def lcdInit():
# LCD initialisieren
lcdCmdByte(0x33) # 110011 Initialize
lcdCmdByte(0x32) # 110010 Initialize
lcdCmdByte (0b101000) # Function Set Command
# ^----- DL: 4-bit-mode (optional 8-bit-Mode)
# ^---- N: 2-line-mode (optional 1-line-mode)
# ^--- F: 5x8 font (optional 5x11 font)
lcdCmdByte (0b1100) # Display on/off Command
# ^----- D: Display on
# ^---- C: Cursor off
# ^--- B: Cursor Pos. off
lcdCmdByte (0b110) # Cursor or Display Shift Command
# ^----- S/C: 1=Shift, 0=Cursor
# ^---- R/L: 1=Right, 0=Left
#
lcdCmd("clear")
def showZs(): # zeigt die eingebauten Zeichen an
lcdCmd("clear")
beg=32
c=beg
line=0
lcdCmdByte(addrLines[0]+0x80)
while c <256:
GPIO.output(pinLcdRS, 1)
lcdByte (c)
c+=1
if (c-beg) % (lcdHeight*lcdWidth) == 0:
sleep(3)
lcdCmd("clear")
line=0
elif (c-beg) % lcdWidth == 0:
line+=1
lcdCmdByte(addrLines[line]+0x80)
GPIO.output(pinLcdRS, 1)
def showZsCGRAM(): # zeigt die selbst definierten Zeichen an
lcdMsg(0,0,"own CGRAM chars:")
lcdCmdByte(addrLines[1]+0x80)
GPIO.output(pinLcdRS, 1)
for c in range (0,8):
lcdByte (c)
for c in range (0,8):
lcdByte(32)
def saveCharToCGRAM(charNr, arrMuster): # speichert ein eigenes Zeichen
lcdCmdByte(charNr*8+0x40)
GPIO.output(pinLcdRS, 1)
for byte in arrMuster:
lcdByte(byte)
sleep (lcdDelayDur)
def loadCustomChars(): # definiert 8 eigene Zeichen (ansprechbar über chr(0) bis chr(7)
muster = [ 0b10000 , # Strich Breite 1
0b10000 ,
0b10000 ,
0b10000 ,
0b10000 ,
0b10000 ,
0b10000 ,
0b10000 ]
saveCharToCGRAM (0, muster)
muster = [ 0b11000 , # Balken Breite 2
0b11000 ,
0b11000 ,
0b11000 ,
0b11000 ,
0b11000 ,
0b11000 ,
0b11000 ]
saveCharToCGRAM (1, muster)
muster = [ 0b11100 , # Balken Breite 3
0b11100 ,
0b11100 ,
0b11100 ,
0b11100 ,
0b11100 ,
0b11100 ,
0b11100 ]
saveCharToCGRAM (2, muster)
muster = [ 0b11110 , # Balken Breite 4
0b11110 ,
0b11110 ,
0b11110 ,
0b11110 ,
0b11110 ,
0b11110 ,
0b11110 ]
saveCharToCGRAM (3, muster)
muster = [ 0b01110 , # Batterie-Symbol
0b11111 ,
0b10001 ,
0b10001 ,
0b10001 ,
0b10001 ,
0b10001 ,
0b11111 ]
saveCharToCGRAM (4, muster)
muster = [ 0b00000 , # CPU-Symbol
0b11111 ,
0b01110 ,
0b11111 ,
0b01110 ,
0b11111 ,
0b00000 ,
0b00000 ]
saveCharToCGRAM (5, muster)
muster = [ 0b01110 , # RAM-Symbol
0b11011 ,
0b01010 ,
0b11011 ,
0b01010 ,
0b11011 ,
0b01110 ,
0b00000 ]
saveCharToCGRAM (6, muster)
muster = [ 0b00000 , # SD-Karten-Symbol
0b11110 ,
0b10001 ,
0b10001 ,
0b10001 ,
0b10001 ,
0b11111 ,
0b00000 ]
saveCharToCGRAM (7, muster)
def loadCustomSmiley (charNr):
muster = [ 0b00000 , # Smiley
0b01010 ,
0b01010 ,
0b00000 ,
0b10001 ,
0b10001 ,
0b01110 ,
0b00000 ]
saveCharToCGRAM (charNr, muster)
def loadCustomCheckmark (charNr):
muster = [ 0b00001 , # Checkmark
0b00001 ,
0b00010 ,
0b00010 ,
0b10100 ,
0b10100 ,
0b01000 ,
0b00000 ]
saveCharToCGRAM (charNr, muster)
def loadCustomEuro(charNr):
muster = [ 0b00011 , # Euro-Zeichen
0b00100 ,
0b11111 ,
0b01000 ,
0b11111 ,
0b00100,
0b00011 ,
0b00000 ]
saveCharToCGRAM (charNr, muster)
def showProzentBalken(line,startCol,prozent): # zeigt einen fein abgestuften Prozent-Balken
# benutzt selbstdefinierte Chars
if prozent == 0: # es gibt nichts anzuzeigen
return(0)
breiteGes=lcdWidth*5-startCol*5
breite=int(breiteGes*prozent/100)+1
if breite > breiteGes:
breite = breiteGes
voll=int(breite/5)
rest=breite % 5
# die vollen 5-Balken darstellen (char Nr. 255)
lcdCmdByte(addrLines[line]+0x80+startCol)
GPIO.output(pinLcdRS, 1)
for n in range (0,voll):
lcdByte (255)
# gefolgt vom Rest
if rest > 0:
lcdByte(rest-1)
# --- Ende Funktionen --- Beginn Hauptprogramm -------------------------------------------
try:
lcdInit() # wichtig. Ohne diesen Befehl macht das LCD nur Müll.
loadCustomChars() # selbst definierte Zeichen laden
screen=1 # voreingestellter Screen
weekdays=["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]
lcdBacklight(1) # lcdBacklight anschalten
seksPerMove = 2*60 # wieviele Sekunden bleibt das Display pro Bewegung an?
lastMove = time.time()
# Variablen zum Wertevergleich, um unnötige Ausgaben auf LCD zu sparen
altDatum=""
altSdGb=""
altScreen=""
altRamMbFree=""
altTakt=""
za=0 # wird in der Schleife hochgezählt, so kann z. B. nur alle 100 Sek. etwas gemacht werden
while 1:
ts=time.time()
if GPIO.input(pinPIR) == GPIO.HIGH: # Es wurde eine Bewegung festgestellt
lastMove = time.time()
lcdBacklight(1)
seksUnbewegt = int(ts - lastMove)
if seksUnbewegt > seksPerMove:
lcdBacklight(0)
# print GPIO.input(pinPIR), seksUnbewegt
if screen ==1:
# Datum, Zeit (mit Wochentag)
ts = datetime.datetime.now()
weekday= int(ts.strftime("%w"))
datum=weekdays[weekday] + ts.strftime(" %d.%m.%y")
zeit=ts.strftime("%H:%M:%S")
if datum <> altDatum:
lcdMsg (0,0,datum)
lcdMsg (0,12,zeit)
altDatum=datum
# CPU-Auslastung
proc=os.popen("ps -eo pcpu")
lines=proc.readlines()
proc.close()
cpu=0.0
for i in range (1,len(lines)):
cpu += float(lines[i])
if cpu > 100:
cpu = 100 # weil mehr nicht sein kann
msg= chr(5)+ str(cpu) + "% "
lcdMsg (2,0,msg)
# Takt
proc=os.popen(" vcgencmd measure_clock arm | awk -F '=' '{print $2}'")
takt=round(float(proc.readline())/1000000000,1)
proc.close()
takt=str(takt)+"GHz "
if takt <> altTakt:
lcdMsg (2,8,takt)
altTakt=takt
# Temperatur
proc=os.popen("cat /sys/class/thermal/thermal_zone*/temp")
temp=float(proc.readline())
proc.close()
msg=str(round(temp/1000,1)) + "°"
lcdMsg (2,15,msg)
# RAM frei
proc=os.popen(" free --mega | awk '{print $4}'")
lines=proc.readlines()
ramMbFree=lines[1].replace(chr(10),"")
proc.close()
ramMbFree=chr(6)+""+ ramMbFree + "MB "
if ramMbFree <> altRamMbFree:
lcdMsg (3,0,ramMbFree)
altRamMbFree = ramMbFree
# GB auf der SD free
proc=os.popen("df -h / | awk '{print $3}'")
lines=proc.readlines()
sdGb=lines[1].replace(",",".").replace("G","").replace(chr(10),"")
proc.close()
sdGb=chr(7)+""+ sdGb + "GB "
if sdGb <> altSdGb:
lcdMsg (3,8,sdGb)
altSdGb=sdGb
if altScreen <>"1/1":
lcdMsg (3,17,"1/1")
altScreen="1/1"
sleep (1)
za += 1
except KeyboardInterrupt:
pass
lcdCmd("clear")
lcdCmd("off")
GPIO.cleanup() # Programm sauber verlassen und Ressourcen wieder freigeben
Der bezüglich des PIR-Sensors hinzugekommene Code:
pinPIR=10 # IN PIR-Bewegungsmelder
GPIO.setup(pinPIR, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
...
seksPerMove = 2*60 # wieviele Sekunden bleibt das Display pro Bewegung an?
...
if GPIO.input(pinPIR) == GPIO.HIGH: # Es wurde eine Bewegung festgestellt
lastMove = time.time()
lcdBacklight(1)
seksUnbewegt = int(ts - lastMove)
if seksUnbewegt > seksPerMove:
lcdBacklight(0)
...
Die Leitung für den PIR-Sensor wird wie angeschlossen als Pin BCM 10 definiert und als Input mit PullDown gesetupt. In der Schleife wird dann der Input abgefragt. Bei jeder Bewegung geht der Sensor für ca. 3 Sekunden auf High. Dann wird sofort das Backlight eingeschaltet (bzw. bleibt an). Wurde die definierte Zeit von 120 Sekunden keine Bewegung festgestellt, wird die Hintergrundbeleuchtung abgeschaltet.