https://www.pexels.com/photo/red-raspberry-fruit-on-white-ceramic-tray-128866/

Poznajemy się z Raspberry Pi i budujemy środowisko uruchomieniowe cz. 2

Przyszła pora, aby zabrać się za programowanie. Pierwsze kroki z kodem w tym projekcie postawiłem po stronie sprzętu. Poniżej znajdziesz opis procesu uruchamiania kolejnych peryferiów, które podłączyłem do mojego RPI. Aktualne wersje poniższych skryptów napisanych w Pythonie (v3.5.2) znajdziesz również na GitHub.

python.org

Dwa modele numeracji pinów GPIO: BOARD i BCM

Listwa czterdziestu pinów dostępna na mojej malince (RPI 3) zawiera 26 wyprowadzeń GPIO (General Purpose Input Output), czyli dwukierunkowych wyprowadzeń ogólnego przeznaczenia. To one posłużą mi do podłączenia peryferiów w postaci czujników i układów wykonawczych. Żeby programowo obsłużyć wybrane wyprowadzenie, należy wiedzieć jak je nazwać lub jak kto woli, jaki ma adres, numer itp. Projektanci przyjęli konwencję w której kolejne piny nazywane są np. GPIO22. Jeśli w programie chcemy wykorzystać to wyprowadzenie, używamy liczby 22. Ten sposób numerowania przetłumaczyłem sobie jako konwencja numerowania BCM. Nie jest to dla mnie zbyt naturalne, wolę numerowanie fizycznych wyprowadzeń czyli konwencję BOARD, w której kolejne wyprowadzenia mają następujące po sobie numery. Przykładowo BCM = 22 odpowiada BOARD = 15. Jeśli opis nie jest zbyt jasny, bardziej multimedialne wytłumaczenie można znaleźć m. in. tu.

Załączanie przekaźnika i triaka

Najprostsze operacje jakie można wykonać z zastosowaniem GPIO to odczyt stanu wejścia cyfrowego oraz ustawienie wyjścia. W zasadzie w tym projekcie większość komponentów będzie w ten sposób sterowana, za wyjątkiem czujnika temperatury DS18B20, który do komunikacji wykorzystuje interfejs 1-wire oraz czujnika DHT11. Wracając do wyjść cyfrowych oraz sterowania przekaźnikiem i triakiem, schemat postępowania jest następujący:

  1. Ustaw opcję numeracji pinów GPIO
  2. Konfiguruj pin jako wyjściowy
  3. Ustaw stan wyjścia HI lub LOW

I tyle w temacie. Czy sterujemy diodą, przekaźnikiem czy żarówką załączaną triakiem, działamy w ten sam sposób. Poniżej zamieszczam krótki skrypt w Pythonie, który ma zobrazować jak to działa. Komentarze #1, #2 oraz #3 odpowiadają powyższym krokom.

# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
import time
import sys
 
RELAY = 3
TRIAC = 21
 
def initialize():
        GPIO.setmode(GPIO.BOARD) #1
        GPIO.setup(RELAY, GPIO.OUT) #2
        GPIO.setup(TRIAC, GPIO.OUT)
 
def relay_triac_change(triac_pin, relay_pin):
        try:      
                GPIO.output(triac_pin,GPIO.HIGH)
                GPIO.output(relay_pin,GPIO.HIGH) #3
                time.sleep(1)
                GPIO.output(triac_pin,GPIO.LOW)  
                GPIO.output(relay_pin,GPIO.LOW)  #3
                time.sleep(1)  
                return
        except KeyboardInterrupt as ex:
                print("Przerwane przez użytkownika")
                GPIO.cleanup()
                sys.exit()
        except Exception as ex:
                print(str(ex))
                GPIO.cleanup()
                sys.exit()
 
if __name__ == "__main__":
        initialize()
        while True:
                relay_triac_change(TRIAC, RELAY)

Działanie skryptu jest następujące:

  1. Ustaw kodowanie pliku
  2. Załącz wymagane pakiety
  3. Ustaw numery pinów, pod które podłączone są układy wykonawcze (zgodnie z notacją BOARD)
  4. Wykonaj inicializację, tj. ustaw opcję numeracji pinów
  5. W pętli nieskończonej zmieniaj stan przekaźnika i triaka co sekundę
  6. Przerwanie działania skryptu inicjowane jest przez użytkownika (CTRL+C)

Odczyt temperatury z czujnika DS18x20

Ten czujnik jest chyba najciekawszym elementem, który chcę podpiąć do mojego RPI. Mówiąc „najciekawszym” mam na myśli część programową, bo fizyczne podpięcie jest trywialne. Czujnik wymaga podłączenia zasilania, GND oraz linii danych podciągniętej do zasilania za pomocą rezystora (pull-up). O czym wcześniej chyba nie wspominałem, w bardzo łatwy sposób możemy zwiększyć liczbę sensorów podłączając je w sposób równoległy. Każdy z czujników posiada swój unikatowy numer ID, dzięki któremu pomimo podłączenia wielu czujników do jednego pinu, będą one rozróżnialne.

O ile się nie mylę, Raspberry Pi ma domyślnie przypisany pin nr 4 dla interfejsu 1-wire, jednak wcześniej został on przeze mnie wykorzystany do czegoś innego. Oczywiście możliwa jest konfiguracja, wystarczy drobna zmiana w pliku /boot/config.txt. W mojej konfiguracji znalazła się tam następująca linijka, która ustawiła to co chciałem, czyli pin 22 według zapisu BCM (15 według notacji BOARD).

dtoverlay=w1-GPIO,gpiopin=22

W tym miejscu przyczepiłem do monitora żółtą karteczkę, która ma za zadanie przypominać mi o tym, żeby sprawdzić, której notacji powinienem użyć. Czujnik nie chciał zadziałać, a powodem był właśnie zły numer pinu, tzn. dobry, ale w złej notacji. Do tej pory operowałem na notacji BOARD, a plik konfiguracyjny wymagał numeru pinu według oznaczeń BCM.
Kolejnym istotnym krokiem jest załadowanie modułów w1-gpio oraz w1-therm do jądra systemu. W sytuacji gdy poprawnie skonfigurowaliśmy pin dla 1-wire oraz wgraliśmy sterowniki, w katalogu /sys/bus/w1/devices znajdziemy podkatalogi zaczynające się od 28-. Części nazwy tych folderów, znajdujące się po myślniku, to numery seryjne podłączonych czujników. Jak widać poniżej, w moim środowisku testowym podłączyłem dwa sensory.

pi@raspberrypi:/sys/bus/w1/devices $ sudo modprobe w1-gpio 
pi@raspberrypi:/sys/bus/w1/devices $ sudo modprobe w1-therm
pi@raspberrypi:/sys/bus/w1/devices $ ls
28-000001a70d9d  28-041633ea4eff  w1_bus_master1

Operacje związane z obsługą DS18B20 to nic innego jak proste operacje na plikach. I tak liczbę podłączonych czujników odczytamy w poniższy sposób:

pi@raspberrypi:/sys/bus/w1/devices $ cat w1_bus_master1/w1_master_slave_count 
2

Listę numerów ID sensorów odczytamy poprzez wyświetlenie pliku /sys/bus/w1/devices/w1_bus_master1/w1_master_slaves. Czego się spodziewałem, polecenie wypisało te same numery co dwa kroki wcześniej :]

pi@raspberrypi:/sys/bus/w1/devices $ cat w1_bus_master1/w1_master_slaves
28-000001a70d9d
28-041633ea4eff

To o co nam chodzi, czyli wartość zmierzonej temperatury znajdziemy w podkatalogach z serii 28-*, wyświetlając zawartość w1_slave.

pi@raspberrypi:/sys/bus/w1/devices $ cat 28-041633ea4eff/w1_slave 
69 01 4b 46 7f ff 0c 10 7d : crc=7d YES
69 01 4b 46 7f ff 0c 10 7d t=22562
pi@raspberrypi:/sys/bus/w1/devices $ cat 28-000001a70d9d/w1_slave 
71 01 4b 46 7f ff 0f 10 56 : crc=56 YES
71 01 4b 46 7f ff 0f 10 56 t=23062

Jak widać, temperatura nie jest tu podana w sposób jawny, natomiast ten format pozwala w łatwy sposób domyślić się o co chodzi. Istotne informacje, które z łatwością możemy wyczytać, znajdują się na końcu każdej linii. Pierwsza z nich informuje o poprawności transmisji, znajduje się tam suma kontrolna CRC z informacją czy transmisja danych z czujnika się udała (YES). Druga linia zawiera wartość temperatury wymnożoną przez 1000, czyli dla przykładowego czujnika o ID równym 041633ea4eff, wartość temperatury wynosi 22,562 °C.

Nie po to realizuję ten projekt, żeby odczytywać temperaturę ręcznie, a już na pewno z taką częstotliwością. Wykona to za mnie skrypt w Pythonie.

# -*- coding: utf-8 -*-
import os
import sys
 
def initialize():
        print("Liczba znalezionych czujników: " + get_sensors_count())
 
def get_sensors_IDs():
        path = '/sys/bus/w1/devices/w1_bus_master1/w1_master_slaves'
        file = open(path,'r')
        lines = file.readlines()
        file.close()
 
        ids = []
        for line in lines:
                ids.append(line.strip('\n'))       
 
        return ids
 
def get_sensors_count():
        path = '/sys/bus/w1/devices/w1_bus_master1/w1_master_slave_count'
        file = open(path,'r')
        lines = file.readlines()
        file.close()
 
        amount = 0
        for line in lines:
                amount = line.strip('\n')
 
        return amount
 
def get_temperature(sensor_id):
        path = '/sys/bus/w1/devices/' + sensor_id + "/w1_slave"
        file = open(path,'r')
        lines = file.readlines()
        file.close()
 
        if 'YES' == lines[0].strip('\n')[-3:]:
                return float(lines[1].strip('\n')[-5:])/1000
        else:
                return -1
 
if __name__ == "__main__":
        initialize()
        ids = get_sensors_IDs()
        print("ID czujników: ")
        print(ids)
        while True:
                for sensor_id in ids:
                        try:
                                temp = get_temperature(sensor_id)
                                if temp != -1:
                                      print(sensor_id + ": " + str(temp) + u' \u00B0C')
                                else:
                                      print('Nieprawidłowa suma kontrolna CRC')
                        except KeyboardInterrupt as ex:
                                print("Przerwane przez użytkownika")
                                sys.exit()
                        except Exception as ex:
                                print(str(ex))

Co prawda nie jest to skomplikowany kod najnowszego Wiedźmina ani systemu Windows 10, ale kilka słów komentarza się przyda. Skrypt rozpocznie swoje działanie od funkcji initialize(), która wyświetli liczbę podłączonych sensorów (funkcja get_sensors_count()). W następnym kroku skrypt pobierze listę numerów ID wszystkich czujników (funkcja get_sensors_IDs()). Są one potrzebne, aby w nieskończonej pętli While iterować po każdym z nich oraz odczytywać temperaturę, która w dalszej kolejności będzie wyświetlona w terminalu. Przerwanie działania programu następuje na żądanie użytkownika (CTRL+C).

Jeśli chodzi o implementację funkcji get_sensors_count(), get_sensors_IDs oraz get_temperature(sensor_id) to jest to w zasadzie prosta obsługa (odczyt) plików tekstowych. Jedynie w tej ostatniej pojawia się coś ciekawszego, tzn. warunek sprawdzenia poprawności komunikacji oraz konwersja wartości temperatury do liczby zmiennoprzecinkowej.

Żeby nie było niedopowiedzeń, skrypt jednak działa:), a jego wynik może przyjąć taką formę:

pi@raspberrypi:~/Blog/HomeController/RaspberryPi $ python ds18B20.py 
Liczba znalezionych czujników: 2
ID czujników: 
['28-000001a70d9d', '28-041633ea4eff']
28-000001a70d9d: 23.062 °C
28-041633ea4eff: 28.437 °C
28-000001a70d9d: 23.125 °C
28-041633ea4eff: 28.625 °C
28-000001a70d9d: 23.187 °C
28-041633ea4eff: 28.687 °C

Kontaktron

W pierwszej części dotyczącej konfiguracji sprzętowej pisałem, że kontaktron podłączyłem do pinu GPIO oraz GND. Teraz dodam, że wykorzystałem piny nr 6 oraz 8 (według oznaczeń BOARD). Jeśli chodzi o obsługę programową, tym razem nie korzystam z wyjścia, a wejścia cyfrowego. W związku z tym konfiguracja w funkcji initialize() będzie wyglądała trochę inaczej. Przede wszystkim ustawiam kierunek pinu poprzez parametr GPIO.IN. Dodatkowo wykorzystuje to, co oferują piny GPIO płytki RPI, czyli wewnętrzne rezystory podciągające do VCC (pull-up). Rezystory podciągające stosuje się w przypadku obsługi takich elementów jak np. przyciski, w których stan OFF skutkowałby pływającym pinem (floating). Jest to generalnie negatywne zjawisko, gdyż w zasadzie nie można przewidzieć w jakim stanie pin się aktualnie znajduje, a dodatkowo ten stan się może zmieniać. Zastosowanie takich wewnętrznych rezystorów ogranicza konieczność podłączenia kolejnego zewnętrznego elementu. Taka sytuacja występuje również w tym przypadku, gdy magnez jest oddalony od kontaktronu o więcej niż wartość graniczna (u mnie ok 20 mm). Kontaktron jest wtedy rozwarty, a pin wisi w powietrzu.

# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
import time
import sys
 
SWITCH = 8
TRIAC = 21
 
def initialize():
        GPIO.setmode(GPIO.BOARD)
        GPIO.setup(SWITCH, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(TRIAC, GPIO.OUT)
 
def triac_change(triac_pin, state):
        GPIO.output(triac_pin, state)
        return
 
if __name__ == "__main__":
        initialize()
        while True:
                try: 
                        switch_state = GPIO.input(SWITCH)
                        if switch_state == 1:
                                triac_change(TRIAC,GPIO.HIGH)
                        else:
                                triac_change(TRIAC,GPIO.LOW)
                except KeyboardInterrupt as ex:
                        print("Przerwane przez użytkownika")
                        GPIO.cleanup()
                        sys.exit()
                except Exception as ex:
                        print(str(ex))
                        GPIO.cleanup()
                        sys.exit()

Jeśli chodzi o dalszą cześć skryptu, widać spore podobieństwo do tych poprzednich. W głównej, nieskończonej pętli While odpytuję stan pinu, do którego podłączony jest kontaktron. W zależności od odczytanej wartości (czyli od faktu czy kontaktron jest zwarty lub rozwarty, a więc czy magnez jest dostatecznie blisko czy nie), załączam lub wyłączam żarówkę sterowaną poznanym wcześniej układem z triakiem.

Odczyt temperatury i wilgotności z DHT11

Dawno temu, gdy byłem jeszcze uczniem, zawsze chciałem zrobić wszystko po swojemu, żeby dokładnie zrozumieć o co chodzi. Taka dociekliwość zapewne jest bardzo istotna, ale niestety jej głównym minusem jest fakt, że proces dogłębnego poznania zawsze zajmuje dużo więcej czasu niż skorzystanie z gotowego rozwiązania. Teraz już wiem, że zawsze warto jak nie zastosować, to chociaż rozważyć wykorzystanie gotowca. Dla czujnika DHT11 zastosowałem właśnie takie rozwiązanie, wykorzystując bibliotekę DHT11 Python library. Niech ten link będzie ostatnim komentarzem do tego tematu, gdyż na stronie biblioteki znajduje się doskonały przykład zastosowania. Dodam tylko, że biblioteka jest na licencji MIT.

Wykrycie obecności wody oraz detekcja ruchu za pomocą PIR

Działanie pozostałych dwóch czujników zdecydowałem się pokazać na jednym skrypcie, ponieważ jest ono w zasadzie identyczne i polega na odczycie stanu wejścia cyfrowego. Skrypt odpytuje w pętli głównej While stany czujników. Jeśli czujnik wykryje obecność wody i jednocześnie drugi sensor wykryje ruch, dioda LED zasygnalizuje „alarm”.

# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
import sys
 
WATER_SENSOR = 10
LED = 11
PIR = 19
 
def initialize():
        GPIO.setmode(GPIO.BOARD)
        GPIO.setup(WATER_SENSOR, GPIO.IN)
        GPIO.setup(PIR, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(LED, GPIO.OUT)
 
def led_change(led_pin, state):
        GPIO.output(led_pin,state)
 
if __name__ == "__main__":
        initialize()
        while True:
                try: 
                        water_sensor_state = GPIO.input(WATER_SENSOR) # 0 - obecnosc wody
                        PIR_state = GPIO.input(PIR) # 1 - wykrycie ruchu
                        if (PIR_state == 1 and water_sensor_state == 0):
                                led_change(LED,GPIO.LOW)
                        else:
                                led_change(LED,GPIO.HIGH)
                except KeyboardInterrupt as ex:
                        print("Przerwane przez użytkownika")
                        GPIO.cleanup()
                        sys.exit()
                except Exception as ex:
                        print(str(ex))
                        GPIO.cleanup()
                        sys.exit()

Funkcja initialize() konfiguruje piny WATER_SENSOR oraz PIR jako wejścia cyfrowe, przy czym w przypadku czujnika ruchu wykorzystuję dodatkowo wbudowany rezystor podciągający, identycznie jak dla kontaktronu. Czujnik obecności wody również podłączony jest przez rezystor podciągający do zasilania, jednak wykorzystałem zewnętrzny element o wartości 1 MΩ.

Literatura obowiązkowa:
GPIO: MODELS A+, B+, RASPBERRY PI 2 B AND RASPBERRY PI 3 B
Raspberry Pi pinouts
HOW TO SET UP THE DHT11 HUMIDITY SENSOR ON THE RASPBERRY PI
DHT11 Python library
DS18B20+ One Wire Digital Temperature Sensor and the Raspberry Pi
Pull-up Resistors
Pull Up and Pull Down Resistors

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *