ESP8266, IoT – praktyczny przykład cz. 2

Poprzednio pokazałem jak podłączyć się do modułu ESP8266, wgrać firmware NodeMCU oraz podpiąć się do sieci WiFi. Teraz, czas na interakcje z modułem przez sieć, bo to jest jeden z aspektów IoT – możliwość wpłynięcia na zachowanie urządzeń przez sieć.

W poprzedniej części napisałem, że koniecznie trzeba nazwać nagrywany plik init.lua. Plik o takiej nazwie jest automatycznie (po restarcie) wykonywany przez ESP8266 z firmware nodeMCU. Jeśli chcesz możesz oczywiście podzielić swój kod na różne pliki, wystarczy w init.lua wydać komendę dofile("NAZWA") by wykonać kod zawarty w pliku NAZWA.
Środowisko ESPlorer niestety nie zna pojęcia projekt, więc potem każdy plik, który chcesz używać trzeba otworzyć ręcznie (przez Ctrl+O) – nie jest to wygodne, ale cóż zrobić.

UWAGA w tym wpisie zajmujemy się podłączaniem urządzeń wykorzystujących zasilanie z sieci 230V. Jeżeli jesteś zupełnym laikiem, nie masz doświadczenia w pracy z takimi napięciami – koniecznie znajdź kogoś doświadczonego, kto może Ci pomóc. Nasz artykuł jest przeznaczony dla osób, które wiedzą jak się obchodzić z takimi napięciami. Nie masz doświadczenia – nie rób tego sam.

Zgodnie z zapowiedzią, naszym celem jest zbudowanie serwera WWW na ESP8266, który przez stronę WWW pozwoli na zdalne kontrolowanie urządzenia elektrycznego.

Najpierw – jak sterować urządzeniem elektrycznym? Jak bezpiecznie je włączyć lub wyłączyć? Użyjemy do tego celu przekaźników. Co to jest przekaźnik? Pomyśl o nim jak o włączniku. Posiada on trzy piny. Jest jedno wejście (tzw wejście wspólne) – tam podłączamy zasilanie. Bez zasilania, lub gdy nie jest wysterowany przekaźnik, wejście wspólne jest zwarte do jednego z wyjść. Gdy przekaźnik zostanie wysterowany, wejście wspólne zostanie odłączone od pierwszego wyjścia a przełączone do drugiego.

Przekaznik - jak działa
Przekaznik – jak działa

Do wejścia nr 2 podłącza się zasilanie (dla urządzenia elektrycznego). Gdy przekaźnik nie jest wysterowany wówczas napięcie jest przekazywane na wyjście nr 4. W momencie włączenia przekaźnika przez podanie zasilania na cewkę (wejście 1 i 5) przesuwa ona magnetycznie przełącznik i zasilanie jest teraz na wyjściu nr 3 – tak długo jak cewka jest zasilana.

By zasilić urządzenie 230V – przewód „gorący” (zwykle ten brązowy przewód, ale zawsze korzystaj z próbnika, by upewnić się który to jest przewód) wpina się do wejścia nr 2 i prowadzi przewód z wyjścia nr 3 do urządzenia. Przewód ochronny i neutralny prowadzi bezpośrednio do urządzenia.

Można oczywiście używać samych przekaźników, ale nie jest to takie proste, potrzebne są dodatkowe elementy – tranzystory sterujące, diody zabezpieczające, przydaje się odizolowanie optoelektryczne. Dlatego polecamy użycie gotowego modułu przekaźników, które są „wszystkomające” – wszystko to o czym pisałem wcześniej, czyli układ sterujący, zabezpieczenia, optoizolację. Mamy w ofercie np takie dwukanałowe moduły – pozwalają one sterować dwoma urządzeniami.

Moduły te są sterowane niskim poziomem logicznym, tzn przy podłączeniu jak opisane wcześniej by zasilić urządzenie podłączone do modułu przekaźnika, na wejście sterujące trzeba podać stan niski (zero).

Moduł przekaźników
Moduł przekaźników

Każdy z przekaźników ma trzy złącza śrubowe. Środkowe, to wspólne wejście, prawe (patrząc od strony złącz śrubowych) domyślne wyjście podłączone oraz po lewej wyjście podłączone przy wysterowaniu stanem niskim. Zasilane są one 5V (więc jeśli użyć będziesz ich chciał w docelowym układzie z ESP8266, to musisz mieć źródło napięcia 3.V dla ESP8266 i 5V dla przekaźników).

W naszym układzie w pierwszej części użyliśmy Arduino UNO jako źródła zasilania dla ESP8266. W tym przypadku jest to kolejna zaleta, bo mamy od razu źródło napięcia 5V do zasilania przekaźników (takie jest napięcie sterujące cewką w przekaźniku). Masę całego układu łączmy z masą modułu przekaźników (pin GND z lewej strony). Najlepszy będzie do tego przewód F-M, wtykamy go w płytkę stykową w szynę zasilającą z masą (niebieska kreska na płytce stykowej) a drugi koniec nakładamy na pin GND. Zasilanie 5V podłączamy takim samym przewodem z gniazda 5V na Arduino do pinu VCC na module przekaźników.

Piny GPIO w ESP8266 i sterowanie w nodeMCU

Nim podłączymy piny sterujące (IN1 oraz IN2) kilka słów o sterowaniu pinami GPIO w ESP8266

Pracując z NodeMCU warto korzystać z opisu modułów. Ta strona to na dziś oficjalne repozytorium z dokumentacją. W naszym projekcie interesuje nas opis korzystania z pinów GPIO, co załatwia moduł gpio. Jedną z podstawowych informacji jest tabelka z opisem numerów GPIO używanych przez nodeMCU a nazwami używanymi na ESP8266. I tak, użyjemy GPIO4 i GPIO5 które wg konwencji nodeMCU mają numer 1 i 2.

Podobnie jak w innych mikrokontrolerach piny GPIO mogą pracować jako wejście lub wyjście. Dlatego pierwszym krokiem jest ustawienie trybu pracy:

gpio.mode(1, gpio.OUTPUT)
gpio.mode(2, gpio.OUTPUT)

Piny są gotowe do sterowania przekaźnikami, ale najpierw musimy mieć serwer WWW.

Serwer WWW na ESP8266

Pierwszy krok, to definicja serwera TCP (TCP to rodzaj połączeń sieciowych, które są używane do transmisji stron WWW):

srv = net.createServer(net.TCP)

Mamy obiekt srv obsługujący połączenia TCP, ale to nie jest wszystko. Połączeń TCP do jednego adresu IP może być bardzo dużo, więc by móc mieć różne serwery TCP na jednym adresie IP protokół przewiduje jeszcze numer portu. Dopiero ten numer z adresem IP pozwala w pełni zaadresować połączenie TCP. Dla serwerów WWW domyślnym numerem portu jest 80. Musimy więc zdefiniować usługę działającą w obrębie serwera TCP na porcie 80. O usłudze oczekującej na połączenia mówi się że „nasłuchuje” (ang. listening) dlatego moduł net oferuje funkcję net.server.listen. Przyjmuje ona za argumenty kolejno: nr portu i funkcję która będzie wywoływana dla każdego połączenia przychodzącego na ten port. Funkcja obsługująca połączenie musi akceptować jeden argument, który będzie zmienną net.socket (jego dokumentacja). To gniazdo (ang. socket) służy do komunikacji z przeglądarką która jest „po drugiej stronie” połączenia. Zaraz się przekonamy w szczegółach jak to działa:

srv:listen(80,function(conn)
 --KOD
 end)

wewnątrz funkcji KOD zostanie zastąpiony przez coś co obsłuży reakcję naszego ESP na połączenie przychodzące. By to lepiej zrozumieć sprawdźmy taki przykład:

srv:listen(80,function(conn)
 --co zrobic na polaczenie przychodzace
 conn:on("receive",function(conn,payload)
   print(payload)
   conn:send('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nOK')
 end)
 --co po wyslaniu danych
 conn:on("sent",function(conn)
   conn:close()
 end)
end)

UWAGA Kolejny minus środowiska ESPlorer (nie wiem, może to „wina” nodeMCU) jest taki, że nie obsługuje poprawnie znaków UTF, więc nie używaj nigdzie polskich znaczków – zarówno w łańcuchach jak i komentarzach….

Jest to minimalny serwer WWW (daaaleki od poprawności i kompletności). Wewnątrz funkcji go obsługującej definiujemy reakcję na dwa zdarzenia. Pierwsze to „receive” –  gdy nasz serwer WWW odbierze całe zapytanie od przeglądarki. Drugie zdarzenie to „sent” symbolizujące moment w którym ESP skończy nadawać odpowiedź do przeglądarki.

W tym drugim zdarzeniu zamykamy połączenie przez conn:close(). Jest to bardzo ważny element każdego serwera TCP. Bez tego zamknięcia połączenia będą nadal aktywne i będą zużywać zasoby zarówno ESP jak i przeglądarki. Ponieważ ESP nie jest potężnym komputerem to szybko mogą się pojawić problemy z ponownym połączeniem (krótko mówiąc implementacje połączeń TCP zużywają pamięć na każde otwarte połączenie, więc zazwyczaj jest ograniczenie ilości otwartych połączeń). Mimo, że będzie działać, nie uda się połączyć z ESP, bo zabraknie miejsca na otwarcie nowego połączenia TCP, wszystkie będą czekać aż upłynie określony czas bez aktywności, nim się zamkną. Dlatego – zamykaj połączenie TCP gdy skończysz z niego korzystać.

Pierwsze zdarzenie, oprócz samego obiektu połączenia jako argument otrzymuje treść całego zapytania wysłanego przez przeglądarkę. W treści tej funkcji wysyłamy całość tego zapytania na konsolę ESP (print("payload")). Dlatego po wgraniu takiego programu na konsoli ESP powinna się pojawić treść, z której będziemy musieli wyłuskać dane nam potrzebne. Używając conn:send odsyłamy naszą odpowiedź. By nas przeglądarka WWW zrozumiała wymagane jest zachowanie pewnego formatu, najpierw informacja, że wszystko jest w porządku (HTTP/1.1 200 OK), potem informacja o tym że odsyłamy tekst, następnie linia odstępu i właściwa odpowiedź do wyświetlenia – czyli napis OK. Proszę wgraj oba pliki (Minimalistyczny serwer WWW) na ESP8266 zobacz jaki adres IP nadał router. Następnie wpisz ten adres w przeglądarkę. Powinieneś dostać pustą stronę z napisem OK. A teraz spójrz na konsolę ESP.

ESP odbiera dane jako serwer WWW
ESP odbiera dane jako serwer WWW

Jeśli masz doświadczenie z pisaniem aplikacji WWW, to pewnie to co zobaczyłeś/aś na konsoli powinno być Tobie doskonale znane. Jeżeli jednak nie masz takich doświadczeń powiem co jest najistotniejsze. Dla naszych celów najważniejsza jest pierwsza linia jaką przysłała przeglądarka. Tutaj to było:

GET / HTTP/1.1

Jest to informacja czego przeglądarka chce od serwera, dzieli się ona na trzy składowe: rodzaj zapytania (GET) jaki zasób chce otrzymać (/ czyli strona główna) oraz protokół jakiego używa przeglądarka (HTTP/1.1 – czyli HTTP w wersji 1.1).

Pozostałe linie to dodatkowe informacje, np o tym co jest w stanie zrozumieć przeglądarka. Serwery WWW zajmują się analizą tych danych przed przygotowaniem odpowiedzi. Nie jest to trywialne zadanie, ale na potrzeby tego przykładu nie potrzebujemy pisać serwera WWW realizującego wszystkie aspekty protokołu HTTP. Powstanie coś ułomnego, realizującego tylko mały podzbiór większej całości, ale nam to wystarczy do naszych testów.

By się przekonać jak zmieni się pierwsza linia wysyłana przez przeglądarkę, wpiszcie nowy adres w przeglądarkę. U nas ESP dostało adres 10.10.1.130, dopiszcie do adresu IP np 1/off czyli u nas to będzie http://10.10.1.130/1/off Pierwsza linia teraz wygląda tak

GET /1/off HTTP/1.1

Sterować przekaźnikami chcemy w ten sposób, by adres miał format http://IP_ESP/NR_PRZEKAŹNIKA/AKCJA Czyli, w naszej sieci, by włączyć przekaźnik nr 2 powinniśmy wysłać żądanie na adres http://10.10.1.130/2/on By go następnie wyłączyć trzeba użyć adresu http://10.10.1.130/2/off

Po odebraniu żądania z przeglądarki, musimy odczytać nr przekaźnika i akcję. Mając te dane możemy włączyć/wyłączyć przekaźnik lub wysłać stronę główną z kodem HTML jeśli nie rozpoznamy nr przekaźnika i akcji.

Odczyt żądania z przeglądarki

Piszemy więc taki ubogi URL parser (bo URL to właśnie nazwa na takie 'adresy’), bardzo go sobie upraszczając. Po pierwsze, nie będziemy się zastanawiać na rodzajem zapytania, po prostu zignorujemy czy to GET, POST czy inny. Dlatego najpierw znajdziemy pierwsze wystąpienie znaku ukośnika:

local e = string.find(request, "/")
if e == nil then return nil,nil end
local request_handle = string.sub(request, e + 1)

Pierwsza linia odnajduje pozycję na której występuje pierwszy znak ukośnika. Ostatnia w zmiennej request_handle zapisuje całość żądania od znaku za ukośnikiem do końca. Jeżeli nie znaleziono znaku ukośnika, znaczy że jest to jakieś zupełnie niestandardowe zapytanie, dlatego przerywamy działanie parsera. Dlaczego dwa razy nil, to się okaże za chwilę.

W następnym kroku obetniemy to co nam nie jest potrzebne:

e = string.find(request_handle, "HTTP")
if e == nil then return nil,nil end
request_handle = string.sub(request_handle, 0, (e-2))

Pierwsza linia odnajduje wystąpienie ciągu HTTP (pamiętacie, kończyło się linia napisem HTTP/1.1), jeżeli go nie ma przerywamy działanie. W ostatniej linii zapisujemy to co w żądaniu zostało przed „HTTP” (cofamy się o dwie pozycje, bo przed HTTP jest jeszcze spacja).

Założyliśmy sobie, że nasze żądania będą miały format NR/AKCJA, więc dzielimy to co zostało względem wystąpienia znaku ukośnika:

 e = string.find(request_handle,"/")
 if e == nil then return nil,nil end
 local id = string.sub(request_handle,0,e-1)
 local action =    string.sub(request_handle,e+1)

Jeśli znaku / nie ma, to przerwij działanie. By lepiej poczuć jak działa ten kod, po wgraniu całości kodu usuń komentarze przed komendami print w module url_pareser.lua. Wtedy w konsoli ESPlorera zobaczysz jak stopniowo jest 'rozbierany’ URL na kawałki.

Mamy już id oraz akcję, więc przyjrzyjmy się jak wygląda obsługa zdarzenia „receive” w naszym nowym serwerze:

 id,parsed_request = Parser.parse(payload)
 if parsed_request == 'on' then gpio.write(id, gpio.LOW) end
 if parsed_request == 'off' then gpio.write(id, gpio.HIGH) end

Parser to moduł zdefiniowany w url_parser.lua. Funkcję parse wyżej omówiliśmy. Powinna zwrócić dwie wartości: nr przekaźnika oraz akcję która ma wykonać. Jeśli nie potrafi odnaleźć danych (bo np to jest żądanie dostarczenia strony głównej, albo ma format inny od naszego założenia) to parser zwróci dwie wartości nil.W LUA można w ten sposób zwracać więcej niż jedną wartość i zapisać je w dwóch zmiennych oddzielonych przecinkiem (lub więcej niż dwie):

 var1, var2 = funkcja()

W razie powodzenia w zależności od akcji jest ustawiane odpowiednie GPIO. Teraz trzeba pamiętać, że stan niski steruje przekaźnikiem, więc gdy mamy komendę ’on’ to ustawiamy stan niski na GPIO. Dla wartości akcja równej nil żaden z ifów ustawiających GPIO 'nie odpali’.

No to teraz na koniec wyślemy coś do przeglądarki:

if id == nil then
 main_page(conn)
else
 http_ok(conn)
end

Nie mamy id? Czyli to chyba żądanie strony głównej – także proszę bardzo. A jeśli nie zrozumieliśmy co od nas chcą, to też id będzie nil i jeszcze raz – strona główna. Dla istniejącego id wyślemy tylko potwierdzenie że wszystko działa. Dobrze, także podłączamy GPIO4 i GPIO5 do przekaźników. Pozostaje przetestować.

Zabezpiecz taśmą spód modułu
Zabezpiecz taśmą spód modułu

Nim podłączymy przekaźniki i jakieś napięcie do nich – zwróć uwagę, że kładąc je na płytce stykowej ryzykujesz, że piny pod wysokim napięciem mogą zewrzeć się na płytce stykowej. Niby piny są za szerokie dla płytki stykowej, ale jak naciśniesz mocniej… Dlatego zabezpiecz spód modułu przekaźników np kilkoma warstwami taśmy izolacyjnej.

 

main_page oraz http_ok to funkcje wysyłające dane do przeglądarki. I tutaj ważna zmiana. Wcześniejsze tutoriale do nodeMCU pokazywały wysyłanie danych do przeglądarki przez wielokrotne wywołanie conn:send. W nowszych SDK takie działanie jest już niepoprawne, ESP się zawiesi wysyłając tak dane. Teraz całość musi być wysłana za jednym wywołaniem conn:send. Dlatego w kodzie używamy .. na początku linii do łączenia kolejnych linii w całość:

'pierwsza linia '
 ..'druga linia'

zostanie sklejone w

'pierwsza linia druga linia'.

main_page wysyła kod HTML (z nagłówkami) prostej strona korzystając z Bootstrap by stworzyć przyciski do włączania i wyłączania każdego z przekaźników. Do każdego przycisku jest przypisana funkcja wywołująca odpowiedni URL w stylu 1/off, itp.

Całość kodu do ściągnięcia: ESP8266 IoT – sterowanie przekaźnikiem Rozpakuj pliki, otwórz wszystkie w ESPlorerze, popraw dane dostępowe do WiFi i wgraj program na ESP. Teraz czekaj aż zaloguje się do sieci WiFi i przetestuj całość. Po wpisaniu adresu IP ESP w przeglądarkę powinieneś zobaczyć stronę z czterema przyciskami. Kliknięcie w nie powinno odpowiednio włączać i wyłączać przekaźniki. Przy włączeniu usłyszysz wyraźny klik oraz zapali się dioda DS1 lub DS2.

Same włączenia i wyłączenia możesz testować wpisując w przeglądarkę adres ESP (załóżmy 10.10.1.130) i włączyć pierwszy przekaźnik: http://10.10.1.130/1/on

Co można poprawić?

Po pierwsze – brakuje sprawdzania czy id ma wartość 1 lub 2 – np wpisanie w przeglądarkę adresu http:/10.10.1.130/2000/on spowoduje restart całego ESP z komunikatem:

PANIC: unprotected error in call to Lua API (server.lua:48: gpio 2000 does not exist)

Stało się tak, bo próbowaliśmy ustawić pin GPIO 2000 – nie ma takiego ;) a NodeMCU restartuje się w razie takich poważniejszych błędów.

Przenoszenie na płytkę

Jak szybko i prosto zrobić  z tego układu płytkę z lutowanymi połączeniami – czytaj w części trzeciej.