Skocz do zawartości

Przeszukaj forum

Pokazywanie wyników dla tagów 'iot'.

  • Szukaj wg tagów

    Wpisz tagi, oddzielając przecinkami.
  • Szukaj wg autora

Typ zawartości


Kategorie forum

  • Elektronika i programowanie
    • Elektronika
    • Arduino i ESP
    • Mikrokontrolery
    • Raspberry Pi
    • Inne komputery jednopłytkowe
    • Układy programowalne
    • Programowanie
    • Zasilanie
  • Artykuły, projekty, DIY
    • Artykuły redakcji (blog)
    • Artykuły użytkowników
    • Projekty - DIY
    • Projekty - DIY roboty
    • Projekty - DIY (mini)
    • Projekty - DIY (początkujący)
    • Projekty - DIY w budowie (worklogi)
    • Wiadomości
  • Pozostałe
    • Oprogramowanie CAD
    • Druk 3D
    • Napędy
    • Mechanika
    • Zawody/Konkursy/Wydarzenia
    • Sprzedam/Kupię/Zamienię/Praca
    • Inne
  • Ogólne
    • Ogłoszenia organizacyjne
    • Dyskusje o FORBOT.pl
    • Na luzie

Kategorie

  • Quizy o elektronice
  • Quizy do kursu elektroniki I
  • Quizy do kursu elektroniki II
  • Quizy do kursów Arduino
  • Quizy do kursu STM32L4
  • Quizy do pozostałych kursów

Szukaj wyników w...

Znajdź wyniki, które zawierają...


Data utworzenia

  • Rozpocznij

    Koniec


Ostatnia aktualizacja

  • Rozpocznij

    Koniec


Filtruj po ilości...

Data dołączenia

  • Rozpocznij

    Koniec


Grupa


Imię


Strona


TempX

Znaleziono 21 wyników

  1. Słowo wstępu Jeśli chodzi o tworzenie własnych PCB, zawsze (oprócz zapachu lutownicy) w powietrzu zawsze wisi trochę magii... Eksplozja kreatywności kończy się urządzeniem, które będzie służyć do rozwiązania konkretnego problemu i ułatwi życie. W naszym starym mieszkaniu miałem gniazdka elektryczne sterowane za pomocą radiowej częstotliwości 433MHz, kontrolowane przez ESP8266. Jednak teraz, gdy zbliżamy się do kolejnych kamieni milowych w naszej wspólnej drodze z moją wspaniałą żoną, w mojej głowie zrodziła się pewna idea i postawiłem sobie wyzwanie, aby zbudować własną Bramkę Zigbee. Chciałbym mieć pełną kontrolę nad urządzeniami, kontrolę nad przepływem między Zigbee a internetem oraz powiadomienia głosowe w jednym urządzeniu. Szczegóły techniczne Pierwszy krok, znany również jako MVP, polegał na stworzeniu urządzenia, które działa jako proxy Ser2Net (zdalny port szeregowy) i potrafi obsługiwać wbudowaną integrację ZHA w Home Assistant poprzez protokół EZSP. Urządzenie miało docelowo również obsługiwać powiadomienia dźwiękowe i posiadać wbudowany czujnik temperatury. Wszystkie kroki zakończyły się powodzeniem. 😇 Design funkcjonalny opiera się o następujące układy: ESP32S3 - główny procesor aplikacyjny ze wsparciem USB oraz WiFi. EFR32MG1 (w postaci modułu Ebyte E-180) - Zigbee Network Co-processor (NCP). MAX98357A - Wzmacniacz i kodek audio I2S. Ponadto na płytce zamontowane są: filtr wejściowy zasilania oparty na dławiku ferrytowym w konfiguracji LC, celem filtracji ewentualnych sygnałów RF. układ ograniczający prąd do 1A (~1.5A peak), dodatkowo działający jako soft-start (zapobiega tzw. inrush current, co w przypadku USB jest ważne). regulator napięcia 3.3V (low noise, ultra-low dropout). Software oparty jest o ksIotFrameworkLib a aplikacja składa się z kilku komponentów: AudioPlay - komponent odpowiedzialny za obsługę audio, w tym sterowanie częstotliwością CPU (dekodowanie wymaga 240MHz a bazowo jest 80MHz). Ser2Net - komponent pośredniczacy w komunikacji między HomeAssistant a procesorem sieci Zigbee. TempSensor - komponent odpowiedzialny za pomiary temperatury. Funkcjonalności takie jak zarządzanie połączeniem WiFi, komunikacja MQTT czy konfiguracja parametrów są dostarczane poprzez framework. Galeria multimediów Linki: Strona projektu na hackaday.io
  2. Interaktywna zabawka dla kotów gwarantująca zabawę w każdej chwili, żaden kot nie oprze się uciekającej czerwonej kropce. Jest to niewielkie pudełeczko z wbudowanym modułem wifi i banalnie prostą obsługą. Główne funkcje: sterowanie dowolnym urządzeniem z przeglądarką internetową. losowe ruchy lasera o zmiennej prędkości. ustawianie czasu jak długo ma działać. ustawianie harmonogramów automatycznego włączenia. regulacja jasności lasera. regulacja zakresu ruchu i prędkości lasera. możliwość sterowania z dowolnego miejsca na świecie przez internet. sterowanie za pomocą google asystenta. prosta konfiguracja. Zabawka może być zasilana dowolną ładowarką od telefonu, może to być również powerbank. Przy pierwszym uruchomieniu zabawki, zostanie uruchomiona nowa sieć wifi ..::LASERCAT::.. wystarczy połączyć się z nią i wskazać naszą sieć domową, a po zrestartowaniu urządzenie automatycznie podłączy się do niej i już możemy korzystać z zabawki. Z tyłu znajduje się wejście zasilania micro USB, jak w telefonie oraz przycisk. Krótkie wciśnięcie to włączenie/wyłączenie lasera, przytrzymanie przez 3 sek. powoduje rozłączenie obecnej sieci wifi i uruchomienie ponownej konfiguracji. Gdy urządzenie jest już podłączone do naszej sieci wifi to po wpisaniu adresu zabawki w przeglądarce internetowej zobaczymy panel sterujący: Zastosowany laser jest małej mocy, taki sam jak w innych tego typu zabawkach czy bazarkowych wskaźnikach. Dodatkowo dla bezpieczeństwa jest możliwość ustawienia mocy świecenia lasera od 0% do 100%. Pozostałe ustawienia pozwolą dostosować zakres ruchów do miejsca w którym znajduje się zabawka i określić czy kropka ma się poruszać tylko po podłodze, czy częściowo wchodzić na ścianę co może dostarczyć dodatkowej frajdy dla kota. Schemat jest bardzo prosty: Widok płytki PCB: Jak zwykle w garażowym zaciszu metodą "żelazkową" - elektronicy używają żelazka zdecydowanie częściej jak ich partnerki 😅 - powstaje mała płytka. Płytka została zabezpieczona przed utlenianiem lakierem PVB16. Całą robotę wykonuje tutaj tani i lubiany układ ESP8266, który posiada moduł WiFi. Dioda laserowa jest zasilana źródłem prądowym dodatkowo kluczowanym z PWM-a co pozwala płynnie regulować jasność od 0% do 100%. Skoro już bebechy mam, to teraz trzeba to wszystko złożyć w całość. Obudowę wykonałem ze sklejki wyciętej laserowo, składanej na wczepy palcowe. No to składamy: Dodanie serwomechanizmów do których przyczepiony jest laser. Oczywiście bez trytytki projekt by się nie udał 😏 No i sprzęt jest gotowy, ale co nam po sprzęcie jak on zupełnie nie wie co ma robić? Nie wie, że teraz trzeba machać tym laserkiem tak żeby kot ganiał w tę i we w tę 😉 Trzeba to wszystko zaprogramować. Uruchamiamy nasze ulubione IDE czyli Visual Studio Code z wtyczką PlatformIO i zaczynamy pisać program. Soft został napisany z wykorzystaniem Arduino Core, a na całość składa się kilka części: główny program sterujący silniczkami, wyznaczanie losowych ścieżek. serwer www, który udostępnia ładny panel sterowania. konfiguracja sieci WiFi z wykorzystaniem Captive Portal. multicast DNS. stworzenie strony www (html + css + javascript). obsługa komunikacji po websockecie. zdalne wgrywanie plików przez stronę www, np. zmiana wyglądu głównej strony. zdalna aktualizacja oprogramowania bez zbędnych kabli. W oczekiwaniu na gotowe oprogramowanie tester cierpliwie czeka. Panel sterujący dostępny z poziomu przeglądarki internetowej nie jest hostowany nigdzie na zewnątrz, całość znajduje się w zabawce, wykorzystałem bootstrapa plus kilka dodatkowych komponentów. Zastosowany mDNS pozwala połączyć się z urządzeniem wpisując w przeglądarce adres "lasercat.local" zamiast adresu IP. Niestety na chwilę obecną android nie wspiera tego typu rozwiązań, ale na iPhonach działa to bardzo dobrze. Na filmie mała prezentacja z trochę już wybawionym głównym testerem Elroyem 🙂 a poniżej pokazano jak można włączyć zabawkę po prostu mówiąc do telefonu "Ok Google, włącz laser"
  3. Ten artykuł jest częścią serii "Tworzenie interfejsu sieciowego z wykorzystaniem ESP" #1 - część 1 (właśnie to czytasz) #2 - część 2 ESP32 czy też ESP8266 na dobre już zagościło w wielu warsztatach domowych majsterkowiczów. Większość obecnych projektów z wykorzystaniem ESP skupia się wokół dorzucenia do niego garści czujników, podłączenia do baterii i wybudzania go od czasu do czasu, aby wysłać dane o wykonanych pomiarach do naszego serwera. Czasem zdarza się, że nasze urządzenie pobiera pewne dane z zewnątrz i je wykorzystuje, np. budzik czas z serwera NTP, czy stacja pogodowa, informacje o pogodzie z wybranego serwisu. Co w sytuacji kiedy chcemy kontrolować nasze urządzenie lub obserwować jego stan z poziomu przeglądarki, a nie posiadamy Raspberry Pi, czy innej opcji, na której moglibyśmy mieć własny serwer? Co jeżeli zastosowanie dodatkowego serwera jest po prostu nieadekwatne do naszego celu? W tym artykule postaram się: omówić najpopularniejsze rozwiązania pokazać jak uruchomić serwer www ESP32 stworzyć prostą stronę www do naszych zadań wykonać interakcje strona-ESP w postaci: kontroli portu GPIO wyświetlanie wyniku pomiaru z ADC pobieranie pliku z pamięci ESP/karty SD Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » Wszystkie powyższe rzeczy postaram się zobrazować w jak najprostszy i przejrzysty sposób. Poruszany temat jest niewątpliwie bardzo złożony i niestety nie jest możliwe aby wszystkie informacje zawrzeć w jednym artykule. Temat wymaga zarówno znajomość obsługi samego ESP, HTML, JavaScriptu czy też CSS, zaś znajomość protokołów sieciowych również byłaby mile widziana. Tutaj będą jedynie ukazane podstawy jak to wszystko ze sobą połączyć. Pokazane metody z pewnością nie będą należeć do najbardziej optymalnych rozwiązań, mają jedynie na celu ukazanie koncepcji i zachęcenia do dalszej analizy tego zagadnienia. Wszystkie kody będą skomentowane. W treści będę również odsyłał do dodatkowych materiałów, które dokładniej opisują poszczególne zagadnienia oraz tam gdzie można zdobyć więcej wartościowych informacji. Ale w jakim celu? Część z osób może zadać pytanie po co uruchamiać serwer na ESP, wiąże się to z dużym poborem energii, pomiary najlepiej z wielu czujników wysyłać w jedno miejsce, to dużo pracy itd. Inni zaś, od razu stwierdzą, że to jest to czego oni potrzebują. Jako że nie widzę większego sensu pisania długich wywodów na temat dlaczego warto, dlaczego nie, kiedy tak, kiedy nie. Przedstawię poniżej dwa praktyczne przykłady i możliwości takich realizacji które pozwolą samemu ocenić te aspekty. Pierwszym przykładem jest zdalny interfejs drukarki 3D. Dzięki niemu możemy zdalnie uruchomić drukarkę, wysyłać do niej pliki, uruchamiać druk, obserwować parametry druku, dostosowywać je, konfigurować drukarkę i wiele innych. Zostało to zrealizowane na ESP8266 i projekt jest dostępny pod tymi linkami Duet WiFi Server oraz Duet Web Control Drugi przykład jest to interfejs do sterowania lampką/oświetleniem LED. Z poziomu przeglądarki możemy ustawiać różne efekty świetlne, barwę, jasność, konfigurować urządzenie. Więcej o tym projekcie można dowiedzieć się tutaj Aircookie WLED Co będzie nam potrzebne? Podstawowa znajomość platformy ESP oraz programowania w Arduino w tym obsługa SPIFFS lub kart SD Płytka z ESP32 (wszystko powinno być kompatybilne z ESP8266) Zainstalowana biblioteka Async Web Server Dodatkowo: Znajomość języka angielskiego – dodatkowe odnośniki Płytka stykowa, potencjometr, fotorezystor czy cokolwiek sobie wymyślicie aby urozmaicić sobie temat 🙂 Zrozumienie tematu również ułatwi znajomość podstaw HTML oraz JavaScriptu. Jako że wymagane są już podstawowe umiejętności odnośnie obsługi ESP oraz Arduino, pominę kwestie instalacji biblioteki, omówienia zagadnień struktury programu czy też obsługi peryferiów. Z czym to się je? Podstawowa koncepcja naszego projektu opiera się na tym, iż na ESP uruchamiamy serwer, który na zapytanie klienta (klient czyli nas - naszej przeglądarki) zwraca odpowiednie pliki lub wykonuje pewne operacje. W ten sposób możemy poprosić ESP aby zwrócił nam plik HTML zawierający naszą stronę, przeglądarka ją odbierze, a my będziemy się mogli cieszyć widokiem naszej witryny. W ten sposób możemy wyróżnić pierwszy ze sposobów interakcji z naszym ESP, czyli z wykorzystaniem metod HTTP. W uproszczeniu, metody są to pewnego rodzaju „komunikaty” czego oczekujemy od naszego serwera. Przykładowo, wysyłamy zapytanie „GET” – oznacza że chcemy coś od serwera i ma on nam to dać, zapytanie „POST” – oznacza że chcemy coś dać od siebie. Każde nasze zapytanie będzie skutkować odpowiedzią (lub jej brakiem 🙂 ). Odpowiedzi posiadają swoje kody, które mają różne znaczenie – to daje nam dodatkowe możliwości interakcji. Wiedząc co oznacza dany kod możemy przykładowo stwierdzać czy dostaliśmy odpowiedź, czy wyświetlić jakiś błąd, lub stwierdzić że coś nie istnieje (każdemu znane 404). Najprostszym użyciem tych zapytań jest po prostu wykorzystanie odpowiednich struktur w HTML z stosownymi atrybutami. Metodę „POST” możemy wykorzystać przy tworzeniu formularza. Wadą tego rozwiązania jest fakt tego iż będzie to skutkować przeładowaniem strony przy każdej tego typu akcji. Inną opcją jest wykorzystanie pomocy Java Scriptu który będzie służył jako nasza „trzecia ręka” wykonująca te operacje w tle. Rozwiązanie to nazywa się AJAX (z angielskiego Asynchronus JavaScript and XML) i na nim się głównie skupimy w tym artykule. Drugą powszechną opcją jest skorzystanie z WebSocket. Jest to sposób ciągłej komunikacji między klientem a serwerem. Polega ona na nawiązaniu „kontaktu” z serwerem i zapytaniem go czy jest chętny na „pogawędkę”. Metoda ta idealnie się nadaje do wymiany ciągów informacji na żywo. Przykładowo potrzebujemy ciągłego odczytu z przetwornika ADC – można stwierdzić „wirtualny port szeregowy”. Oczywiście moglibyśmy zrealizować to samo zadanie z wykorzystaniem wcześniej wspomnianych metod, ale wykorzystanie metody HTTP wiąże się z całym procesem, wysłania zapytania, otrzymania odpowiedzi, co w skali procesora trwa wieki (np. jedno zapytanie kilkadziesiąt – set ms). Tutaj nie mamy tego problemu, gdyż nasze połączenie ciągle trwa i sobie rozmawiamy. W przypadku gdy nie zależy nam na ciągłym podglądzie (np. odświeżanie informacji raz na pół minuty) możemy spokojnie zadowolić się wykorzystaniem AJAX i metod HTTP. Ponadto warto nadmienić iż korzystanie z WebSocketów jest zarówno korzystne dla serwera jak i klienta ze względu na minimalną ilość przesyłanych danych (ograniczenie tego co jest nam w rzeczywistości zbędne). No to zaczynamy! Na wstępie warto nadmienić że pracujemy wewnątrz sieci lokalnej. Jeżeli połączymy się z naszym WiFi, inne urządzenia z tej samej sieci będą miały dostęp do naszego serwera. Bez stosownej konfiguracji sieci (jak i czasem ograniczeń narzuconych przez naszego dostawcę internetowego) nie będziemy mieć dostępu do naszego urządzenia z dowolnego miejsca na świecie. Na początek zacznijmy od tego czym jest nasza biblioteka i dlaczego ona. Otóż umożliwia ona komunikację asynchroniczną, co pozwala nam na posiadanie więcej niż jednego połączenia w danej chwili i działa poza pętlą loop(). Aby się nie rozpisywać na temat innych zalet i możliwości zainteresowanych dogłębną analizą odeślę tutaj. Uwaga dla użytkowników ESP8266! Biblioteka od obsługi WiFi definiuje się jako: #include <ESP8266WiFi.h> Zaś obsługa SPIFFS: #include <FS.h> Ponadto w poniższej pętli while() potrzebne jest opóźnienie, aby zapobiec uruchamianiu się watchdoga while (WiFi.status() != WL_CONNECTED){ delay(1000); } Powyższe uwagi będą zawarte w komentarzach kodów. Uruchamiamy serwer! #include <Arduino.h> #include <WiFi.h> //ESP8266 //#include <ESP8266WiFi.h> #include <SPIFFS.h> //ESP8266 //#include <FS.h> #include <ESPAsyncWebServer.h> #define SSID "nazwa sieci" #define PASS "hasło sieci" AsyncWebServer serwer(80); //utwórzmy obiekt serwera na porcie 80 void setup() { Serial.begin(115200); //zainicjujmy port szeregowy WiFi.begin(SSID, PASS); //połącz z naszą siecią wifi while (WiFi.status() != WL_CONNECTED){ //poczekajmy aż ESP połączy się z naszą seicią //delay(1000); //dla ESP8266 } Serial.printf("\nAdres IP:"); Serial.println(WiFi.localIP()); //wypisz adres IP naszego ESP przez port szeregowy //tutaj odbywa sie obsługa zapytań serwer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/" typu GET, request->send_P(200, "text/plain", "Witaj! :)"); //odpowiedz mu kodem 200, danymi tekstowymi, o treści "Witaj! :)" }); serwer.begin(); //zainicjujmy nasz serwer } void loop() { } W powyższym kodzie widzimy następujące etapy, łączymy się z naszą siecią WiFi, ESP zwraca nam przez port szeregowy swój adres IP w naszej sieci. Będzie on nam potrzebny do wpisania w pasku przeglądarki w celu połączenia się z serwerem. Następnie tworzymy funkcję która obsługuje konkretne zapytania, w naszym przypadku po otrzymaniu zapytania GET pod adresem „/” – można to określić jako „folder główny” serwera, tak samo jak w komputerze mamy dysk np. „D:\” – odeśle klientowi odpowiedź o kodzie 200 (oznacza to „ok” – więcej o kodach tutaj) i zawartości typu tekstowej (są to typy MIME, mówią one przeglądarce co jej chcemy przekazać – więcej o typach MIME tutaj). Rezultatem, po wpisaniu w pasek przeglądarki adresu IP naszego ESP, jest strona. Tworzymy prostą stronę Jako że celem tutaj nie jest nauka HTML czy też CSS, ograniczyłem stronę do absolutnego minimum, potrzebnego do naszych zabaw. Tutaj też, odeślę do wartościowego źródła gdzie można znaleźć wiele wartościowych informacji odnośnie HTML, JavaScript, CSS oraz innych. Nasza strona będzie się składać z pola tekstowego gdzie wyświetlimy wartość odczytaną z ADC, dwóch przycisków do włączania i wyłączania diody oraz przycisku pobierania pliku z naszego ESP. <!DOCTYPE html> <html> <head> <title>Strona</title> <meta charset="UTF-8"/> </head> <body> <p id="pomiar">Wartość:</p> <button id="on">Włącz</button> <button id="off">Wyłącz</button><br> <button id="download">Pobierz obrazek</button> <script> </script> </body> </html> Kluczowe podczas tworzenia takiej strony jest nadawanie unikalnego ID każdemu elementowi, ułatwi to współpracę z JavaScriptem. Gdy już mamy przygotowaną stronę musimy ją wgrać do SPIFFS. Stąd będziemy wysyłać plik HTML jako odpowiedź dla klienta. Analogicznie można te pliki wgrać na kartę pamięci i z delikatną modyfikacją kodu serwować z niej pliki. #include <Arduino.h> #include <WiFi.h> //ESP8266 //#include <ESP8266WiFi.h> #include <SPIFFS.h> //ESP8266 //#include <FS.h> #include <SPIFFS.h> #include <ESPAsyncWebServer.h> #define SSID "nazwa sieci" #define PASS "hasło sieci" AsyncWebServer serwer(80); //utwórzmy obiekt serwera na porcie 80 void setup() { Serial.begin(115200); //zainicjujmy port szeregowy SPIFFS.begin(); //zainicjujmy system plików WiFi.begin(SSID, PASS); //połącz z naszą siecią wifi while (WiFi.status() != WL_CONNECTED){ //poczekajmy aż ESP połączy się z naszą seicią //delay(1000); //dla ESP8266 } Serial.printf("\nAdres IP:"); Serial.println(WiFi.localIP()); //wypisz adres IP naszego ESP przez port szeregowy //tutaj odbywa sie obsługa zapytań serwer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytania pod adresem "/" typu GET, request->send(SPIFFS, "/index.html", "text/html"); //odpowiedz plikiem index.html z SPIFFS (można to zmienić na kartę SD) //zawierającym naszą stronę będącą plikem tekstowym HTML }); serwer.begin(); //zainicjujmy nasz serwer } void loop() { } Teraz po wpisaniu adresu IP naszej strony w pasek przeglądarki ukaże się nam prosta strona. Pora na działanie! Na pierwszy ogień weźmiemy obsługę LED. W tym celu konieczne będzie dorzucenie trochę JavaScriptu do naszej strony document.getElementById("on").onclick = function () { //po nacisinięciu elementu o ID "on" const zapytanie = new XMLHttpRequest(); //wyślijmy zapytanie GET, pod adresem /on zapytanie.open("GET", "/on"); zapytanie.send(); }; document.getElementById("off").onclick = function () { //po nacisinięciu elementu o ID "off" const zapytanie = new XMLHttpRequest(); //wyślijmy zapytanie GET, pod adresem /off zapytanie.open("GET", "/off"); zapytanie.send(); }; Kod ten sprawdza czy któryś z przycisków został naciśnięty, a jeżeli został wysyła stosowne zapytanie do naszego serwera. Finalnie kod strony przedstawia się jak poniżej. <!DOCTYPE html> <html> <head> <title>Strona</title> <meta charset="UTF-8"/> </head> <body> <p id="pomiar">Wartość:</p> <button id="on">Włącz</button> <button id="off">Wyłącz</button><br> <button id="download">Pobierz obrazek</button> <script> document.getElementById("on").onclick = function () { //po nacisinięciu elementu o ID "on" const zapytanie = new XMLHttpRequest(); //wyślijmy zapytanie GET, pod adresem /on zapytanie.open("GET", "/on"); zapytanie.send(); }; document.getElementById("off").onclick = function () { //po nacisinięciu elementu o ID "off" const zapytanie = new XMLHttpRequest(); //wyślijmy zapytanie GET, pod adresem /off zapytanie.open("GET", "/off"); zapytanie.send(); }; </script> </body> </html> Ponadto w sekcji setup() naszego kodu ESP musimy dodać obsługę nowo powstałych zapytań. serwer.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/on" typu GET, digitalWrite(LED, LOW); //zapal diodę request->send(200); //odeślij odpowiedź z kodem 200 OK }); serwer.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, digitalWrite(LED, HIGH); //zgaś diodę request->send(200); //odeślij odpowiedź z kodem 200 OK }); Co daje nam w rezultacie kod jak poniżej. Ważne aby wszystkie zapytania były przed funkcją serwer.begin() #include <Arduino.h> #include <WiFi.h> //ESP8266 //#include <ESP8266WiFi.h> #include <SPIFFS.h> //ESP8266 //#include <FS.h> #include <SPIFFS.h> #include <ESPAsyncWebServer.h> #define SSID "nazwa sieci" //nazwa sieci #define PASS "haslo sieci" //hasło sieci #define LED 22 //numer pinu gdzie mamy podłączoną diodę AsyncWebServer serwer(80); //utwórzmy obiekt serwera na porcie 80 void setup() { Serial.begin(115200); //zainicjujmy port szeregowy SPIFFS.begin(); //zainicjujmy system plików pinMode(LED, OUTPUT); //ustawmy naszeg pin jako wyjście WiFi.begin(SSID, PASS); //połącz z naszą siecią wifi while (WiFi.status() != WL_CONNECTED){ //poczekajmy aż ESP połączy się z naszą seicią //delay(1000); //dla ESP8266 } Serial.printf("\nAdres IP:"); Serial.println(WiFi.localIP()); //wypisz adres IP naszego ESP przez port szeregowy //tutaj odbywa sie obsługa zapytań serwer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytania pod adresem "/" typu GET, request->send(SPIFFS, "/index.html", "text/html"); //odpowiedz plikiem index.html z SPIFFS (można to zmienić na kartę SD) //zawierającym naszą stronę będącą plikem tekstowym HTML }); serwer.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/on" typu GET, digitalWrite(LED, LOW); //zapal diodę request->send(200); //odeślij odpowiedź z kodem 200 OK }); serwer.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, digitalWrite(LED, HIGH); //zgaś diodę request->send(200); //odeślij odpowiedź z kodem 200 OK }); serwer.begin(); //zainicjujmy nasz serwer } void loop() { } Teraz możemy zaobserwować działanie naszego kodu. Odczyt ADC Teraz pora na odczyt wartości z przetwornika analogowo-cyfrowego. Tym razem nasz skrypt będzie automatycznie, z pewnym interwałem czasowym (500ms), wysyłał zapytanie do serwera. setInterval(function () { const zapytanie = new XMLHttpRequest(); zapytanie.open("GET", "/adc"); zapytanie.send(); zapytanie.onreadystatechange = function () { if (this.readyState == 4 && this.status == 200) { document.getElementById("pomiar").innerHTML = "Pomiar:" + this.responseText; } }; }, 500); Powyższy fragment powinien znaleźć się w pliku .html w sekcji <script>, tak jak poprzednio. Serwer w odpowiedzi będzie zwracał wartość z ADC w postaci tekstu, zaś JavaScript, w tle będzie nam podmieniał wartości na stronie uzyskane w odpowiedzi od serwera, bez konieczności przeładowania. W kodzie ESP wystarczy że dodamy taki fragment kodu do sekcji setup() przed funkcją serwer.begin(). serwer.on("/adc", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, String wartosc = String(analogRead(ADC)); //wykonaj pomiar ADC i zapisz do Stringa request->send(200, "text/plain", wartosc); //odeślij odpowiedź z kodem 200 OK i odczytem z wartością }); Na powyższej animacji widać jak zmieniają się wartości. W konsoli przeglądarki (przycisk F12 powinien nam ją uruchomić w większości przeglądarek) można obserwować wszystkie zapytania wymieniane między klientem a serwerem. Jest to bardzo przydatne narzędzie do „debugowania” kiedy coś nie chce do końca z nami współpracować. Powyższe zadania możemy zrealizować również w inny sposób, poprzez wywołanie naszej funkcji z poziomu funkcji obsługi zapytań. Przykład obsługi ADC przedstawiałby się w następujący sposób. #include <Arduino.h> #include <WiFi.h> //ESP8266 //#include <ESP8266WiFi.h> #include <SPIFFS.h> //ESP8266 //#include <FS.h> #include <SPIFFS.h> #include <ESPAsyncWebServer.h> #define SSID "nazwa sieci" //nazwa sieci #define PASS "hasło sieci" //hasło sieci #define ADC 34 //numer pinu potencjometru AsyncWebServer serwer(80); //utwórzmy obiekt serwera na porcie 80 String odczyt_ADC() { return String(analogRead(ADC)); } void setup() { Serial.begin(115200); //zainicjujmy port szeregowy SPIFFS.begin(); //zainicjujmy system plików pinMode(LED, OUTPUT); //ustawmy naszeg pin jako wyjście WiFi.begin(SSID, PASS); //połącz z naszą siecią wifi while (WiFi.status() != WL_CONNECTED){ //poczekajmy aż ESP połączy się z naszą seicią //delay(1000); //dla ESP8266 } Serial.printf("\nAdres IP:"); Serial.println(WiFi.localIP()); //wypisz adres IP naszego ESP przez port szeregowy //tutaj odbywa sie obsługa zapytań serwer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytania pod adresem "/" typu GET, request->send(SPIFFS, "/index.html", "text/html"); //odpowiedz plikiem index.html z SPIFFS (można to zmienić na kartę SD) //zawierającym naszą stronę będącą plikem tekstowym HTML }); serwer.on("/adc", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, request->send(200, "text/plain", odczyt_ADC()); //odeślij odpowiedź z kodem 200 OK i odczytem z wartością }); serwer.begin(); //zainicjujmy nasz serwer } void loop() { } Pobieranie pliku Na koniec zajmiemy się pobieraniem pliku z naszego serwera. W celu pokazania jak korzystać z typów MIME przedstawię jak pobrać obrazek z naszego prostego serwera. Do naszej ESP pamięci wgramy poniższy obrazek. W tym celu musimy dodać fragment skryptu do naszej strony. document.getElementById("download").onclick = function () { //po nacisinięciu elementu o ID "download" location.href = "/download"; //przekieruj pod /download }; Podobnie jak uprzednio dodajemy go do naszej sekcji <script></script>. Działa on podobnie jak poprzednie włączanie i wyłączanie diody, lecz w normalnej sytuacji, takie działanie spowodowałoby przekierowanie pod ten adres /download. Ponieważ w kodzie programu ustawimy atrybut pobierania. Będzie to skutkowało wyskoczeniem okna pobierania. serwer.on("/download", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, request->send(SPIFFS, "/Lenna.png", "image/png", true); //odeślij odpowiedź w postaci pliku png o nazwie obrazek.png z SPIFFS i umożliwij pobranie (true) }); Jak widzimy musimy wskazać skąd nasz plik ma zostać pobrany (SPIFFS, może to być również karta SD), następnie wskazujemy dokładną lokalizację naszego pliku, jego rodzaj (MIME) oraz ustawiamy atrybut pobierania jako true. W efekcie uzyskujemy pobieranie naszego pliku. Zachęcam do sprawdzenia rezultatu po zmienieniu atrybutu pobierania na false. Poniżej zamieszam finalne wersje programu Arduino oraz kodu strony HTML. #include <Arduino.h> #include <WiFi.h> //ESP8266 //#include <ESP8266WiFi.h> #include <SPIFFS.h> //ESP8266 //#include <FS.h> #include <SPIFFS.h> #include <ESPAsyncWebServer.h> #define SSID "nazwa sieci" //nazwa sieci #define PASS "hasło sieci" //hasło sieci #define LED 22 //numer pinu gdzie mamy podłączoną diodę #define ADC 34 //numer pinu potencjometru AsyncWebServer serwer(80); //utwórzmy obiekt serwera na porcie 80 void setup() { Serial.begin(115200); //zainicjujmy port szeregowy SPIFFS.begin(); //zainicjujmy system plików pinMode(LED, OUTPUT); //ustawmy naszeg pin jako wyjście WiFi.begin(SSID, PASS); //połącz z naszą siecią wifi while (WiFi.status() != WL_CONNECTED){ //poczekajmy aż ESP połączy się z naszą seicią //delay(1000); //dla ESP8266 } Serial.printf("\nAdres IP:"); Serial.println(WiFi.localIP()); //wypisz adres IP naszego ESP przez port szeregowy //tutaj odbywa sie obsługa zapytań serwer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytania pod adresem "/" typu GET, request->send(SPIFFS, "/index.html", "text/html"); //odpowiedz plikiem index.html z SPIFFS (można to zmienić na kartę SD) //zawierającym naszą stronę będącą plikem tekstowym HTML }); serwer.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/on" typu GET, digitalWrite(LED, LOW); //zapal diodę request->send(200); //odeślij odpowiedź z kodem 200 OK }); serwer.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, digitalWrite(LED, HIGH); //zgaś diodę request->send(200); //odeślij odpowiedź z kodem 200 OK }); serwer.on("/adc", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, String wartosc = String(analogRead(ADC)); //wykonaj pomiar ADC i zapisz do Stringa request->send(200, "text/plain", wartosc); //odeślij odpowiedź z kodem 200 OK i odczytem z wartością }); serwer.on("/download", HTTP_GET, [](AsyncWebServerRequest *request){ //na otrzymane od klienta zapytanie pod adresem "/off" typu GET, request->send(SPIFFS, "/Lenna.png", "image/png", false); //odeślij odpowiedź w postaci pliku png o nazwie obrazek.png z SPIFFS i umożliwij pobranie (true) }); serwer.begin(); //zainicjujmy nasz serwer } void loop() { } <!DOCTYPE html> <html> <head> <title>Strona</title> <meta charset="UTF-8" /> </head> <body> <p id="pomiar">Wartość:</p> <button id="on">Włącz</button> <button id="off">Wyłącz</button><br> <button id="download">Pobierz obrazek</button> <script> document.getElementById("on").onclick = function () { //po nacisinięciu elementu o ID "on" const zapytanie = new XMLHttpRequest(); //wyślijmy zapytanie GET, pod adresem /on zapytanie.open("GET", "/on"); zapytanie.send(); }; document.getElementById("off").onclick = function () { //po nacisinięciu elementu o ID "on" const zapytanie = new XMLHttpRequest(); //wyślijmy zapytanie GET, pod adresem /off zapytanie.open("GET", "/off"); zapytanie.send(); }; setInterval(function () { const zapytanie = new XMLHttpRequest(); //wyślijmy zapytanie jak poprzednio zapytanie.open("GET", "/adc"); zapytanie.send(); zapytanie.onreadystatechange = function () { if (this.readyState == 4 && this.status == 200) { document.getElementById("pomiar").innerHTML = "Wartość:" + this.responseText; } }; }, 500); document.getElementById("download").onclick = function () { //po nacisinięciu elementu o ID "download" location.href = "/download"; }; </script> </body> </html> Podsumowanie Bardzo się cieszę że dotrwałeś do tego momentu! Jak wspomniałem na początku, przedstawione rozwiązania są najprostszymi, niekoniecznie zgodnymi ze sztuką rozwiązaniami. Starałem się w kodach programów ograniczyć wszystkie zbędne fragmenty i uprościć do absolutnego minimum – czego często brakuje w poradnikach z internetu, co skutkuje utrudnioną analizą działania programu. Pokazane sposoby mają na celu jedynie wprowadzenie do koncepcji tematu, zachęcenia do pracy oraz poznawania możliwości rozwiązań sieciowych, o których można by było pisać całe książki. Zarówno komunikacja z wykorzystaniem Websocketów czy tworzenie samej strony którą widzi klient – czyli strony internetowej – mogłaby zająć czas na oddzielne artykuły. W drugiej części artykułu omówię w teoretyczny sposób (bez gotowych rozwiązań programowych) jak z wykorzystaniem ESP oraz dostępnych technologii i bibliotek rozwiać takie problemy jak: konfigurowanie urządzenia z poziomu przeglądarki przeglądanie i zarządzanie plikami w pamięci ESP provisioning i co to oraz po co to właściwie jest M. S.
  4. Wstęp Jesteś na wakacjach albo posiadasz dwa mieszkania i bardzo chcesz podejrzeć wykresy z grafany, sprawdzić temperaturę w mieszkaniu czy zdalnie sterować światłem przez domoticz. Napotykasz pewien problem. Twoje urządzenia działają w twojej sieci domowej, nie masz możliwości bezpośredniego połączenia się z internetu. Możemy rozwiązać ten problem, odpowiednio konfigurując swoje urządzenia sieciowe, jednak to rozwiązanie ma wiele wad. Wystawienie swoich urządzeń IoT bezpośrednio do internetu może narazić je na ataki internetowe. W dzisiejszych czasach nie jest to niczym dziwnym. Przekonał się o tym chyba każdy kto udostępnił usługę SSH, a już po paru minutach mógł zobaczyć setki prób logowania z egzotycznych adresów IP w swoim /var/log/auth.log. Istnieje wiele metod zabezpieczeń tak wystawionych usług od stosowania trudnych haseł po wysyłanie serii pakietów na odpowiednie kombinacje portów sieciowych w celu autoryzacji (Port knocking) Wirtualna sieć prywatna Otwieranie portów do internetu wydaje się złym pomysłem i tutaj z pomocą przychodzą nam wirtualne sieci prywatne (VPN). Możliwe, że kojarzysz VPN z usługami anonimizujących twoje działania w internecie. Jest to jedna z korzyści, które może posiadać sieć VPN, ale to nie na tej funkcji skupimy się w tym artykule. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » Sieci VPN umożliwiają połączenie naszych urządzeń w jedną sieć, niezależnie od tego, gdzie aktualnie znajduje się dane urządzenie. Byle by miało połączenie do głównego serwera. Rozwiązanie to doskonale sprawdza się od wielu lat w sieciach firmowych, gdzie pracownicy są w stanie bezpiecznie podłączyć się do firmowego serwera bezpośrednio ze swojego domu. Rozwiązanie to jednak jest trudne w konfiguracji i wymaga serwera centralnego z dobrym połączeniem sieciowym, który musi być dostępny z internetu. Więc czy istnieje darmowe rozwiązanie, które będzie zawierać wszystkie zalety sieci VPN, nie będzie wymagać dodatkowego serwera oraz jednocześnie będzie omijać wszystkie przeszkody w postaci braku publicznego adresu ip? Istnieje! VPN peer to peer. Tak p2p, technologia znana zapewne użytkownikom protokołu BitTorrent jednak tutaj wykorzystujemy go do zestawiania połączeń sieciowych. ZeroTier Jednym z tego typu usług jest otwartoźródłowa aplikacja na licencji BSL o nazwie ZeroTier. Umożliwia ona tworzenie zdecentralizowanych wirtualnych sieci, czyli takich, które nie wykorzystują żadnego serwera centralnego. Dzięki zastosowaniu szyfrowanego połączenia bezpośrednio między naszymi urządzeniami, omijamy potrzebę wymiany naszych pakietów przez serwer pośredniczący, dzięki czemu ten nie ogranicza nas swoją przepustowością sieci. Połączenie takie nie tylko jest bezpieczne, ale też zapewnia niskie opóźnienia, co może mieć kluczowe znaczenie, jeśli chcemy wykorzystać to rozwiązanie do zastosowań rozrywkowych jak granie w gry czy udostępnianie pulpitu zdalnego. Rejestracja Na początku musimy zarejestrować się w usłudze ZeroTier. Przechodzimy na adres Centrali ZeroTier oraz wypełniamy formularz kryjący się pod przyciskiem Register. Alternatywą jest zalogowanie się, korzystając z kont usług Google, Github lub Microsoft. Tworzenie nowej sieci Po zalogowaniu się do Centrali ZeroTier w zakładce Network klikamy na przycisk z napisem Create A Network. Natychmiastowo na liście sieci pojawi się nam nasza nowo utworzona sieć. Wyróżnić możemy Identyfikator składający się z liter i cyfr, losowa nazwa, aktualna podsieć i ilość urządzeń online i offline. Przechodzimy do konfiguracji klikając w ID naszej sieci na liście. W zakładce Settings -> Basic możemy ustawić nazwę (Name) oraz opis (Description) dla naszej sieci. Jest to zabieg mający na celu łatwiejszą identyfikację na liście sieci, jeśli byśmy posiadali ich więcej. Ustawienie Access Control ustawiamy na PRIVATE, dzięki czemu każdy nowy klient w naszej sieci będzie musiał zostać zaakceptowany ręcznie. Zapobiegnie to niechcianym intruzom. Zakładka Advanced zawiera ustawienia dotyczące adresacji naszej sieci. Pewnie zaawansowani administratorzy poczują się tutaj jak ryba w wodzie. Opcji jest wiele, możemy zarządzać wirtualnym routerem, dowolnie przypisywać prywatne adresy ip lub zdecydować się na statyczne adresowanie. Dla mniej zaawansowanym użytkowników zalecam tutaj ustawić tylko zakres adresów IPv4 Auto-Assign na taki, który nie koliduje z naszymi lokalnymi sieciami domowymi. Na potrzeby tego artykułu wybrałem 10.144.*.* Zakładka Members zawiera listę aktualnie podłączonych użytkowników. Lista na razie jest pusta. Wszystkie zmiany są zapisywane na bieżąco. Dodawanie urządzeń do sieci Na potrzeby tego artykułu zestawimy sieć prywatną między dwoma urządzeniami. Raspberry Pi 2 Komputer z systemem Windows 10 ZeroTier można również z powodzeniem stosować na urządzeniach mobilnych z systemem Android lub iOS. Wszystkie możliwe opcje znajdziemy na stronie producenta. Linux (Raspberry Pi) Instalacja ZeroTier na urządzeniach z systemem Linux jest bardzo prosta. Ogranicza się ona do wykonania jednej komendy. Procedura może się różnić w zależności od dystrybucji systemu. Na początku musimy się zalogować do swojego urządzenia i uruchomić terminal. Warto upewnić się, że nas system jest aktualny. W tym celu wpisujemy w konsoli: sudo apt update; sudo apt full-upgrade; sudo apt autoremove; Następnie upewniamy się, że posiadamy w systemie narzędzie Curl poleceniem sudo apt install curl ZeroTier instalujemy poleceniem curl -s https://install.zerotier.com | sudo bash Po poprawnie wykonanej instalacji zobaczymy komunikat *** Success! You are ZeroTier address [ f2a983da64 ]. Interakcja z aplikacją odbywa się poprzez polecenie zerotier-cli Aby uzyskać informacje o dostępnych opcjach wpisujemy zerotier-cli --help Dołączamy do naszej sieci poprzez polecenie join oraz ID naszej sieci. sudo zerotier-cli join 565799d8f6699d5b Windows W systemie operacyjnym Windows w celu instalacji udajemy się na stronę producenta. Pobieramy instalator, klikając w ikonkę naszego systemu. Instalator następnie uruchamiamy i przeprowadzamy instalację. W czasie instalacji możemy zostać zapytani o dodanie wyjątku do zapory sieciowej, zgadamy się. Po instalacji program jest dostępny na pasku zadań, interakcje z nim przeprowadzamy poprzez przyciśnięcie prawego klawisza myszy na jego ikonie. Aby dołączyć do naszej sieci wybieramy opcję Join Network… Następnie podajemy ID naszej sieci z Centrali ZeroTier, w moim przypadku jest to 565799d8f6699d5b. Zatwierdzamy przyciskiem Join. Autoryzacja Ostatnim krokiem jest autoryzacja nowo dodanych urządzeń w panelu zarządzania naszą siecią. W tym celu w zakładce Members zaznaczamy pozycję Auth? Dla tych urządzeń, którym chcemy zezwolić na dostęp do sieci. Odczytujemy również jaki adres IP został przyznany z puli. Po tej czynności pozostało nam już tylko przetestować połączenie. W tym celu wykonujemy polecenie ping w konsoli systemu Windows z docelowym IP naszego Raspberry Pi w sieci zerotier ping 10.144.214.244 W systemach linux sprawdzamy, czy jest widoczna wirtualna karta sieciowa za pomocą polecenia sudo ifconfig Jeśli karta nie jest widoczna, spróbuj zresetować urządzenie. Podsumowanie Tym oto sposobem stworzyliśmy darmową bezpieczną sieć wirtualną dla naszych urządzeń IoT. Instalując aplikację na telefonie, jesteś w stanie kontrolować swoje urządzenia domowe, tak samo jakbyś był podłączony do swojej lokalnej sieci wifi. A jeśli będziesz kiedyś chciał pograć z kolegą “po lanie” to masz już alternatywę dla Hamachi. 😉
  5. Hej wszystkim! Jako, że posiadam własnoręcznie zbudowanego campera i chciałbym w najbliższym czasie pokusić się o stworzenie systemu IoT, który pozwalałby mi na sterowanie bezprzewodowo, również przez internet, różnymi jego podzespołami. Jak to zwykle bywa przy dużych projektach - czym więcej się czyta, tym mniej się wie, więc postanowiłem, że spróbuję napisać do Was, być może ktoś mnie nakieruje co muszę wiedzieć 🙂 Ogólny zarys jest taki - system musi mieć serce (tudzież mózg ;)) - tutaj natknąłem się na takie platformy jak arduino, raspberry pi, czy ESP32. Nie chcę, żeby całość pobierała więcej energii niż musi, a podzespoły były jak najtańsze i dawały najwięcej możliwości rozbudowy/modyfikacji. Odpadają więc dosyć powszechnie stosowane w branży urządzenia typu sonoff, czy tuya. Do centrali podłączone miałyby być konkretne czujniki (jak poziomu wody, otwartych okien/drzwi, itp.), odbiorniki (przekaźniki sterujące urządzeniami zarówno na 12v jak i na 230v), piloty (sterowanie oknem dachowym - podczerwień, sterowanie ogrzewaniem postojowym - pilot radiowy), oraz silniki elektryczne (zawór spustowy szarej wody). Najbardziej przeraża mnie mnogość rozwiązań oprogramowania - z tego co się dowiedziałem są rozwiązania płatne typu Arduino Cloud, albo open source, jak Thinx. Ja chciałbym coś, co wymaga jak najmniej fizycznego programowania (choć jeśli to konieczne, to nie odrzucam i takich rozwiązań), oraz ma możliwość zastosowania czytelnej aplikacji na smartfona, komputer i tablet. Pytań i wątpliwości jest oczywiście więcej, ale może na podstawowe tego co przedstawiłem, ktoś będzie w stanie polecić jakieś rozwiązania, za co z góry dziękuję.😉
  6. Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową #4 - Projekt praktyczny, Hosting Ostatnio dowiedzieliśmy się czym jest Firebase, jak stworzyć projekt i jak zadbać o jego bezpieczeństwo. Teraz nadszedł czas na bardziej praktyczne zastosowanie. W tej części omówimy dostęp do Firestore i RTDB z poziomu ESP8266 i ESP32. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » Wykorzystywane w dalszych przykładach bazy danych uzupełnijmy tak jak na poniższych zrzutach ekranu. Realtime Database Firestore DANE DOSTĘPOWE Do połączenia się z naszym projektem będą potrzebne jego identyfikator oraz klucz API. Możemy je znaleźć wchodząc w ustawienia naszego projektu. Widok ustawień projektu z zaznaczonym identyfikatorem i kluczem API. Będzie też potrzebny adres URL naszego projektu, który możemy znaleźć w zakładce RTDB. Widok zakładki RTDB z zaznaczonym adresem URL projektu. PRZYGOTOWANIE Do obsługi Firebase na płytkach ESP będziemy wykorzystywać bibliotekę Firebase_ESP_Client by mobizt. W tym celu na początku musimy ją zaimportować do naszego Arduino IDE (pobierając z Github lub wyszukując w menedżerze bibliotek Firebase Arduino Client Library for ESP8266 and ESP32). Dalsze przykłady będą bazować na przykładach dostarczonych przez twórcę tej biblioteki. Niezależnie od tego, z których usług Firebase chcemy korzystać, na początku musimy zaimportować biblioteki, połączyć się z wifi i zainicjalizować połączenie z Firebase. Importujemy biblioteki. Zwróćmy uwagę, że dzięki zaimportowaniu ich w taki sposób, nasz kod będzie działał na ESP8266 i na ESP32 bez konieczności modyfikowania go. // dołączamy bibliotekę WiFi w zależności od płytki #if defined(ESP32) #include <WiFi.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #endif // dołączenie biblioteki do obsługi Firebase #include <Firebase_ESP_Client.h> Następnie definiujemy obiekty, z których będziemy później korzystać: //Definiujemy obiekt Firebase Data FirebaseData fbdo; //Definiujemy FirebaseAuth, gdzie będziemy trzymać dane do uwierzytelniania FirebaseAuth auth; // Definiujemy FirebaseConfig, gdzie będziemy trzymać dane do konfiguracji FirebaseConfig config; Kolejnym krokiem jest połączenie z wifi. Robimy to wewnątrz funkcji setup(): void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); // ustawiamy swoje dane dostępu do wifi Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } } Musimy jeszcze zainicjować połączenie z naszym projektem: void setup() { // łączenie z wifi... //Do ustawień konfiguracji (config) zapisujemy klucz API i adres URL naszego projektu config.host = FIREBASE_HOST; //"PROJECT_ID.firebaseio.com" config.api_key = API_KEY; //Do ustawień uwierzytelniania zapisujemy dane do konta użytkownika, którego stworzyliśmy w poprzedniej części artykułu auth.user.email = USER_EMAIL; // "test@test.test" auth.user.password = USER_PASSWORD; // "123456" //Inicjalizujemy połączenie z Firebase używając ustawionych danych Firebase.begin(&config, &auth); //Opcjonalnie ustawiamy automatyczne łączenie z WiFi po zerwaniu połączenia (zalecane) Firebase.reconnectWiFi(true); } Gotowe! Nasz program łączy się już z naszym projektem i za moment będziemy mogli korzystać z dobrodziejstw oferowanych przez Firebase. Na chwilę oderwijmy się jednak od Firebase i spójrzmy na inną funkcjonalność, którą zapewnia nam ta biblioteka. OPERACJE NA FORMACIE JSON Twórca biblioteki dostarcza nam możliwość łatwego operowania na obiektach JSON za pomocą biblioteki FirebaseJson. Przyda nam się to podczas pracy z bazami danych, bo RTDB jest jednym wielkim drzewem JSON (omawialiśmy to w poprzedniej części artykułu). Do dyspozycji mamy 3 typy obiektów: FirebaseJson - struktura Json, dane przechowywane są w { } FirebaseJsonArray - tablica, którą możemy umieścić wewnątrz FirebaseJson, dane przechowywane są w [ ] FirebaseJsonData - typ służący do przechowywania fragmentów pobranych z FirebaseJson np. sama wartości temperatury z czujnik1. Przyjrzyjmy się najważniejszym funkcjom oferowanym przez tę bibliotekę: // deklaracja obiektu FirebaseJson FirebaseJson json; // deklaracja obiektu FirebaseJsonArray FirebaseJsonArray arr; // deklaracja obiektu FirebaseJsonData FirebaseJsonData jsonData; // dodanie nowej gałęzi z jej wartością json.add(“ścieżka”, wartość); // edytowanie, nadpisanie lub utworzenie nowej (jeśli nie istnieje) gałęzi json.set(“ścieżka”, wartość); // usuwanie gałęzi json.remove(“scieżka”); // zapisywanie obiektu json do postaci napisu (string) String nazwa_zmiennej_string; json.toString(nazwa_zmiennej_string, true); // czyszczenie zawartości obiektu json.clear(); // eksport zawartości gałęzi do jsonData json.get(jsonData, "ścieżka"); // zamienianie napisu (typ string) w obiekt FirebaseJson json.setJsonData(string); // dodawanie zawartości do obiektu FirebaseJsonArray arr.add(wartość); arr.add(“ścieżka”, wartość); // edytowanie, nadpisanie lub utworzenie nowej zawartości w obiekcie FirebaseJsonArray arr.set(“ścieżka”, wartość); // usuwanie zawartości komórki tablicy arr.remove(“ścieżka”); // zapisywanie obiektu arr do postaci napisu (string) String nazwa_zmiennej_string; arr.toString(nazwa_zmiennej_string, true); // czyszczenie zawartości obiektu arr arr.clear(); // eksport zawartości komórki tablicy do jsonData arr.get(jsonData, "ścieżka"); // parametr określający czy do jsonData udało się zapisać dane - true lub false jsonData.success // parametr określający typ danych w jsonData: string, int, double, bool, object, array, null, undefined jsonData.type // wypisywanie zawartości jsonData dla różnych typów danych Serial.println(jsonData.stringValue); Serial.println(jsonData.intValue); Serial.println(jsonData.boolValue); Serial.println(jsonData.floatValue); Serial.println(jsonData.doubleValue); Przykładowe zastosowanie: #include <Arduino.h> #include <FirebaseJson.h> void setup() { Serial.begin(115200); FirebaseJson json; FirebaseJson json1; json1.add("temperatura", 23.5); json1.add("wilgotnosc", 67); json.set("led/led1", false); json.set("led/led2", true); json.set("pomiary/czujnik1", json1); // dodajemy json1 jako wartość gałęzi "pomiary/czujnik1" String jsonStr; json.toString(jsonStr, true); Serial.println("----------------------------"); Serial.println(jsonStr); /* Otrzymujemy: { "led": { "led1": false, "led2": true }, "pomiary": { "czujnik1": { "temperatura": 23.5, "wilgotnosc": 67 } } } */ FirebaseJsonArray arr; arr.add("pomiar1"); arr.add("pomiar2"); arr.set("[3]/pomiar3/temperatura", "34"); arr.set("[3]/pomiar3/wilgotnosc", "44"); arr.set("[3]/pomiar4/temperatura", "11"); arr.set("[3]/pomiar4/wilgotnosc", "22"); json.clear(); json.add("pomiary",arr); // do gałęzi "pomiary" jako wartość dodajemy tablicę arr json.toString(jsonStr, true); Serial.println("----------------------------"); Serial.println(jsonStr); /* Otrzymujemy: { "pomiary": [ "pomiar1", //[0] - add automatycznie numeruje od zera "pomiar2", //[1] - następnie dodaje element o id 1 null, //[2] - element o id 2 jest null, bo pomiar3 i pomiar4 zapisujemy do elementu o id 3 { //[3] - tutaj dodajemy pomiar 3 i 4 "pomiar3": { "temperatura": "34", "wilgotnosc": "44" }, "pomiar4": { "temperatura": "11", "wilgotnosc": "22" } } ] } */ arr.clear(); FirebaseJsonArray arr2; arr.add("banan"); arr.add("gruszka"); arr.add("winogrono"); arr2.add("pomidor"); arr2.add("ogorek"); arr2.add("ziemniak"); json.clear(); json.add("owoce",arr); json.add("warzywa",arr2); json.toString(jsonStr, true); Serial.println("----------------------------"); Serial.println(jsonStr); /* Otrzymujemy: { "owoce": [ "banan", "gruszka", "winogrono" ], "warzywa": [ "pomidor", "ogorek", "ziemniak" ] } */ arr.clear(); arr2.clear(); arr.set("banan"); arr.set("gruszka"); arr.set("winogrono"); arr2.set("pomidor"); arr2.set("ogorek"); arr2.set("ziemniak"); json.clear(); json.add("owoce",arr); json.add("warzywa",arr2); json.toString(jsonStr, true); Serial.println("----------------------------"); Serial.println(jsonStr); /* Otrzymujemy: { "owoce": [ "root": [ "root": [ "root": [] ] ] ], "warzywa": [ "root": [ "root": [ "root": [] ] ] ] } Jest to przykład niepoprawnego użycia set(). Nie możemy set() używać zamiennie z add()! */ } void loop() { } Omówiliśmy tylko podstawowe funkcjonalności. Dokładne omówienie wszystkich funkcji, które oferuje ta biblioteka, możemy znaleźć w jej dokumentacji. OBSŁUGA REALTIME DATABASE Większość funkcji do obsługi RTDB wywołujemy w postaci Firebase.RTDB.nazwa_funkcji() . ODCZYT DANYCH Z RTDB Metoda, której będziemy używać zależy od tego, czy znamy typ danych w bazie. Jeśli wiemy jakiego typu są dane, które chcemy odczytać możemy użyć funkcji określonych dla konkretnych typów danych: getInt, getBool, getFloat, getDouble, getString, getJSON Jeśli tego nie wiemy możemy użyć funkcji get i sprawdzić typ otrzymanych danych za pomocą obiekt_FirebaseData.dataType() np. fbdo.dataType(). Ogólna postać wywołania wymienionych wyżej funkcji wygląda następująco: Firebase.RTDB.nazwa_funkcji(obiekt, "ścieżka") //obiekt to wskaźnik na utworzony wcześniej obiekt FirebaseData // dla uproszczenia w dalszej części będziemy go nazywać fbdo //np: Firebase.RTDB.nazwa_funkcji(&fbdo, "/pomiary/czujnik1/temperatura") Wywołanie tych funkcji zwraca prawdę, jeśli udało się odczytać dane, lub fałsz, jeśli wystąpił błąd. Po wywołaniu funkcji otrzymane dane będą przechowywane w obiekcie FirebaseData (będziemy go nazywać fbdo). W zależności od ich typu możemy odczytać je za pomocą jednego z poleceń: fbdo.intData() fbdo.floatData() fbdo.doubleData() fbdo.boolData() fbdo.stringData() fbdo.jsonString() fbdo.jsonObject() fbdo.jsonObjectPtr() fbdo.jsonArray() fbdo.jsonArrayPtr() fbdo.jsonData() fbdo.blobData() Próba pozyskania danych innego typu niż uzyskane z bazy zwróci pustkę (pustą zmienną/ pusty obiekt / pustą tablicę). Przykład zastosowania, gdy nie znamy typu: if(Firebase.RTDB.get(&fbdo, "/led/led1")) { //Sukces Serial.print("Udalo sie odczytac dane! typ = "); Serial.println(fbdo.dataType()); if(fbdo.dataType() == "int"){ Serial.print("wartosc = "); Serial.println(fbdo.intData()); }else if(fbdo.dataType() == "bool"){ if(fbdo.boolData()) Serial.println("wartosc: true"); else Serial.println("wartosc: false"); } }else{ //Błąd, wypisz powód Serial.print("Blad pobierania danych: "); Serial.println(fbdo.errorReason()); } Przykład zastosowania, gdy znamy typ odczytywanych danych: if (Firebase.RTDB.getInt(&fbdo, "/pomiary/czujnik1/wilgotnosc")) { if (fbdo.dataType() == "int") { Serial.println(fbdo.intData()); } } else { Serial.println(fbdo.errorReason()); } ZAPIS DANYCH DO RTDB Dane do RTDB możemy zapisać na 3 sposoby: set - zapisuje lub zmienia dane w konkretnym miejscu (konkretna ścieżka) np. możemy zmienić temperaturę w /pomiary/czujnik1/temperatura. Metoda ta spowoduje nadpisanie danych w określonej lokalizacji, w tym wszelkich węzłów podrzędnych. Oznacza to, że zapisując tylko wartość temperatury do lokalizacji /pomiary/czujnik1 utracimy zapisaną wartość wilgotności. update - zmienia dane w konkretnym miejscu bez nadpisywania pozostałych danych. Oznacza to, że zapisując tylko wartość temperatury do lokalizacji /pomiary/czujnik1 zapisana wartość wilgotności pozostanie bez zmian. push - dodaje nowy węzeł do bazy nadając mu unikalne id. Ma to zastosowanie np. jeżeli chcemy gromadzić historię pomiarów (a nie tylko ostatni). Przy zapisie za pomocą set i push, analogicznie jak podczas odczytu danych, możemy korzystać z różnych funkcji dla różnych typów danych: set, setInt, setFloat, setDouble, setBool, setString, setJSON, setArray, setBlob, setFile, push, pushInt, pushFloat, pushDouble, pushBool, pushString, pushJSON, pushArray, pushBlob, pushFile Funkcje te zwracają true lub false, w zależności od tego, czy udało się zapisać dane, czy nie. Standardowo wywołanie funkcji wygląda następująco: Firebase.RTDB.nazwa_funkcji(&fbdo, “ścieżka”, wartość); Dodatkowo istnieją funkcje, za pomocą których możemy zapisać aktualny czas serwera. pushTimestamp(&fbdo, “ścieżka”); setTimestamp(&fbdo, “ścieżka”); Chcąc skorzystać z metody update musimy nasze dane umieścić w strukturze FirebaseJson. Mamy do dyspozycji dwie funkcje: updateNode(&fbdo, “ścieżka”, &json); // serwer zwraca odpowiedź updateNodeSilent(&fbdo, “ścieżka”, &json); // serwer nie zwraca odpowiedzi Przykład zastosowania: // dołączamy bibliotekę WiFi w zależności od płytki #if defined(ESP32) #include <WiFi.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #endif // dołączenie biblioteki do obsługi Firebase #include <Firebase_ESP_Client.h> //Definiujemy obiekt Firebase Data FirebaseData fbdo; //Definiujemy FirebaseAuth, gdzie będziemy trzymać dane do uwierzytelniania FirebaseAuth auth; // Definiujemy FirebaseConfig, gdzie będziemy trzymać dane do konfiguracji FirebaseConfig config; void setup() { Serial.begin(115200); WiFi.begin("WIFI_SSID", "WIFI_HASLO"); Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } //Do ustawień uwierzytelniania zapisujemy dane do konta użytkownika auth.user.email = "test@test.test"; auth.user.password = "123456"; //Do ustawień konfiguracji (config) zapisujemy klucz API i adres projektu config.host = "https://ADRES.firebaseio.com"; config.api_key = "API_KEY"; //Inicjalizujemy połączenie z Firebase używając ustawionych danych Firebase.begin(&config, &auth); //Opcjonalnie ustawiamy automatyczne łączenie z WiFi po zerwaniu połączenia Firebase.reconnectWiFi(true); //----------------- ODCZYT --------------------------------------// Serial.print("GETINT: wilogotnosc: "); if (Firebase.RTDB.getInt(&fbdo, "/pomiary/czujnik1/wilgotnosc")) { if (fbdo.dataType() == "int") { Serial.println(fbdo.intData()); } } else { Serial.println(fbdo.errorReason()); } //----------------------- UPDATE ---------------------------------// Serial.println("Robie UPDATE"); FirebaseJson json; json.add("temperatura",55.8); if(Firebase.RTDB.updateNode(&fbdo, "/pomiary/czujnik1", &json)) { //sukces Serial.println("Update - sukces"); }else{ //blad Serial.print("Blad w update: "); Serial.println(fbdo.errorReason()); } delay(5000); //---------------------- PUSH --------------------------------------// Serial.println("Robie PUSH"); json.clear().add("temperatura",34.4); json.add("wilgotnosc", 55); if(Firebase.RTDB.pushJSON(&fbdo, "/pomiary/", &json)) { //sukces Serial.println("Push - sukces"); }else{ //blad Serial.print("Blad w push: "); Serial.println(fbdo.errorReason()); } delay(10000); //-------------------- SET - WERSJA 1 -------------------------------// Serial.println("Robie SET v1"); if(Firebase.RTDB.setFloat(&fbdo, "/pomiary/czujnik1/temperatura", 99.9)) { //sukces Serial.println("Set v1 - sukces"); }else{ //blad Serial.print("Blad w set v1: "); Serial.println(fbdo.errorReason()); } delay(5000); //------------------- SET - WERSJA 2 --------------------------// Serial.println("Robie SET v2"); json.clear().add("temperatura",33.3); if(Firebase.RTDB.setJSON(&fbdo, "/pomiary/czujnik1", &json)) { //sukces Serial.println("Set v2 - sukces"); }else{ //blad Serial.print("Blad w set v2: "); Serial.println(fbdo.errorReason()); } delay(5000); //----------------- ZAPIS CZASU ------------------------------// Serial.println("Robie zapis czasu"); if(Firebase.RTDB.pushTimestamp(&fbdo, "/czas")) { //sukces Serial.println("Zapis czasu - sukces"); }else{ //blad Serial.print("Blad w zapisie czasu: "); Serial.println(fbdo.errorReason()); } } void loop() {} Kolejne etapy wykonywania programu: 0. Widok przed uruchomieniem programu. 1. Update na /pomiary/czujnik1 zmieniło tylko wartość temperatury (wilgotność pozostała bez zmian). 2. Push stworzyło nową gałąź o unikalnym id. 3. Set wykonane na /pomiary/czujnik1/temperatura zmodyfikowało tylko gałąź z temperaturą. 4. set wykonane na /pomiary/czujnik1 (tak samo jak update) nadpisało całą gałąź. Zapisało nową wartość temperatury, ale w przeciwieństwie do update straciliśmy zapisaną wartość wilgotności. 5. Zapis czasu. Widok monitora portu szeregowego. USUWANIE DANYCH Z RTDB Możemy usunąć całą gałąź przy pomocy funkcji deleteNode(&fbdo, “ścieżka”). NASŁUCHIWANIE ZMIAN W CZASIE RZECZYWISTYM Czasami potrzebujemy wiedzieć, że jakieś dane w bazie uległy zmianie (np. użytkownik poprzez stronę internetową chce włączyć lampkę). Autor biblioteki udostępnił do tego celu funkcje pozwalające na śledzenie zmian w wybranej gałęzi. Rozpoczęcie nasłuchiwania zmian: beginStream(&fbdo, “ścieżka”); Ustawienie jaka funkcja ma zostać wywołana po wykryciu zmian w bazie: setStreamCallback(&fbdo, “nazwa_funkcji”, “nazwa_funkcji_wywoływanej_po_przekroczeniu_limitu_czasu”); W praktyce możemy to przetestować na przykładowym kodzie dostarczonym przez autora biblioteki. W Arduino IDE z przykładów dla tej biblioteki wybieramy RTDB->Stream_Callback. Usuwamy całą zawartość funkcji loop() oraz zbędne zmienne i funkcję printResult(FirebaseData &data), zmieniamy String path na "/led/led1" i ustawiamy nasze dane dostępowe (wifi, dane użytkownika, dane bazy). Tak zmodyfikowany kod będzie wyświetlał informacje w monitorze portu szeregowego za każdym razem, gdy zmienimy wartość w gałęzi "/led/led1" w bazie (my na razie zrobimy to ręcznie w konsoli Firebase). Zmodyfikowany kod: /** * Created by K. Suwatchai (Mobizt) * * Email: k_suwatchai@hotmail.com * * Github: https://github.com/mobizt * * Copyright (c) 2021 mobizt * */ #if defined(ESP32) #include <WiFi.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #endif #include <Firebase_ESP_Client.h> /* 1. Define the WiFi credentials */ #define WIFI_SSID "xxxxxxx" #define WIFI_PASSWORD "xxxxxxx" /* 2. Define the Firebase project host name and API Key */ #define FIREBASE_HOST "https://ADRES.firebaseio.com" #define API_KEY "API_KEY" /* 3. Define the user Email and password that alreadey registerd or added in your project */ #define USER_EMAIL "test@test.test" #define USER_PASSWORD "123456" //Define FirebaseESP8266 data object FirebaseData fbdo1; FirebaseAuth auth; FirebaseConfig config; String path = "/led/led1"; void printResult(FirebaseStream &data); // funkcja, która zostanie wywołana po wykryciu zmiany wartości w bazie void streamCallback(FirebaseStream data) { Serial.println("Stream Data1 available..."); Serial.println("STREAM PATH: " + data.streamPath()); Serial.println("EVENT PATH: " + data.dataPath()); Serial.println("DATA TYPE: " + data.dataType()); Serial.println("EVENT TYPE: " + data.eventType()); Serial.print("VALUE: "); printResult(data); Serial.println(); } void streamTimeoutCallback(bool timeout) { if (timeout) { Serial.println(); Serial.println("Stream timeout, resume streaming..."); Serial.println(); } } void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } Serial.println(); Serial.print("Connected with IP: "); Serial.println(WiFi.localIP()); Serial.println(); /* Assign the project host and api key (required) */ config.host = FIREBASE_HOST; config.api_key = API_KEY; /* Assign the user sign in credentials */ auth.user.email = "test@test.test"; auth.user.password = "123456"; Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); #if defined(ESP8266) //Set the size of WiFi rx/tx buffers in the case where we want to work with large data. fbdo1.setBSSLBufferSize(1024, 1024); #endif //Set the size of HTTP response buffers in the case where we want to work with large data. fbdo1.setResponseSize(1024); if (!Firebase.RTDB.beginStream(&fbdo1, path.c_str())) { Serial.println("------------------------------------"); Serial.println("Can't begin stream connection..."); Serial.println("REASON: " + fbdo1.errorReason()); Serial.println("------------------------------------"); Serial.println(); } // rozpoczynamy nasłuchiwanie zmian Firebase.RTDB.setStreamCallback(&fbdo1, streamCallback, streamTimeoutCallback); } void loop() { } void printResult(FirebaseStream &data) { if (data.dataType() == "int") Serial.println(data.intData()); else if (data.dataType() == "float") Serial.println(data.floatData(), 5); else if (data.dataType() == "double") printf("%.9lf\n", data.doubleData()); else if (data.dataType() == "boolean") Serial.println(data.boolData() == 1 ? "true" : "false"); else if (data.dataType() == "string" || data.dataType() == "null") Serial.println(data.stringData()); else if (data.dataType() == "json") { Serial.println(); FirebaseJson *json = data.jsonObjectPtr(); //Print all object data Serial.println("Pretty printed JSON data:"); String jsonStr; json->toString(jsonStr, true); Serial.println(jsonStr); Serial.println(); Serial.println("Iterate JSON data:"); Serial.println(); size_t len = json->iteratorBegin(); String key, value = ""; int type = 0; for (size_t i = 0; i < len; i++) { json->iteratorGet(i, type, key, value); Serial.print(i); Serial.print(", "); Serial.print("Type: "); Serial.print(type == FirebaseJson::JSON_OBJECT ? "object" : "array"); if (type == FirebaseJson::JSON_OBJECT) { Serial.print(", Key: "); Serial.print(key); } Serial.print(", Value: "); Serial.println(value); } json->iteratorEnd(); } else if (data.dataType() == "array") { Serial.println(); //get array data from FirebaseData using FirebaseJsonArray object FirebaseJsonArray *arr = data.jsonArrayPtr(); //Print all array values Serial.println("Pretty printed Array:"); String arrStr; arr->toString(arrStr, true); Serial.println(arrStr); Serial.println(); Serial.println("Iterate array values:"); Serial.println(); for (size_t i = 0; i < arr->size(); i++) { Serial.print(i); Serial.print(", Value: "); FirebaseJsonData *jsonData = data.jsonDataPtr(); //Get the result data from FirebaseJsonArray object arr->get(*jsonData, i); if (jsonData->typeNum == FirebaseJson::JSON_BOOL) Serial.println(jsonData->boolValue ? "true" : "false"); else if (jsonData->typeNum == FirebaseJson::JSON_INT) Serial.println(jsonData->intValue); else if (jsonData->typeNum == FirebaseJson::JSON_FLOAT) Serial.println(jsonData->floatValue); else if (jsonData->typeNum == FirebaseJson::JSON_DOUBLE) printf("%.9lf\n", jsonData->doubleValue); else if (jsonData->typeNum == FirebaseJson::JSON_STRING || jsonData->typeNum == FirebaseJson::JSON_NULL || jsonData->typeNum == FirebaseJson::JSON_OBJECT || jsonData->typeNum == FirebaseJson::JSON_ARRAY) Serial.println(jsonData->stringValue); } } else if (data.dataType() == "blob") { Serial.println(); for (size_t i = 0; i < data.blobData().size(); i++) { if (i > 0 && i % 16 == 0) Serial.println(); if (i < 16) Serial.print("0"); Serial.print(data.blobData()[i], HEX); Serial.print(" "); } Serial.println(); } else if (data.dataType() == "file") { Serial.println(); File file = data.fileStream(); int i = 0; while (file.available()) { if (i > 0 && i % 16 == 0) Serial.println(); int v = file.read(); if (v < 16) Serial.print("0"); Serial.print(v, HEX); Serial.print(" "); i++; } Serial.println(); file.close(); } } Po uruchomieniu program wypisze w monitorze portu szeregowego aktualny stan gałęzi led/led1 i będzie czekać aż dokonamy zmian tej gałęzi w bazie. Za każdym razem, gdy dokonamy zmiany zostanie wypisany aktualny stan gałęzi. Widok monitora portu szeregowego po uruchomieniu programu i po zmianie wartość led1 z true na false. OBSŁUGA FIRESTORE Teraz zajmiemy się obsługą Firestore. Analogicznie do RTDB funkcje dotyczące Firestore wywołujemy w postaci Firebase.Firestore.nazwa_funkcji() . ODCZYT DANYCH W przeciwieństwie do RTDB nie mamy oddzielnej funkcji dla każdego rodzaju danych. Do pobierania danych z Firestore używamy tylko funkcji getDocument(). W ogólności funkcja ta wygląda następująco: getDocument(FirebaseData *fbdo, “id_projektu”, “id_bazy_danych”, “ścieżka”, “maska”); id_bazy_danych to najczęściej “” lub (default), więc my będziemy używać postaci: getDocument(&fbdo, “id_projektu”, "", “ścieżka”, “maska”) “maska” wskazuje, które elementy ze ścieżki mają być zwrócone. Pozwala to na pewnego rodzaju filtrowanie danych. Pominięcie tego argumentu zwróci całą zawartość ze ścieżki. Pozyskane dane możemy odczytać przy pomocy fbdo.payload(). Funkcja ta zwraca nam tekst (string), który wyglądem przypomina drzewo JSON. Przykład odczytania z bazy zawierającej w kolekcji “pomiary” dokumenty o id “czujnik2” i “czujnik3” (tak jak uzupełniliśmy na początku tej części). Ścieżka “pomiary”, brak maski. Przykład odczytu z tej samej bazy, ale ze ścieżką “pomiary/czujnik2”, brak maski. Przykład odczytu z tej samej bazy, ale ze ścieżką “pomiary” i maską “wilgotnosc”. Aby dostać się do konkretnej wartości możemy zamienić ten string na obiekt FirebaseJson. Służy do tego funkcja setJsonData(string). FirebaseJson json; json.setJsonData(fbdo.payload()); Następnie możemy pozyskać dane z gałęzi, która nas interesuje za pomocą funkcji get(). Teraz spróbujemy odczytać temperaturę odczytaną przez czujnik2 ze stworzonej przez nas bazy. Interesuje nas kolekcja pomiary i dokument czujnik2, więc jako ścieżkę ustawiamy "pomiary/czujnik2". Chcemy pobrać jedynie temperaturę (bez wilgotności), więc ustawiamy maskę “temperatura”. Firebase.Firestore.getDocument(&fbdo, FIREBASE_PROJECT_ID, "", "pomiary/czujnik2", "temperatura") Po wywołaniu funkcji możemy podejrzeć pobrane z bazy dane w monitorze portu szeregowego poleceniem: Serial.println(fbdo.payload()); Widok monitora portu szeregowego. Aby uzyskać samą wartość temperatury musimy najpierw zamienić to co otrzymaliśmy na obiekt FirebaseJson. FirebaseJson json; json.setJsonData(fbdo.payload()); Następnie zapisujemy wartość temperatury do FirebaseJsonData za pomocą funkcji get(). Z wyświetlonego w monitorze portu szeregowego schematu naszego obiektu JSON możemy odczytać, że ścieżką do naszej wartości temperatury będzie "fields/temperatura/doubleValue". FirebaseJsonData jsonData; json.get(jsonData, "fields/temperatura/doubleValue"); Ścieżka do wartości temperatury. Wartość temperatury jest zapisana jako typ double, więc aby ją uzyskać odczytujemy parametr: jsonData.doubleValue Cały kod wygląda następująco: #if defined(ESP32) #include <WiFi.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #endif #include <Firebase_ESP_Client.h> /* 1. Define the WiFi credentials */ #define WIFI_SSID "xxxx" #define WIFI_PASSWORD "xxxx" /* 2. Define the Firebase project host name and API Key */ #define FIREBASE_HOST "https://ADRES.firebaseio.com" #define API_KEY "API_KEY" #define FIREBASE_PROJECT_ID "PROJECT_ID" /* 3. Define the user Email and password that alreadey registerd or added in your project */ #define USER_EMAIL "test@test.test" #define USER_PASSWORD "123456" //Define Firebase Data object FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config; void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } Serial.println(); /* Assign the project host and api key (required) */ config.host = FIREBASE_HOST; config.api_key = API_KEY; /* Assign the user sign in credentials */ auth.user.email = USER_EMAIL; auth.user.password = USER_PASSWORD; Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); if (Firebase.Firestore.getDocument(&fbdo, FIREBASE_PROJECT_ID, "", "pomiary/czujnik2", "temperatura")) { Serial.println("WIDOK W POSTACI JSON"); Serial.println("------------------------------------"); Serial.println(fbdo.payload()); Serial.println("------------------------------------"); FirebaseJson json; json.setJsonData(fbdo.payload()); FirebaseJsonData jsonData; json.get(jsonData, "fields/temperatura/doubleValue"); Serial.print("Wartosc temperatury: "); Serial.println(jsonData.doubleValue); } else { Serial.println("FAILED"); Serial.println("REASON: " + fbdo.errorReason()); Serial.println("------------------------------------"); Serial.println(); } } void loop() { } Wynik w monitorze portu szeregowego. ZAPIS DANYCH Do zapisu możemy użyć funkcji createDocument() (do tworzenia) oraz patchDocument() (do edytowania). Funkcje te analogicznie do poprzednich zwracają true lub false. createDocument(FirebaseData *fbdo, “id_projektu”, “id_bazy_danych”, “ścieżka”, “wartosc”); patchDocument(FirebaseData *fbdo, “id_projektu”, “id_bazy_danych”, “ścieżka”, “wartosc”, “pola_do_zmodyfikowania”); Do tych funkcji wartość musimy podać w takiej postaci, w jakiej przed chwilą odczytywaliśmy funkcją getDocument(), to znaczy napis (string) w postaci drzewa JSON. Załóżmy, że chcemy prowadzić historię pomiarów. Dane będziemy przechowywać w kolekcji historia w oddzielnym dokumencie dla każdego pomiaru (nazwanym pomiar_nr). Do każdego dokumentu będziemy zapisywać temperaturę i wilgotność. Pomiary będziemy robić co 10 sekund. Oczekiwany rezultat. Na początku tworzymy zawartość dokumentu i zapisujemy ją do zmiennej typu String: String content; FirebaseJson js; js.set("fields/temperatura/doubleValue", String(random(10000)/100.00).c_str()); // tempertura to losowa liczba niecałkowita 0.00 - 99.99 js.set("fields/wilgotnosc/integerValue", String(random(100)).c_str()); // wilgotność to losowa liczba całkowita 0-99 js.toString(content); Teraz tworzymy ścieżkę do dokumentu. Składa się ona z nazwy kolekcji i id dokumentu: String documentPath = "historia/pomiar_" + String(numer); gdzie numer to zmienna, po której iterujemy pętlę wykonującą się co 10 sekund. Oto jak będzie wyglądać nasze wysyłanie funkcji createDocument: Firebase.Firestore.createDocument(&fbdo, FIREBASE_PROJECT_ID, "", documentPath.c_str(), content.c_str()) Cały kod wygląda następująco: #if defined(ESP32) #include <WiFi.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #endif #include <Firebase_ESP_Client.h> #define WIFI_SSID "xxxx" #define WIFI_PASSWORD "xxxx" #define FIREBASE_HOST "https://ADRES.firebaseio.com" #define API_KEY "API_KEY" #define FIREBASE_PROJECT_ID "PROJECT_ID" #define USER_EMAIL "test@test.test" #define USER_PASSWORD "123456" FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config; void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } Serial.println(); config.host = FIREBASE_HOST; config.api_key = API_KEY; auth.user.email = USER_EMAIL; auth.user.password = USER_PASSWORD; Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); } unsigned long dataMillis = 0; int numer = 0; void loop() { if (millis() - dataMillis > 10000 || dataMillis == 0) { dataMillis = millis(); String content; FirebaseJson js; String documentPath = "historia/pomiar_" + String(numer); js.set("fields/temperatura/doubleValue", String(random(10000)/100.00).c_str()); js.set("fields/wilgotnosc/integerValue", String(random(100)).c_str()); js.toString(content); numer++; Serial.println("------------------------------------"); Serial.println("Create a document..."); if (Firebase.Firestore.createDocument(&fbdo, FIREBASE_PROJECT_ID, "", documentPath.c_str(), content.c_str())) { Serial.println("SUKCES"); } else { Serial.println("BLAD: " + fbdo.errorReason()); } } } Po uruchomieniu programu nasz baza Firestore zacznie się zapełniać kolejnymi pomiarami co 10 sekund: pomiar_0, pomiar_1, pomiar_2…, a w monitorze portu szeregowego zobaczymy komunikat o sukcesie. Widok monitora portu szeregowego - sukces! Jednak jeżeli uruchomimy program ponownie to dostaniemy błąd! W monitorze portu szeregowego wyskakuje błąd 😞 Dzieje się tak dlatego, że funkcja createDocument() służy tylko do tworzenia dokumentów, które jeszcze nie istnieją. Ponowna próba stworzenia dokumentu pomiar_0 powoduje błąd. Jeżeli chcemy edytować już istniejący dokument musimy użyć funkcji patchDocument(). W naszym kodzie musimy zmienić tylko linijkę z getDocument() na: Firebase.Firestore.patchDocument(&fbdo, FIREBASE_PROJECT_ID, "", documentPath.c_str(), content.c_str(),"temperatura,wilgotnosc") W miejscu “pola_do_zmodyfikownia” wpisujemy "temperatura,wilgotnosc" (bez spacji!), ponieważ chcemy zaktualizować w bazie temperaturę oraz wilgotność. Jeżeli wpisalibyśmy tylko jeden z tych parametrów (np. “temperatura”) to wartość pozostałych parametrów (wilgotność) nie uległaby zmianie. Gdy używamy patch, a podany dokument nie istnieje to zostanie on utworzony (tak jak byśmy użyli create). USUWANIE DOKUMENTÓW Do usuwania dokumentów służy funkcja deleteDocument(). PODSUMOWANIE W tej części nauczyliśmy się zapisywać i odczytywać dane z RTDB i z Firestore za pomocą ESP8266 i ESP32. Dowiedzieliśmy się także jak operować na obiektach JSON w Arduino IDE. Przedstawiona biblioteka daje jeszcze wiele możliwości, których nie omówiliśmy. Polecam zapoznać się z dokumentacją biblioteki, gdzie są one dokładnie opisane. W dokumentacji Firebase znajduje się także opis działania Firebase z innymi językami programowania oraz dostęp poprzez API. W kolejnej części dowiemy się więcej o prezentowaniu zawartości naszej bazy użytkownikowi. Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 (właśnie to czytasz)
  7. Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową #4 - Projekt praktyczny, Hosting Potrafimy już obsługiwać Firebase za pomocą urządzeń ESP oraz przez JavaScript. Dzisiaj podsumujemy wiedzę zdobytą w poprzednich trzech częściach kursu i wykorzystamy ją w praktyce. Zbudujemy prosty system smarthome. Będzie on umożliwiał użytkownikowi na zdalne: sprawdzenie aktualnej temperatury w pokoju sprawdzenie kilku ostatnich pomiarów wilgotności ziemi w doniczce kwiatka sterowanie diodą led System ten jest bardzo prosty, ale umożliwi nam praktyczne wykorzystanie wielu funkcji Firebase. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » Potrzebny sprzęt: ESP8266 (ew. ESP32) cyfrowy czujnik temperatury DS18B20 czujnik wilgotności gleby dioda led rezystory 4.7kΩ , 330Ω płytka stykowa, przewody połączeniowe... Jeśli nie masz któregoś z czujników, to możesz użyć dowolnego innego. QUO VADIS? Najpierw musimy określić jak dokładnie będzie działać nasz system. Urządzenie będzie co określony czas wysyłać do bazy wartość temperatury, zastępując wcześniej zapisaną wartość, oraz wysyłać wartość wilgotności gleby tworząc nową gałąź (nie tracimy poprzednich wyników). Będzie nasłuchiwać zmian w gałęzi, w której jest zapisany stan diody led i odpowiednio ją zapalać lub gasić. Zalogowany użytkownik będzie mógł poprzez stronę internetową sprawdzić aktualną wartość temperatury oraz 4 ostatnie pomiary wilgotności, a także zobaczyć aktualny stan diody i zdecydować o jej włączeniu lub wyłączeniu. Nasza baza danych będzie przechowywała mało informacji, które nie będą bardzo zagnieżdżone, a my nie będziemy wykonywać skomplikowanych operacji na danych, więc w naszym projekcie użyjemy Realtime Database. Nie chcemy, aby sąsiad mógł się bawić naszą diodą, więc użyjemy Auth do uwierzytelniania użytkowników. KONFIGURACJA FIREBASE Na początku tworzymy projekt Firebase, konfigurujemy Auth (uwierzytelnianie za pomocą e-mail) i dodajemy użytkownika np. test@test.test, 123456 (jest to opisane w I części kursu). Tworzenie projektu i konfiguracja Auth. Następnie dodajemy do projektu Realtime Database i konfigurujemy zabezpieczenia, aby tylko zalogowany użytkownik mógł mieć dostęp do naszej bazy. Dodawanie RTDB, reguły zabezpieczeń. Teraz tworzymy strukturę danych w bazie: w gałęzi led będziemy przechowywać stan diody (true/false) w gałęzi temperatura będziemy przechowywać wartość ostatniego pomiaru temperatury i czas tego pomiaru (w czasie uniksowym) w gałęzi wilgotnosc będziemy przechowywać kolejne pomiary wilgotności. Klucz będzie generowany automatycznie, każdy wpis będzie zawierał zmierzoną wartość i czas pomiaru. Struktura naszej bazy danych. Musimy jeszcze dodać aplikację webową do naszego projektu i pozyskać dane dostępowe projektu (klucz API, adres URL). Dodawanie aplikacji, dane dostępowe. ESP8266 HARDWARE Mając skonfigurowany Firebase możemy wziąć się za budowę naszego urządzenia. Łączymy: wyjście czujnika wilgotności do pinu A0 ESP wyjście czujnika temperatury DS18B20 do pinu D2 oraz wpinamy rezystor 4.7kΩ między wyjściem, a dodatnią szyną zasilania (tak jak zostało to opisane w kursie Arduino) dłuższą nożkę diody do pinu D2, a krótszą przez rezystor 330Ω do GND Całość prezentuje się następująco: Połączenie. SOFTWARE Teraz zajmiemy się pisaniem oprogramowania dla naszego ESP. Tutaj przyda się wiedza z II części kursu. Czujnik wilgotności jest najczęściej czujnikiem analogowym i wymaga kalibracji. W tym celu wgrywamy na płytkę przykład 01.Basics -> AnalogReadSerial z ArduinoIDE. Następnie sprawdzamy jakie wartości pokazują się w monitorze portu szeregowego, gdy czujnik znajduje się w suchym miejscu (zakładamy wilgotność 0%) oraz po zanurzeniu w wodzie (wilgotność 100%) i zapamiętujemy te wartości. U mnie wynoszą one odpowiednio 720 i 270. Tworzymy nowy projekt, importujemy biblioteki do czujnika temperatury i do Firebase oraz definiujemy obiekty Firebase. #include <OneWire.h> #include <DallasTemperature.h> // dołączamy bibliotekę WiFi w zależności od płytki #if defined(ESP32) #include <WiFi.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #endif // dołączenie biblioteki do obsługi Firebase #include <Firebase_ESP_Client.h> //Definiujemy obiekt Firebase Data FirebaseData fbdo; FirebaseData fbdo_stream; //Definiujemy FirebaseAuth, gdzie będziemy trzymać dane do uwierzytelniania FirebaseAuth auth; // Definiujemy FirebaseConfig, gdzie będziemy trzymać dane do konfiguracji FirebaseConfig config; Następnie definiujemy numery pinów, zmierzone przed chwilą wartości wilgotności oraz czas między pomiarami. #define LED_PIN D3 #define TEMP_PIN D2 #define SOIL_PIN A0 const int AirValue = 720; // wartość wilgotności w powietrzu - 0% const int WaterValue = 270; // watość wilgotności w wodzie - 100% const int TEMP_DELAY = 10000;//ms const int SOIL_DELAY = 20000;//ms Teraz konfigurujemy czujnik temperatury: OneWire oneWire(TEMP_PIN); DallasTemperature temp(&oneWire); Zdefiniujmy sobie funkcję, która będzie mierzyła wilgotność i zwróci wynik w procentach. Funkcja odczytuje wartość z czujnika, przeskalowuje ją z wartości maksymalnej i minimalnej, które zmierzyliśmy, do 0-100%. Dodatkowo wprowadzamy zabezpieczenie, żeby wynik zawsze mieścił się w przedziale 0-100% int getSoil() // pomiar wilgotnosci, zwraca % { int soilValue = analogRead(SOIL_PIN); int soilPercent = map(soilValue, AirValue, WaterValue, 0, 100); if(soilPercent > 100) soilPercent = 100; else if (soilPercent <0) soilPercent = 0; return(soilPercent); } Tworzymy funkcję do odczytu temperatury: float getTemperature() { temp.requestTemperatures(); return(temp.getTempCByIndex(0)); } Wewnątrz setup() ustawiamy pin diody jako wyjście, załączamy czujnik temperatury, łączymy się z wifi i konfigurujemy połączenie z Firebase (tak jak jest to opisane w II części kursu). void setup() { Serial.begin(115200); pinMode(D3, OUTPUT); temp.begin(); WiFi.begin("ssid", "haslo"); //zmienić Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } //Do ustawień uwierzytelniania zapisujemy dane do konta użytkownika auth.user.email = "test@test.test"; auth.user.password = "123456"; //Do ustawień konfiguracji (config) zapisujemy klucz API i adres projektu config.host = "https://xxxxx-default-rtdb.firebaseio.com"; // zmienić config.api_key = "xxxxxxxx"; // zmienić //Inicjalizujemy połączenie z Firebase używając ustawionych danych Firebase.begin(&config, &auth); //Ustawiamy automatyczne łączenie z WiFi po zerwaniu połączenia Firebase.reconnectWiFi(true); } Wewnątrz funkcji loop() tworzymy dwa warunki (pomiar temperatury i wilgotności), które wykonują się co określony wcześniej czas. Korzystamy do tego z millis(), tak jak zostało to opisane w kursie Arduino. W każdym z tych warunków mierzymy odpowiednio temperaturę lub wilgotność i wysyłamy do RTDB. void loop() { static unsigned long prevTempMillis = 0; static unsigned long prevSoilMillis = 0; if(millis()-prevTempMillis >= TEMP_DELAY) { prevTempMillis = millis(); float temp = getTemperature(); Serial.print("T: "); Serial.println(temp); Serial.println(); if(Firebase.RTDB.setFloat(&fbdo, "/temperatura/wartosc", temp)) { //sukces Serial.println("Set v1 - sukces"); }else{ //blad Serial.print("Blad w set v1: "); Serial.println(fbdo.errorReason()); } Firebase.RTDB.setTimestamp(&fbdo, "/temperatura/czas"); } if(millis()-prevSoilMillis >= SOIL_DELAY) { prevSoilMillis = millis(); int soil = getSoil(); Serial.print("W: "); Serial.print(soil); Serial.println(" %"); Serial.println(); FirebaseJson json; json.add("wartosc",soil); if(Firebase.RTDB.pushJSON(&fbdo, "/wilgotnosc/", &json)) { //sukces Serial.println("Push - sukces"); }else{ //blad Serial.print("Blad w push: "); Serial.println(fbdo.errorReason()); } String path = "/wilgotnosc/"; path = path + fbdo.pushName(); path = path + "/czas"; Firebase.RTDB.setTimestamp(&fbdo, path.c_str()); } } Zostało nam tylko ustawić nasłuchiwanie zmian w gałęzi led. W tym celu tworzymy funkcję streamCallback(), która wykonuje się po wykryciu zmiany w bazie, oraz pustą funkcję streamTimeoutCallback(), która wykonuje się po przekroczeniu limitu czasu połączenia (my zostawiamy ją pustą, ale musimy ją stworzyć). void streamCallback(FirebaseStream data) { if (data.dataType() == "int") digitalWrite(LED_PIN, data.intData()); else if (data.dataType() == "boolean") digitalWrite(LED_PIN, data.boolData()); } void streamTimeoutCallback(bool timeout) { ; } Ostatnim krokiem jest włączenie nasłuchiwania wewnątrz setup(): Firebase.RTDB.beginStream(&fbdo_stream, "/led"); Firebase.RTDB.setStreamCallback(&fbdo_stream, streamCallback, streamTimeoutCallback); Cały kod wygląda następująco: #include <OneWire.h> #include <DallasTemperature.h> // dołączamy bibliotekę WiFi w zależności od płytki #if defined(ESP32) #include <WiFi.h> #elif defined(ESP8266) #include <ESP8266WiFi.h> #endif // dołączenie biblioteki do obsługi Firebase #include <Firebase_ESP_Client.h> //Definiujemy obiekt Firebase Data FirebaseData fbdo; FirebaseData fbdo_stream; //Definiujemy FirebaseAuth, gdzie będziemy trzymać dane do uwierzytelniania FirebaseAuth auth; // Definiujemy FirebaseConfig, gdzie będziemy trzymać dane do konfiguracji FirebaseConfig config; #define LED_PIN D3 #define TEMP_PIN D2 #define SOIL_PIN A0 const int AirValue = 720; // watość wilgotności w powietrzu - 0% const int WaterValue = 270; // watość wilgotności w wodzie - 100% const int TEMP_DELAY = 10000;//ms const int SOIL_DELAY = 20000;//ms OneWire oneWire(TEMP_PIN); DallasTemperature temp(&oneWire); int getSoil() // pomiar wilgotnosci, zwraca % { int soilValue = analogRead(SOIL_PIN); //put Sensor insert into soil Serial.print("Wilgotnosc: "); //później zakomentować Serial.println(soilValue); int soilPercent = map(soilValue, AirValue, WaterValue, 0, 100); if(soilPercent > 100) soilPercent = 100; else if (soilPercent <0) soilPercent = 0; return(soilPercent); } float getTemperature() { temp.requestTemperatures(); return(temp.getTempCByIndex(0)); } void streamCallback(FirebaseStream data) { if (data.dataType() == "int") digitalWrite(LED_PIN, data.intData()); else if (data.dataType() == "boolean") digitalWrite(LED_PIN, data.boolData()); } void streamTimeoutCallback(bool timeout) { ; } void setup() { Serial.begin(115200); pinMode(D3, OUTPUT); temp.begin(); WiFi.begin("xxxx", "xxxx"); //zmienić Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } //Do ustawień uwierzytelniania zapisujemy dane do konta użytkownika auth.user.email = "test@test.test"; auth.user.password = "123456"; //Do ustawień konfiguracji (config) zapisujemy klucz API i adres projektu config.host = "https://xxxxx-default-rtdb.firebaseio.com"; //zmienić config.api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxx"; //zmienić //Inicjalizujemy połączenie z Firebase używając ustawionych danych Firebase.begin(&config, &auth); //Opcjonalnie ustawiamy automatyczne łączenie z WiFi po zerwaniu połączenia Firebase.reconnectWiFi(true); Firebase.RTDB.beginStream(&fbdo_stream, "/led"); Firebase.RTDB.setStreamCallback(&fbdo_stream, streamCallback, streamTimeoutCallback); } void loop() { static unsigned long prevTempMillis = 0; static unsigned long prevSoilMillis = 0; if(millis()-prevTempMillis >= TEMP_DELAY) { prevTempMillis = millis(); float temp = getTemperature(); Serial.print("T: "); Serial.println(temp); Serial.println(); if(Firebase.RTDB.setFloat(&fbdo, "/temperatura/wartosc", temp)) { //sukces Serial.println("Set v1 - sukces"); }else{ //blad Serial.print("Blad w set v1: "); Serial.println(fbdo.errorReason()); } Firebase.RTDB.setTimestamp(&fbdo, "/temperatura/czas"); } if(millis()-prevSoilMillis >= SOIL_DELAY) { prevSoilMillis = millis(); int soil = getSoil(); Serial.print("W: "); Serial.print(soil); Serial.println(" %"); Serial.println(); FirebaseJson json; json.add("wartosc",soil); if(Firebase.RTDB.pushJSON(&fbdo, "/wilgotnosc/", &json)) { //sukces Serial.println("Push - sukces"); }else{ //blad Serial.print("Blad w push: "); Serial.println(fbdo.errorReason()); } String path = "/wilgotnosc/"; path = path + fbdo.pushName(); path = path + "/czas"; Firebase.RTDB.setTimestamp(&fbdo, path.c_str()); } } Po wgraniu programu na płytkę nasze urządzenie zacznie wysyłać do bazy co 10 sekund wartość temperatury i co 20 sekund wartość wilgotności oraz włączać lub wyłączać diodę, w zależności od wartości gałęzi led. Działanie. STRONA INTERNETOWA Teraz zajmiemy się interfejsem dla naszego użytkownika. Nasz projekt Firebase możemy zintegrować z zewnętrznymi narzędziami (np. NodeRed, Sketchware, ...), napisać aplikację mobilną lub stworzyć własną stronę w html+css+js. My skorzystamy z tej ostatniej ścieżki. Tworzenie własnej strony od zera daje nam dużo możliwości, to my decydujemy o każdym szczególe naszego serwisu. Niestety wymaga to umiejętności programowania w html+css+js, aby osiągnąć zamierzony efekt. W tym kursie skupiamy się na poznawaniu Firebase, więc nasza strona będzie bardzo prosta, ale nic nie stoi na przeszkodzie, aby dodać do niej więcej funkcji. Oto nasz dzisiejszy cel: Widok gotowej strony. Na początku tworzymy plik index.html. Robimy w nim szkielet naszej strony i konfigurujemy połączenie z Firebase. Zamieszczamy w nim identyfikatory wskazujące: miejsce na formularz logowania panel diody, miejsce na aktualny stan diody, przycisk do zmieniania stanu diody panel temperatury, miejsce na aktualną wartość temperatury, miejsce na czas ostatniego pomiaru panel na listę ostatnich pomiarów wilgotności Dane w tych miejscach będziemy uzupełniać z poziomu JavaScript. Cały kod wygląda następująco (zwróć uwagę na komentarze): <!DOCTYPE HTML> <html lang="pl"> <html> <head> <meta charset="utf-8" /> <title>Panel użytkownika</title> <!-- Importujemy skrypty do obsługi Firebase --> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-app.js"></script> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-auth.js"></script> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-database.js"></script> <!-- Dołączamy plik css odpowiadający za wygląd naszej strony --> <link rel="stylesheet" href="styles.css"> </head> <body> <h1>Firebase w zastosowaniach IoT</h1> <div id="formularz_logowania"></div> <!-- Miejsce na formularz logowania --> <div> <!-- Miejsca na informacje o diodzie, temperaturze i wilgotności --> <div id="panel-led" class="panel"> Stan diody: <span id="wartosc-led">[stan]</span> <button id="przycisk-led">Zmień</button> </div> <div id="panel-temperatura" class="panel"> Temperatura: <b><span id="wartosc-temperatura">[wartość]</span></b> (Ostatni pomiar: <span id="czas-temperatura">[czas]</span>) </div> <div id="panel-wilgotnosc" class="panel"></div> </div> <script> // Konfigurujemy dane dostępowe do Firebase var config = { apiKey: "xxxxxxxxx", authDomain: "xxxxx.firebaseapp.com", databaseURL: "https://xxxxx-default-rtdb.firebaseio.com", projectId: "xxxxx", storageBucket: "xxxxx.appspot.com", messagingSenderId: "xxxxxxxx", appId: "xxxxxxxxxxxxxxxxxxxx" }; firebase.initializeApp(config); // Inicjalizujemy Auth const auth = firebase.auth(); // Inicjalizujemy RTDB const rtdb = firebase.database(); </script> <!-- Dołączamy nasze pliki .js --> <script src="login.js"></script> <script src="temperatura.js"></script> <script src="wilgotnosc.js"></script> <script src="led.js"></script> </body> </html> Przygotowałem prosty plik .css, który zadba o trochę ładniejszy wygląd naszej strony (nie jestem mistrzem designu 🙃). .panel { width: 90%; padding: 5px; margin: 5px; border: 3px solid gray; float: left; border-radius: 15px; } .kafelek-wilgotnosc { width: 90%; padding: 5px; margin: 5px; border: 3px solid gray; float: left; border-radius: 15px; } Teraz zajmijmy się mózgiem naszej strony, czyli kodem js. Dla lepszej czytelności postanowiłem, że stworzymy 4 pliki, ale równie dobrze wszystko mogłoby się znaleźć w jednym. Są to: login.js - obsługa funkcji logowania i wylogowywania użytkownika temperatura.js - nasłuchiwanie i aktualizowanie wartości temperatury wilgotnosc.js - nasłuchiwanie i aktualizowanie wartości wilgotności led.js - nasłuchiwanie i aktualizowanie stanu diody, obsługa przycisku LOGIN W pliku login.js umieścimy logikę odpowiedzialną za logowanie i wylogowywanie użytkownika. Na początku sprawdzamy czy użytkownik jest już zalogowany - jeśli tak to w miejscu o id="formularz_logowania" wstawiamy przycisk służący do wylogowania, jeśli nie to wstawiamy tam formularz do logowania. Wykorzystamy do tego poznaną w III części kursu funkcję auth.onAuthStateChanged(). Użytkownik zalogowany / niezalogowany. Gdy zalogowany użytkownik kliknie przycisk “Wyloguj się” zostanie wywołana funkcja auth.signOut() oraz wypisany alert "Wylogowano pomyślnie". Wtedy zadziała także funkcja auth.onAuthStateChanged() wewnątrz pozostałych plików js, która zmieni wartości diody, temperatury i wilgotności na pusty tekst "", a przycisk “Wyloguj się” na formularz logowania. Gdy niezalogowany użytkownik uzupełni i zatwierdzi formularz, zostanie wywołana funkcja auth.signInWithEmailAndPassword(email, haslo), która spróbuje zalogować użytkownika podanymi danymi. Jeśli się to uda zostanie wyświetlony alert “Pomyślnie zalogowano”. Wtedy zadziała także auth.onAuthStateChanged() wewnątrz pozostałych plików js, która zacznie pobierać i wyświetlać wartości z bazy danych, a formularz logowania zamieni na przycisk “Wyloguj się”. Jeśli przy próbie logowania wystąpi błąd to zostanie on wypisany użytkownikowi. Cały kod wygląda następująco: const uchwyt_formularz_logowania = document.querySelector('#formularz_logowania'); // uchwyt do miejsca na formularz logowania auth.onAuthStateChanged(user => { if (user) { //zalogowany // zamiast formularza logowania umieszczamy na stronie przycisk do wylogowania uchwyt_formularz_logowania.innerHTML = '<button id="wyloguj-button">Wyloguj się</button>'; const wyloguj_button = document.querySelector('#wyloguj-button'); // dodajemy funkcję wywoływaną po naciśnięciu przycisku wyloguj_button.addEventListener('click', (e) =>{ e.preventDefault(); auth.signOut().then(() => {alert("Wylogowano pomyślnie.")}) }); } else { //wylogowany //umieszczamy formularz logowania na stronie uchwyt_formularz_logowania.innerHTML =`<form id="formularz"> <input type="email" name="email" placeholder="email"> <input type="password" name="haslo" placeholder="haslo"> <button>Zaloguj</button> </form>`; const formularz = document.querySelector('#formularz'); // dodajemy funkcję wywoływaną po zatwierdzeniu formularza formularz.addEventListener('submit', (e)=>{ e.preventDefault(); //odczytujemy dane wpisane przez użytkownika const email = formularz['email'].value; const haslo = formularz['haslo'].value; auth.signInWithEmailAndPassword(email, haslo).then((cred) =>{ alert("Pomyślnie zalogowano"); }).catch(function(error) { // jeśli coś się nie udało to sprawdzamy powód var errorCode = error.code; var errorMessage = error.message; if (errorCode === 'auth/wrong-password') { alert('Hasło nieprawidłowe!'); } else if (errorCode === 'auth/user-not-found') { alert('Nie ma takiego użytkownika!'); } else if (errorCode === 'auth/invalid-email') { alert('Wpisz poprawny adres email!'); } else { alert(errorMessage); } }); }); } }) TEMPERATURA Analogicznie jak w przypadku login.js, w temperatura.js zawartość widziana przez użytkownika będzie zależeć od jego stanu zalogowania. Jeżeli użytkownik jest wylogowany to w miejscach o id “wartosc-temperatura” i “czas-temperatura” będzie pustka “”. Jeśli jest zalogowany to uruchamiamy nasłuchiwanie zmian temperatury w bazie danych za pomocą funkcji on(). Po wykryciu zmiany zamieniamy wyświetlone wartości “wartosc-temperatura” i “czas-temperatura” na nowe. Cały kod wygląda następująco: const uchwyt_wartosc_temp = document.querySelector('#wartosc-temperatura'); const uchwyt_czas_temp = document.querySelector('#czas-temperatura'); auth.onAuthStateChanged(user => { if (user) { //zalogowany // nasłuchujemy zmian rtdb.ref('temperatura').on('value', (snapshot) => { uchwyt_wartosc_temp.textContent = snapshot.val()['wartosc']; // wypisujemy na stronie nową wartość temperatury var d = new Date(); d.setTime(snapshot.val()['czas']); uchwyt_czas_temp.textContent = d.toLocaleString("pl-PL"); // wypisujemy datę/czas w polskim formacie }); } else //wylogowany - pokazujemy pusty szkielet strony - bez danych { uchwyt_wartosc_temp.textContent = ''; uchwyt_czas_temp.textContent = ''; } }); WILGOTNOŚĆ W wilgotnosc.js nasłuchujemy zmian wilgotności analogicznie do temperatura.js, ale zamiast jednego wyniku odczytujemy ostatnie 4 pomiary i dla każdego z nich tworzymy kafelek, który umieszczamy w miejsce starych kafelków na liście o id “panel-wilgotnosc”. Kod: const uchwyt_panel_wilgotnosc = document.querySelector('#panel-wilgotnosc'); auth.onAuthStateChanged(user => { if (user) { //zalogowany //funkcja tworząca kafelek z wartością wilgotności oraz czasem pomiaru i umieszczająca go na liście function dodajWilgotnosc(wartosc, czas){ // tworzymy kafelek let kafelek = document.createElement('div'); kafelek.className = "kafelek-wilgotnosc"; //tworzymy elementy kafelka let elWartosc = document.createElement('div'); let elCzas = document.createElement('div'); // zapisujemy odczytaną z bazy wartość wilgotności do kafelka elWartosc.textContent = wartosc; // zapisujemy czas pomiaru do kafelka var d = new Date(); d.setTime(czas); elCzas.textContent = d.toLocaleString("pl-PL"); // dodajemy elementy do kafelka kafelek.appendChild(elWartosc); kafelek.appendChild(elCzas); // dodajemy kafelek na górze listy pomiarów uchwyt_panel_wilgotnosc.insertBefore(kafelek, uchwyt_panel_wilgotnosc.childNodes[0]); } // nasłuchiwanie zmian/nowych pomiarów rtdb.ref('wilgotnosc').orderByKey().limitToLast(4).on('value', (snapshot) => { //sortujemy po id wpisu i wybieramy ostatnie 4 (najnowsze) uchwyt_panel_wilgotnosc.textContent=''; for (const pomiar in snapshot.val()) { dodajWilgotnosc(snapshot.val()[pomiar]['wartosc'], snapshot.val()[pomiar]['czas']); } }); } else //wylogowany - pokazujemy pusty szkielet strony - bez danych uchwyt_panel_wilgotnosc.textContent=''; }); LED W led.js nasłuchujemy zmian stanu diody analogicznie jak temperatury. Oprócz tego kodujemy obsługę wciśnięcia przycisku. Aktualny stan diody przechowujemy w zmiennej stan i po wciśnięciu przycisku zapisujemy do bazy stan przeciwny niż aktualny za pomocą funkcji update(). Kod: const uchwyt_wartosc_led = document.querySelector('#wartosc-led'); const uchwyt_przycisk_led = document.querySelector('#przycisk-led'); auth.onAuthStateChanged(user => { if (user) { //zalogowany var stan = false; // nasłuchujemy zmian stanu diody rtdb.ref('led').on('value', (snapshot) => { uchwyt_wartosc_led.textContent = snapshot.val(); //wypisujemy na stronie nowy stan diody stan = snapshot.val(); }); // funkcja wywoływana po kliknięciu przycisku function zmienLed() { if(stan == true || stan == 1) { var json = { led : false }; rtdb.ref('/').update(json); } else { var json = { led : true }; rtdb.ref('/').update(json); } } uchwyt_przycisk_led.onclick = zmienLed; } else //wylogowany - pokazujemy pusty szkielet strony - bez danych { uchwyt_wartosc_led.textContent=''; } }); DZIAŁANIE Nasza strona jest już gotowa. Możemy ją przetestować otwierając plik index.html w przeglądarce. HOSTING Nasza strona działa, ale jest dostępna tylko z poziomu naszego komputera. Takie rozwiązanie nas nie zadowala, bo chcielibyśmy sterować naszym system z dowolnego urządzenia z dowolnego miejsca na świecie. Aby było to możliwe musimy zapewnić naszemu projektowi hosting. Możemy stworzyć własny domowy hosting (np. na Raspberry Pi), skorzystać z hostingu innej firmy lub wykorzystać Hosting Firebase. Tak jak całe Firebase jest on dostępny za darmo w ramach limitów. Aby rozpocząć, przechodzimy do zakładki Hosting w konsoli Firebase i klikamy “Get started”. Wyświetli się nam instrukcja jak krok po kroku umieścić naszą stronę na serwerze. Na początku musimy zainstalować na komputerze narzędzia Firebase wpisując w wierszu poleceń: npm install -g firebase-tools Następnie w wierszu poleceń przechodzimy do pliku na komputerze, gdzie chcemy zainicjalizować nasz projekt. Działa to analogicznie do tworzenia repozytoriów Gita. W Windowsie służy do tego polecenie: cd "ścieżka_do_pliku" Następnie musimy się zalogować kontem google, na którym mamy projekt Firebase za pomocą polecenia: firebase login Gdy już jesteśmy zalogowani inicjujemy projekt wpisując: firebase init Naszym oczom powinien ukazać się piękny napis FIREBASE i zaczną wyskakiwać pytania. Spokojnie, co prawda będzie dużo pytań, ale nie jest to egzamin, więc nie musisz się stresować 😉 . W pierwszym pytaniu Firebase zapyta się nas czy na pewno chcemy w tym miejscu zainicjować projekt. Wpisujemy Yes. Pierwsze pytanie. Odpowiedź: Yes. Następnie musimy zaznaczyć, z których usług Firebase korzystamy w tym projekcie. Wybieramy Database i Hosting. Drugie pytanie. W kolejnym kroku wybieramy, z którym projektem w konsoli Firebase ma być powiązany projekt, który inicjalizujemy na komputerze. My wybieramy istniejący projekt, który wcześniej utworzyliśmy. Wybór istniejącego projektu. Następne dwa pytania dotyczą zasad bezpieczeństwa oraz nazwy publicznego katalogu projektu. Zostawiamy wartości domyślne klikając enter. Kolejne pytanie (już 6?) dotyczy formy naszej aplikacji webowej. Musimy zdecydować, czy ma to być aplikacja z tylko jedną podstroną (/index.html), czy z większą ilością podstron. W naszym wypadku mamy tylko jeden plik html, więc możemy wybrać Y (choć nie ma to większego znaczenia). Wpisujemy Y. Ostatnie (nareszcie) pytanie dotyczy integracji z GitHubem. My na razie tego nie chcemy, więc wybieramy N. Udało się! Właśnie zainicjalizowaliśmy katalog z projektem Firebase na naszym komputerze. Po wejściu w folder, który wybraliśmy do przechowywania naszego projektu, możemy zobaczyć kilka plików oraz folder public. Właśnie ten folder interesuje nas najbardziej, bo wewnątrz public będą przechowywane wszystkie pliki naszej strony. Automatycznie utworzone pliki. Wewnątrz public usuwamy automatycznie utworzony plik index.html i przenosimy w to miejsce pliki tworzące naszą stronę: index.html styles.css temperatura.js wilgotnosc.js login.js led.js Zawartość katalogu public. Zanim wyślemy naszą stronę w świat, możemy podejrzeć jak będzie wyglądała. W tym celu w wierszu poleceń, z ustawioną ścieżką naszego projektu, wpisujemy: firebase serve Możemy podejrzeć naszą stronę wpisując w przeglądarce: http://localhost:5000 Podgląd strony przed wrzuceniem do sieci. Jeżeli wszystko się zgadza, możemy wrzucić naszą stronę na serwer poleceniem: firebase deploy Jeżeli wszystko się uda, w konsoli powinniśmy zobaczyć adres url, pod którym jest dostępna nasza strona. Domyślnie jest to: https://id-projektu.web.app Udało się! Nasza strona jest już dostępna publicznie 🙂 Możemy zarządzać kolejnymi wersjami naszej strony z poziomu konsoli Firebase. Konsola Firebase. Gotowe! Nasza strona jest już dostępna publicznie. Na razie jest ona bardzo prosta, ale istnieje wiele możliwości jej rozwoju np.: personalizacja wyników dla różnych użytkowników logowanie za pomocą mediów społecznościowych poprawienie designu kolory kafelków zależne od wartości temperatury i wilgotności Teraz zostało nam tylko umieścić czujnik wilgotności w doniczce i nasz pierwszy projekt Firebase jest gotowy 🙂 PODSUMOWANIE Tym optymistycznym akcentem kończymy naszą wycieczkę do krainy Firebase. To była ostatnia część naszego kursu, w której podsumowaliśmy zdobytą wiedzę i nauczyliśmy się korzystać z Hostingu Firebase. Mam nadzieję, że chociaż trochę przybliżyłem Ci temat Firebase. Jest to wspaniałe narzędzie, które daje mnóstwo możliwości. Zachęcam do samodzielnego zgłębienia tematu, bo kwestie, które poruszyłem są tylko wierzchołkiem góry lodowej. PS. Jeśli masz pomysł do czego można zastosować Firebase, koniecznie podziel się nim w komentarzu 🙂 Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową #4 - Projekt praktyczny, Hosting (właśnie to czytasz)
  8. Cześć! Chciałbym podzielić się z Wami projektem rozwijanym przeze mnie równolegle do AgeBota (który zresztą na tejże platformie bazuje) o nazwie Veides. A co to? Głównym celem Veidesa jest umożliwienie każdemu korzystania z dobrodziejstw Sztucznej Inteligencji bez konieczności posiadania zaawansowanej wiedzy na temat działania poszczególnych algorytmów z tej dziedziny, co pozwala skupić się na tworzeniu rozwiązań, a nie rozwiązywaniu problemów. A do czego to? Jednym z założeń Veidesa jest to, aby mógł współpracować z dowolnymi urządzeniami (robotami, robotycznymi ramionami, wszelkimi DIY, Smart Home), którym przyda się trochę inteligencji. Nie ma wymogu posiadania warstwy sprzętowej, więc wszystko zależy od fantazji. Wystarczy połączenie z internetem. A co to potrafi? Jednym z głównych elementów systemu są tzw. przepływy. Określają one wysokopoziomowe zachowanie Waszego robota, urządzenia czy oprogramowania (dalej "agent"), a definiuje się je za pomocą graficznego edytora w konsoli: Przepływy można zdefiniować jako bazę wiedzy każdego agenta, która potem ewoluuje w zależności od środowiska, w jakim agent się znajduje czy w jakie interakcje z nim wchodzi. Uczy się, trochę jak dziecko 🙂 Idąc nieco w przód, wymarzyłem sobie świat, w którym to robot przyrządza i podaje mi rano kawę. Przygotowanie kawy dla nas jest czymś względnie prostym, natomiast dla robota już niekoniecznie. Z jego punktu widzenia jest to dosyć skomplikowana sekwencja czynności do wykonania, gdyż musi posiadać wiedzę na temat środowiska zaczynając od tego gdzie jest czajnik czy ekspres, przez to gdzie są kubki (oraz czy mam jakieś preferencje co do kubka) i ile czasu kawę należy parzyć (jeśli użyje czajnika). Być może znacząco uprościłem, ale myślę, że to oddaje obraz złożoności tego zadania. Jeśli dołożymy zmieniające się, dynamiczne środowisko gdzie ktoś może przestawić czajnik/ekspres lub na drodze do nich pojawi się niespodziewana przeszkoda, to jeszcze bardziej zwiększamy poziom skomplikowania. I tutaj wkracza Veides. Rolą platformy w tym wszystkim jest koordynacja wykorzystywania zgromadzonej wiedzy oraz zdobywania nowej. Nasze zadanie to - w przypadku robota - reagowanie w pożądany sposób "sprzętem" na przychodzące akcje, czyli konkretne instrukcje do zrobienia: Jedź prosto 3 metry Obróć się w lewo o 30 stopni itd... Analogicznie w przypadku innych urządzeń i oprogramowania. Dzięki systemowi możemy lepiej skupić się na tym co chcemy osiągnąć. Powyższą wizję powoli wdrażam. Na początek AgeBot jest w stanie uczyć się pewnych zachowań, może także zapamiętać twarze i przypisać do nich imiona oraz oczywiście posiada wiele innych możliwości, np. reagowanie na krytyczny stan baterii - w czym pomaga Veides. Następnym modułem systemu jest wbudowany czat tekstowy, dzięki któremu mamy możliwość komunikowania się z agentem za pomocą języka naturalnego. Komunikacja jest dwustronna, tzn. można ten kanał wykorzystać do wydawania poleceń ("Skocz po piłkę") oraz do tego, aby agent informował nas o ważnych zdarzeniach ("Chodźcie, Adaś skacze!"). Istnieje także możliwość "poproszenia" o kilka zadań, które wykonają się sekwencyjnie, dodatkowo można te zadania "odkładać" na później (przykład w tym filmie) czy ustawić sobie budzik. Polecenia nie muszą być wykonywane natychmiast. Jednym z kolejnych kroków w rozwoju modułu komunikacji będzie m.in. komunikacja głosowa. Kolejny moduł to kokpit, w którym możemy na żywo obserwować działanie agentów dostosowane do naszych potrzeb: A jak się tego używa? Do projektu spisana jest dokumentacja zawierająca najważniejsze informacje (m.in. tutorial jak zacząć), które w miarę możliwości uzupełniam. Zarówno dokumentacja, konsola oraz moduł czatu wspierają obecnie jedynie język angielski. Veides dostarcza SDK dla Pythona, C oraz dla aplikacji ROS paczkę zawierającą node (gotowy przykład znajduje się na GitHubie). Bilbioteki SDK dla innych technologii (NodeJS, Java (również w kontekście Androida), Arduino oraz ROS2) zostaną zaimplementowane w niedalekiej przyszłości, jeśli będzie taka potrzeba. A kiedy to? Veides jest aktualnie w fazie alfa, jednak minimalna ilość funkcjonalności jest już dostępna niepublicznie. Użytkownikom forum chcącym sprawdzić jak Veides działa w praktyce, chętnie dam dostęp do systemu na miesiąc (poproszę zgłosić się w wiadomości prywatnej), a jeśli projekt się spodoba, po miesiącu zastanowimy się co dalej 🙂 W zamian jedyne o co bym prosił to podzielenie się opinią o projekcie. Takie możliwości jak nauka zachowań czy twarzy na razie są dostępne tylko dla AgeBota, ale oczywiście zostanią wraz z rozwojem platformy udostępnione. A co dalej? Projekt rozwijam systematycznie w ramach hobby, co sprawia mi mnóstwo frajdy. Mam wiele pomysłów bezpośrednio dotyczących rozwoju AgeBota jak i tych dotyczących ekosystemu Veidesa, które powoli wcielam w życie. Na razie konkretnych dat nie jestem w stanie podać, ale na pewno w tym wątku będę Was na bieżąco informował o wszelkich postępach. Zachęcam także do śledzenia wątku AgeBot. Jeśli macie jakieś pytania, opinie, wątpliwości - zapraszam do dyskusji 🙂
  9. Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową #4 - Projekt praktyczny, Hosting Wiemy już czym jest Firebase oraz jak połączyć go z urządzeniami bazującymi na ESP. Teraz zajmiemy się kontaktem z użytkownikiem. Dowiemy się jak zintegrować Auth, RTDB i Firestore ze stroną internetową, tworząc interfejs dla użytkownika. UWAGA: Do pełnego zrozumienia tej część kursy potrzebna jest podstawowa znajomość html i js. Naszym celem jest zapoznanie z Firebase, więc nie będziemy zwracać szczególnej uwagi na pełną poprawność kodów html i js. W internecie jest dostępnych wiele kursów na ten temat i myślę, że każdy zainteresowany znajdzie odpowiedni dla siebie. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » KONFIGURACJA W FIREBASE Na początku musimy dodać aplikację webową do naszego projektu. W tym celu przechodzimy do strony głównej projektu w Firebase i klikamy Dodaj aplikację -> Aplikacja sieciowa oraz nadajemy nazwę naszej aplikacji. Dodawanie nowej aplikacji sieciowej. W następnym kroku wyświetlą się nam dane, których będziemy potrzebować do łączenia plików strony internetowej z Firebase. Widok po dodaniu aplikacji. W każdej chwili możemy też je znaleźć przechodząc do ustawień projektu. Widok danych dostępowych w ustawieniach projektu. ZACZYNAMY! Na początku tworzymy plik html i umieszczamy w nim podstawowy szablon. <html> <head> </head> <body> </body> </html> Między znacznikami <head> </head> dołączamy skrypt do obsługi Firebase oraz skrypty do konkretnych usług, których będziemy używać. Wersję 8.2.9 w poniższym kodzie można zamienić na nowszą. // Dołączenie skryptu do obsługi Firebase <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-app.js"></script> // Dołączenie skryptów dla konkretnych usług <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-firestore.js"></script> // Firestore <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-auth.js"></script> // Auth <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-database.js"></script> // RTDB Jak widać jest to kilka początkowych linijek ze skopiowanych wcześniej danych dostępowych. W tym przykładzie dołączyliśmy Firestore, Auth i RTDB. Sposób dołączania innych usług jest opisany w dokumentacji. Następnie wewnątrz znaczników <body></body> wstawiamy skrypt <script></script> z danymi naszego projektu: var firebaseConfig = { apiKey: "xxxxxxxxxxxxx", authDomain: "xxxx.firebaseapp.com", databaseURL: "https://xxxx-default-rtdb.firebaseio.com", projectId: "xxxx", storageBucket: "xxxx.appspot.com", messagingSenderId: "1234567890", appId: "xxxxxxxxxxxxx" }; Tuż pod spodem inicjalizujemy Firebase i wybrane usługi: // Inicjalizujemy Firebase firebase.initializeApp(firebaseConfig); // Inicjalizujemy Firestore const db = firebase.firestore(); db.settings({ timestampsInSnapshots: true }); // Inicjalizujemy Auth const auth = firebase.auth(); // Inicjalizujemy RTDB const rtdb = firebase.database(); W kolejnym kroku tworzymy plik JavaScript (.js), w którym będzie część logiczna naszej strony i dołączamy go do pliku html. Cały kod html wygląda następująco: <html> <head> <!-- Dołączamy skrypt do obsługi Firebase --> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-app.js"></script> <!-- Dodajemy skrypty do obsługi poszczególnych produktów https://firebase.google.com/docs/web/setup#available-libraries --> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-firestore.js"></script> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-auth.js"></script> <script src="https://www.gstatic.com/firebasejs/8.2.9/firebase-database.js"></script> </head> <body> <!-- Tutaj jakaś zawartość strony... --> <script> // Wklejamy dane dostępowe do naszego projektu var firebaseConfig = { apiKey: "xxxxxx", authDomain: "xxxxx", databaseURL: "xxxxx", projectId: "xxxxx", storageBucket: "xxxxx", messagingSenderId: "xxxxx", appId: "xxxxx" }; // Inicjalizujemy Firebase firebase.initializeApp(firebaseConfig); // Inicjalizujemy Firestore const db = firebase.firestore(); db.settings({ timestampsInSnapshots: true }); // Inicjalizujemy Auth const auth = firebase.auth(); // Inicjalizujemy RTDB const rtdb = firebase.database(); </script> <!-- Dołączenie naszego pliku .js--> <script src="nazwa.js"></script> </body> </html> AUTH W serwisach internetowych uwierzytelnianie użytkowników jest jedną z podstawowych funkcji. Stworzenie samodzielnie bezpiecznego i sprawnego systemu do zarządzania użytkownikami może być skomplikowane i wymaga dużo pracy. Zaraz zobaczymy jak łatwo da się to zrobić przy użyciu Firebase Auth. Podczas inicjalizacji Auth stworzyliśmy uchwyt do tej usługi o nazwie auth: const auth = firebase.auth(); Z tego powodu wszystkie funkcje, które dotyczą Auth będziemy wywoływać w postaci: auth.nazwaFunkcji() SPRAWDZANIE STANU UŻYTKOWNIKA Na początku nauczymy się sprawdzać czy osoba przeglądająca naszą stronę jest zalogowanym użytkownikiem, czy nie. Służy do tego funkcja onAuthStateChanged(), która wykrywa w czasie rzeczywistym zmianę stanu zalogowania użytkownika. Wewnątrz tej funkcji możemy określić co ma się stać, gdy użytkownik jest zalogowany, a co gdy nie. auth.onAuthStateChanged(user => { if (user) { console.log('ZALOGOWANY: ', user); } else { console.log('NIEZALOGOWANY'); } }) W tym przykładzie user to obiekt przedstawiający naszego użytkownika. Jeżeli użytkownik jest zalogowany to przechowuje informacje o nim, a jeżeli nie jest zalogowany to user wynosi null. Z tego powodu stan zalogowania możemy sprawdzić zwykłą instrukcją warunkową if. Jeżeli użytkownik jest zalogowany to program wypisze w konsoli ZALOGOWANY: dane_użytkownika, a jeżeli nie jest zalogowany to wypisze NIEZALOGOWANY. Oczywiście zamiast tego możemy samodzielnie zdecydować co ma się zadziać np. wywołanie określonej funkcji, przekierowanie na inną stronę itp. Umieszczamy powyższy fragment kodu w pliku .js dołączonym do naszego pliku html i otwieramy go w przeglądarce. Powinna ukazać nam się pusta strona. To dlatego, że nie stworzyliśmy zawartości naszej strony w pliku html, a tylko wypisywaliśmy komunikaty do konsoli. Pusto... Jak się dostać do konsoli? Klikamy prawy przycisk myszy, wybieramy Zbadaj i przechodzimy do zakładki console. Powinniśmy teraz widzieć komunikat wypisany przez nasz program. Widok konsoli. Na razie pojawia się tam NIEZALOGOWANY, ponieważ nie poznaliśmy jeszcze mechanizmu logowania. Zaraz to zmienimy! LOGOWANIE I WYLOGOWYWANIE Do logowania użytkownika za pomocą adresu e-mail i hasła służy funkcja: auth.signInWithEmailAndPassword(email, haslo); Jeżeli dopiszemy to polecenie poniżej polecenia sprawdzającego stan zalogowania (email zamieniamy na “test@test.test”, a hasło na “123456”) i odświeżymy stronę to najpierw otrzymamy komunikat o niezalogowanym użytkowniku, a później o zalogowanym wraz z danymi użytkownika. Widok konsoli - 2 komunikaty. Wynika to z tego, że na początku byliśmy niezalogowani, a zalogowaliśmy się dopiero po sprawdzeniu stanu zalogowania (kod logowania jest poniżej kodu sprawdzania). Jeśli jednak teraz odświeżymy stronę to dostaniemy tylko komunikat o zalogowanym użytkowniku. Wynika to z tego, że przeglądarka zapamiętała nasze zalogowanie i nas nie wylogowała. Widok konsoli - przeglądarka nas pamięta. Jak zatem się wylogować? Służy do tego funkcja: auth.signOut(); Dodajmy ją zatem na końcu naszego kodu i sprawdźmy co się stanie. Widok konsoli. Co tu się wydarzyło!? Chyba nie o to nam chodziło… Najpierw pojawia się komunikat o zalogowanym użytkowniku, co się zgadza, bo byliśmy wcześniej zalogowani. Później komunikat o wylogowaniu, a na końcu znowu o zalogowaniu. Przecież w kodzie umieściliśmy te polecenia w odwrotnej kolejności! Okazuje się, że zalogowanie zajmuje dużo więcej czasu niż wylogowanie, więc wylogowanie dokonało się zanim udało nam się zalogować. Ktoś mógłby się zastanawiać, czy wywołanie funkcji signOut() nie powinno zaczekać na koniec wywołania funkcji signIn(). Otóż nie! Jest to bardzo niebezpieczne zjawisko i może powodować trudne do wykrycia błędy np. gdy postanowimy wyświetlać spersonalizowaną stronę użytkownika i kod ją generujący wywoła się zanim funkcja signIn() skutecznie go zaloguje. Błąd ten da się łatwo naprawić. Wykorzystamy do tego celu funkcję then(), która zapisana po funkcji signIn() zaczeka, aż wykona się ona w całości i dopiero pozwoli na wywołanie funkcji signOut(). Więcej o funkcji then() można dowiedzieć się tutaj. Zamiast signIn i signOut zapisujemy: auth.signInWithEmailAndPassword("test@test.test", "123456").then(function(){auth.signOut();}); Wylogowanie nastąpiło dopiero po zalogowaniu - sukces 😎 Oczywiście nie zawsze użytkownikowi udaje się poprawnie zalogować. Jeżeli przy logowaniu wystąpi błąd, to chcielibyśmy wiedzieć z czego on wynika. Wystarczy, że na końcu naszej funkcji logowania dopiszemy: .catch(function(error) {console.log(error);}); Teraz jeżeli nastąpi błąd podczas logowania to zostanie on wypisany w konsoli. Przykład błędu po podaniu nieprawidłowego hasła. Oprócz wypisania w konsoli całego błędu możemy odczytać tylko jego kod za pomocą error.code i na tej podstawie informować użytkownika o błędzie. Najczęstsze kody błędów to: auth/wrong-password - niepoprawne hasło auth/user-not-found - nie ma takiego użytkownika auth/invalid-email - niepoprawny adres e-mail DODAWANIE UŻYTKOWNIKÓW Z poziomu aplikacji webowej możemy też tworzyć konta nowym użytkownikom. Służy do tego funkcja: auth.createUserWithEmailAndPassword(email, hasło) Po stworzeniu konta użytkownika jest on automatycznie zalogowany. Polecam lekturę dokumentacji, gdzie dokładniej są opisane wszystkie możliwości Auth. RTDB Teraz zajmiemy się obsługą Realtime Database w JavaScript. Podczas inicjalizacji RTDB stworzyliśmy uchwyt do tej usługi o nazwie rtdb. Analogicznie do Auth, wszystkie funkcje dotyczące RTDB będą miały postać: rtdb.nazwaFunkcji(); W części I tego kursu ustawiliśmy reguły RTDB w taki sposób, aby do odczytu i zapisu danych wymagane było zalogowanie. Z tego powodu funkcje operujące na RTDB powinny zostać wywołane po funkcji logowania lub po funkcji sprawdzającej stan zalogowania, a najlepiej wewnątrz then(). POJEDYNCZY ODCZYT Do jednorazowego odczytu danych z bazy służy funkcja get(). Musimy ją poprzedzić funkcją ref() wskazującą na gałąź, którą chcemy odczytać: rtdb.ref('ścieżka').get() //np. rtdb.ref('led/led1').get() Aby uzyskać dostęp do odczytanych danych musimy jeszcze dopisać: .then(function(snapshot) { //treść funkcji }); Dane odczytane z bazy znajdują się wewnątrz obiektu snapshot. Teraz wypiszmy do konsoli wartość zapisaną w 'led/led1': auth.signInWithEmailAndPassword("test@test.test", "123456").then(function () { rtdb.ref('led/led1').get().then(function (snapshot) { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("Nie odczytano danych!"); } }).catch(function (error) { //łapanie błędów console.error(error); }); }).catch(function (error) { console.log(error); }); Widok konsoli z wartością ‘led/led1’. Widok konsoli, gdy zmienimy ścieżkę na ‘led’ (w bazie wewnątrz led znajdują się led1 z wartością true i led2 z wartością false). NASŁUCHIWANIE ZMIAN W CZASIE RZECZYWISTYM Funkcja get() odczytuje dane tylko raz - w momencie wywołania. Jednak czasem potrzebujemy na bieżąco dowiadywać się o nowych zmianach w bazie bez odświeżania strony np. na bieżąco śledzić zmiany temperatury. Umożliwia nam to funkcja on(): rtdb.ref('led').on('value', (snapshot) => { console.log(snapshot.val()); //co ma się wykonać po wykryciu zmiany }); Nową wartość w obserwowanej gałęzi możemy odczytać za pomocą snapshot.val(). Widok konsoli. Każda zmiana wartości w gałęzi ‘led’ to nowy komunikat. W takiej postaci funkcja on() reaguje na każdą zmianę wartości we wskazanej gałęzi. Czasami jednak interesuje nas tylko konkretny rodzaj modyfikacji np. tylko dodanie nowej gałęzi lub tylko zmiana istniejącej wartości. W tym celu należy zmienić ‘value’ na: ‘child_added’ - jeżeli interesuje nas tylko dodanie nowej gałęzi. W snapshot znajduje się tylko wartość nowej gałęzi. Po odświeżeniu strony wypisze pojedynczo wartość wszystkich istniejących gałęzi należących do podanej ścieżki np. oddzielnie wartości led1 i led2 dla ścieżki ‘led’. ‘child_changed’ - jeżeli istnieje nas tylko zmiana wartości w istniejących gałęziach. Po odświeżeniu strony nic nie zwraca dopóki jakaś gałąź nie zostanie zmodyfikowana. ‘child_removed’ - jeżeli interesuje nas tylko usunięcie gałęzi. Zwraca wartość usuniętej gałęzi. ZAPIS DANYCH DO BAZY Zapisać dane do RTDB możemy na 3 sposoby, tak jak w przypadku zapisu za pomocą ESP: set() - zapisuje lub zmienia dane w konkretnym miejscu (konkretna ścieżka) np. możemy zmienić temperaturę w /pomiary/czujnik1/temperatura. Metoda ta spowoduje nadpisanie danych w określonej lokalizacji, w tym wszelkich węzłów podrzędnych. Oznacza to, że zapisując tylko wartość temperatury do lokalizacji /pomiary/czujnik1 utracimy zapisaną wartość wilgotności. update() - zmienia dane w konkretnym miejscu bez nadpisywania pozostałych danych. Oznacza to, że zapisując tylko wartość temperatury do lokalizacji /pomiary/czujnik1 zapisana wartość wilgotności pozostanie bez zmian. push() - dodaje nową gałąź do bazy nadając jej unikalne id. Ma to zastosowanie np. jeżeli chcemy gromadzić historię pomiarów (a nie tylko ostatni). Dane do zapisu, które podajemy jako argument tych funkcji, muszą być w postaci JSON. var json = { wilgotnosc : 58 }; // zmienia się tylko wartość wilgotności - temperatura bez zmian rtdb.ref('pomiary/czujnik1').update(json); // tracimy wartość temperatury rtdb.ref('pomiary/czujnik1').set(json); // dodajemy nową gałąź o automatycznie generowanym id rtdb.ref('pomiary').push(json); Widok początkowy. Po wywołaniu update(). Po wywołaniu set(). Po wywołaniu push(). Jeżeli chcemy wiedzieć jakie id dokumentu wygenerowała funkcja push() możemy podczas jej wywołania dopisać .key: console.log(rtdb.ref('pomiary').push(json).key); FIRESTORE Teraz zajmiemy się obsługą Firestore w JavaScript. Wszystkie funkcje dotyczące Firestore będą miały postać: db.nazwaFunkcji(); W Firestore zamiast ref() używamy .collection("nazwa_kolekcji").doc("nazwa_dokumentu"). Dla Firestore także ustawiliśmy reguły bezpieczeństwa, więc także musimy najpierw zalogować użytkownika. POJEDYNCZY ODCZYT Do jednorazowego odczytu danych z bazy służy funkcja get(). Aby odczytać dane z konkretnego dokumentu musimy użyć jej w takiej postaci: db.collection("nazwa").doc("nazwa").get(); //np. db.collection("pomiary").doc("czujnik2").get(); Aby uzyskać dostęp do odczytanych danych musimy jeszcze dopisać: .then((doc) => {//treść funkcji }); Dane odczytane z bazy znajdują się wewnątrz doc. Za pomocą doc.exists możemy sprawdzić czy jakieś dane zostały pobrane z bazy. Aby odczytać dane musimy użyć: doc.data() W ten sposób uzyskujemy wszystkie dane wskazanego dokumentu w postaci json. Kolekcja pomiary, dokument czujnik2 - uzyskaliśmy wartość temperatury i wilgotności. Jeżeli interesuje nas konkretne pole w danym dokumencie musimy po kropce wpisać jego nazwę: doc.data().nazwa_pola Kolekcja pomiary, dokument czujnik2, doc.data().temperatura. Możemy także odczytać wszystkie dokumenty w kolekcji. W tym przypadku doc.data() i doc.data().nazwa_pola wywołujemy oddzielnie dla każdego odczytanego dokumentu: db.collection("pomiary").get().then((querySnapshot) => { querySnapshot.forEach((doc) => { // funkcja wykonująca się dla każdego dokumentu }); }); // np: // wypisanie id oraz zawartości wszystkich dokumentów w kolekcji “pomiary” db.collection("pomiary").get().then((querySnapshot) => { querySnapshot.forEach((doc) => { console.log(doc.id, " => ", doc.data()); }); }); Wypisanie id oraz zawartości wszystkich dokumentów w kolekcji “pomiary”. Istnieje także możliwość filtrowania wyników - zapytanie zwróci tylko dokumenty spełniające określone warunki. W tym celu pomiędzy collection() a get() musimy dopisać klauzulę where(): where("pole", "warunek", “wartość”) //np: where("temperatura", ">", 50) db.collection("pomiary").where("temperatura", ">", 50).get().then(...) where("temperatura", ">", 50) nie zwraca nam dokumentu czujnik2, bo jest w nim zapisana temperatura 34.5 Wszystkie dostępne warunki zapytań, konstruowanie zapytań złożonych oraz sortowanie kolejności zwracanych dokumentów możemy znaleźć w dokumentacji. NASŁUCHIWANIE ZMIAN W CZASIE RZECZYWISTYM Tak samo jak w RTDB, możemy w czasie rzeczywistym reagować na zmiany w bazie. Możemy ustalić co ma się wydarzyć, gdy dokona się jakakolwiek zmiana w podanej kolekcji lub reagować tylko na konkretny typ zmiany (np. dodanie nowego dokumentu lub modyfikacja już istniejącego): db.collection('pomiary').onSnapshot(snapshot => { let changes = snapshot.docChanges(); changes.forEach(change => { //funkcja wykonująca się dla każdego dokumentu, dla którego zaszła zmiana console.log(change.doc.id, " => ", change.doc.data()); // Możemy rozróżniać typy zmian if(change.type == 'added'){ console.log("Dodano"); } if (change.type === "modified") { console.log("Zmodyfikowano"); } }); }); Przykład dla powyższego kodu - wypisanie początkowe wszystkich dokumentów, modyfikacja wilgotności w czujnik2, dodanie czujnik4. ZAPIS DANYCH DO BAZY Podobnie jak w RTDB, w Firebase mamy 3 sposoby zapisu danych do bazy: set, update i add (zamiast push). Funkcja set nadpisuje istniejący lub tworzy nowy dokument o podanej nazwie (wszystkie wcześniejsze dane zapisane w tym dokumencie są tracone): db.collection("pomiary").doc("czujnik5").set({ temperatura: 12.35, wilgotnosc: 97 }) Dodano czujnik5. Ponowne wywołanie set(), ale tym razem tylko z parametrem temperatury, usunie zapisaną wartość temperatury: db.collection("pomiary").doc("czujnik5").set({ temperatura: 42.35 }) Funkcja add() dodaje do określonej kolekcji nowy dokument o automatycznie generowanym id: db.collection("pomiary").add({ temperatura: 32.35, wilgotnosc: 56 }) Funkcja update() aktualizuje istniejący dokument: db.collection("pomiary").doc("czujnik2").update({ temperatura: 37.75, wilgotnosc: 59 }) Zaktualizowane wartości temperatury i wilgotności. Jeżeli podamy do zaktualizowanie nie wszystkie istniejące pola, to pozostałe pola nie zmienią swojej wartości: db.collection("pomiary").doc("czujnik2").update({ wilgotnosc: 63 }) Jeśli podamy jej dokument, który nie istnieje to wyskoczy błąd: db.collection("pomiary").doc("czujnik99").update({ temperatura: 32.35, wilgotnosc: 56 }) (wiel)BŁĄD 🐪 Za to możemy dodać w dokumencie pola, które wcześniej nie istniały: db.collection("pomiary").doc("czujnik2").update({ nieistnieje: true }) Dodaliśmy pole, które wcześniej nie istniało. Dodatkowo update pozwala nam zapisywać czas serwera... db.collection("pomiary").doc("czujnik2").update({ timestamp: firebase.firestore.FieldValue.serverTimestamp() }) Zapisaliśmy czas serwera. … a także inkrementować wartość wybranego pola: db.collection("pomiary").doc("czujnik2").update({ wilgotnosc: firebase.firestore.FieldValue.increment(1) //inkrementacja o 1 - jedynkę można zastąpić dowolną liczbą }) PODSUMOWANIE Jeżeli udało Ci się wytrzymać do tego momentu to gratuluję! W tych trzech częściach starałem się przekazać jak najwięcej informacji na temat Firebase przez co wyszły one bardzo treściwe. Obiecuję, że to już ostatnia tak nudna część 😉. W następnym odcinku bajki połączymy wszystkie zdobyte do tej pory informacje tworząc prosty system oparty na Firebase, który umieścimy na dedykowanym hostingu. Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową (właśnie to czytasz)
  10. Ten artykuł jest częścią serii "Firebase w zastosowaniach IoT" #1 - Czym jest Firebase? Jak zacząć? #2 - Firebase z ESP32 i ESP8266 #3 - Wyświetlanie danych użytkownikowi poprzez stronę internetową #4 - Projekt praktyczny, Hosting Tworząc projekty IoT często musimy przetwarzać dane i mieć do nich dostęp spoza sieci domowej. W zależności od potrzeb konkretnego projektu możemy postawić swój serwer np. na Raspberry Pi lub korzystać z gotowych rozwiązań innych firm, których na rynku jest mnóstwo. Dzisiaj przyjrzymy się propozycji od Google, czyli Firebase. Czym jest Firebase? Firebase to zestaw narzędzi od Google pozwalający w łatwy sposób tworzyć backend aplikacji webowych i mobilnych. Dzięki swojej prostocie, dużym możliwościom i rozbudowanej społeczności, tworzącej biblioteki do obsługi wielu języków programowania, jest także często wykorzystywany w systemach IoT. Firebase to zestaw wielu różnych usług, z których większość przeznaczona jest do profesjonalnego zastosowania przez twórców aplikacji. Dla nas (w kontekście projektów IoT) najważniejsze będą: Cloud Firestore i Realtime Database - bazy danych NoSQL Authentication - narzędzie pozwalające na łatwe zarządzanie logowaniami i użytkownikami Hosting - hosting dla naszego projektu Storage - usługa do przechowywania plików np. zdjęć i filmów Google Firebase pozwala też na integrację z innymi usługami od Google m.in. Google Cloud, gdzie możemy np. analizować dane za pomocą sztucznej inteligencji. Ten artykuł bierze udział w naszym konkursie! 🔥 Na zwycięzców czekają karty podarunkowe Allegro, m.in.: 2000 zł, 1000 zł i 500 zł. Potrafisz napisać podobny poradnik? Opublikuj go na forum i zgłoś się do konkursu! Czekamy na ciekawe teksty związane z elektroniką i programowaniem. Sprawdź szczegóły » Ile to kosztuje? Usługi Firebase są udostępniane w systemie pay as you go. Pozwala to na łatwe skalowanie w zależności od aktualnych potrzeb projektu, bo płacimy tylko za to co wykorzystujemy. Google udostępnia darmowy plan Spark, który w zupełności wystarcza do zastosowań domowych i małych projektów. Jeżeli jednak darmowy limit nam nie starcza, w każdej chwili możemy przejść na płatny plan Blaze. Dokładne limity i ceny, wraz z intuicyjnym kalkulatorem, są opisane w cenniku. Bazy danych w Firestore Firebase daje nam do wyboru dwa rodzaje baz danych (obie NoSQL). Pierwszą z nich jest Realtime Database (RTDB). Jest to starsza usługa, gdzie dane są przechowywane w postaci jednego wielkiego drzewa JSON. Nowszym rozwiązaniem jest Cloud Firestore, gdzie dane przechowywane są w postaci zbioru dokumentów. Pozwala to wykonywać bardziej złożone zapytania na danych i umożliwia lepszą skalowalność projektu. W RTDB dane są przechowywane w postaci drzewa JSON. W Firestore dane przechowywane są w kolekcjach, które podzielone są na dokumenty. To, że RTDB jest starsza nie oznacza, że jest jest gorsza - wszystko zależy od wymagań konkretnego projektu. W dokumentacji znajduje się dokładniejsze porównanie tych baz danych wraz z quizem, który pomoże wybrać odpowiednią bazę do danego projektu. Tworzymy nasz pierwszy projekt Skoro już wiemy czym jest Firebase to nadszedł czas, aby utworzyć nasz pierwszy projekt. W tym celu przechodzimy do konsoli Firebase (wymagane jest zalogowanie kontem Google). Tutaj pokazane są wszystkie nasze dotychczasowe projekty. Klikamy okienko z wielkim plusem i napisem Dodaj projekt. Widok wszystkich naszych projektów w konsoli Firebase. Następnie nadajemy naszemu projektowi nazwę. W kolejnym kroku mamy możliwość podpięcia Google Analytics do naszego projektu, ale my na razie to pominiemy. Tworzenie projektu Firebase. Po utworzeniu naszego projektu pokazuje nam się ekran główny. Gdy mamy gotowy projekt to możemy podpinać do niego wybrane usługi i narzędzia Firebase. Po lewej stronie znajduje się boczny pasek menu, za pomocą którego możemy przejść do wybranych narzędzi. Ekran główny projektu. Na początku podepniemy Realtime Database. W tym celu przechodzimy do karty usługi klikając w menu bocznym. Następnie klikamy Utwórz bazę danych. Widok przed utworzeniem bazy danych. Wybieramy lokalizację serwera dla naszej bazy danych: USA lub Europa (na razie w wersji beta). W następnym kroku ustawiamy reguły zabezpieczeń dostępu do naszej bazy danych. Możemy wybrać tryb blokady (dostęp do zapisu i odczytu jest zablokowany dla wszystkich) lub tryb testowy (dostęp do zapisu i odczytu jest dostępny dla wszystkich użytkowników internetu). My na początku wybieramy tryb testowy. Więcej o regułach zabezpieczeń powiemy sobie później. Tworzenie bazy danych RTDB. I gotowe! Mamy pustą bazę danych i możemy zacząć z niej korzystać. Uzupełnijmy ją przykładowymi danymi - baza przechowująca ostatni pomiar z czujnika temperatury i wilgotności. Dodawanie danych do bazy RTDB. Teraz podepniemy bazę danych Firestore. Postępujemy analogicznie jak przy dodawaniu Realtime Database. W bocznym menu wybieramy Firestore i klikamy Utwórz bazę danych. Następnie wybieramy reguły zabezpieczeń (znowu tryb testowy) i lokalizację serwera. Tworzenie bazy danych Firestore. Gotowe! Uzupełnijmy bazę przykładowymi danymi (zwróćmy uwagę na różnicę w strukturze w porównaniu z analogicznym przykładem dla RTDB). Dodawanie kolekcji. Dodawanie nowego dokumentu w kolekcji. Uwierzytelnianie użytkowników W aplikacjach mobilnych, ale i w systemach IoT, zawsze gdy przechowujemy dane w internecie, ważne jest, aby zabezpieczyć je przed dostępem niepowołanych osób. Nie chcemy przecież, aby sąsiad mógł sterować odkurzaczem w naszym inteligentnym domu. Twórcy Firebase o tym pomyśleli i udostępnili usługę Firebase Authentication (Auth). Pozwala ona na łatwe uwierzytelnianie użytkowników i zarządzanie ich kontami. Umożliwia uwierzytelnianie poprzez zewnętrznego dostawcę (np. Google, Facebook...) lub poprzez e-mail i hasło. Dla tej ostatniej metody obsługuje również wysyłanie wiadomości e-mail o resetowaniu hasła i weryfikacji adresu e-mail. Aby dodać Auth do naszego projektu przechodzimy do zakładki Authentication w bocznym menu i klikamy Rozpocznij. Powinniśmy zostać przeniesieni do sekcji Sign-in method, gdzie widzimy wszystkie dostępne metody uwierzytelniania użytkowników. Aby móc używać danej metody musimy najpierw na nią kliknąć i ją włączyć. My na początku włączymy weryfikację przez e-mail. Dostępne metody uwierzytelniania użytkowników. Brawo! Właśnie udało nam się dodać metodę uwierzytelniania użytkowników poprzez e-mail. Jednak zanim będziemy mogli się zalogować musimy najpierw utworzyć konto użytkownika. Możemy to zrobić umieszczając w kodzie strony/aplikacji formularz do rejestracji lub zrobić to ręcznie z poziomu konsoli. My nie mamy jeszcze strony internetowej, więc skorzystamy z tej drugiej metody. W tym celu przechodzimy do sekcji Users i klikamy Dodaj użytkownika. Wpisujemy adres e-mail oraz hasło dla konta dodawanego użytkownika. Po zatwierdzeniu nowy użytkownik powinien pojawić się na liście. Dodawanie konta naszego pierwszego użytkownika. Zabezpieczenie baz danych Tworząc bazy danych wybraliśmy tryb testowy zabezpieczeń. Oznacza to, że obecnie każdy użytkownik internetu może modyfikować dane w naszych bazach danych, czego chcemy uniknąć (zazwyczaj). Pomocne będą reguły zabezpieczeń baz danych. Możemy ustalić, kto będzie mieć możliwość zapisu i odczytu danych w naszej bazie. Możemy skonfigurować oddzielne reguły zabezpieczeń dla każdej operacji i dla konkretnych części bazy danych np. możemy pozwolić użytkownikowi na dostęp do tych dokumentów, które są nazwane identyfikatorem tego użytkownika. Teraz ograniczymy dostęp do obu naszych baz tylko dla zalogowanych użytkowników. W tym celu przechodzimy do sekcji Reguły odpowiednio dla każdej z baz. RTDB W RTDB reguły mają postać: { "rules": { ".read": "warunek1", //odczytywanie danych z bazy jest możliwe, gdy spełniony jest warunek1 ".write": "warunek2", //zapisywanie danych do bazy jest możliwe, gdy spełniony jest warunek2 } } Domyślnie tryb testowy pozwala na dostęp wszystkim przez 30 dni. Reguła ta wygląda następująco: { "rules": { ".read": "now < 1616108400000", // 2021-3-19 ".write": "now < 1616108400000", // 2021-3-19 } } Reguły możemy dowolnie modyfikować. Teraz zezwolimy na odczyt i zapis w całej bazie tylko zalogowanemu użytkownikowi za pomocą warunku "auth != null": { "rules": { ".read": "auth != null", ".write": "auth != null", } } Działanie wpisanych przez nas warunków możemy testować w środowisku do testowania reguł. Działanie środowiska do testowania reguł. Firestore W Firestore reguły mają postać: rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} //ścieżka dostępu do dokumentu - w tym przykładzie definiujemy identyczne reguły dla wszystkich dokumentów { allow read: if warunek1; //odczytywanie danych z bazy jest możliwe, gdy spełniony jest warunek1 allow write: if warunek2; //zapisywanie danych do bazy jest możliwe, gdy spełniony jest warunek2 } } } Domyślnie tryb testowy pozwala na dostęp wszystkim przez 30 dni. Reguła ta wygląda następująco: rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.time < timestamp.date(2021, 3, 19); } } } Reguły możemy dowolnie modyfikować. Teraz zezwolimy na odczyt i zapis w całej bazie tylko zalogowanemu użytkownikowi za pomocą warunku request.auth != null: rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null; } } } Tak jak w przypadku RTDB działanie wpisanych przez nas warunków możemy testować w środowisku do testowania reguł. Działanie środowiska do testowania reguł. Więcej możliwości ustawienie reguł zabezpieczeń znajduje się w dokumentacji RTDB i dokumentacji Firestore. Podsumowanie W tej części dowiedzieliśmy się czym jest Firebase, jakie typy baz danych oferuje, jak stworzyć projekt Firebase oraz czym jest i jak działa Firebase Authentication. W kolejnych częściach przejdziemy już do zastosowania praktycznego.
  11. Witam, Mam na imię Kamil, poszukuję pasjonatów chętnych do współpracy nad projektem systemu Internetu Rzeczy. Jak wiadomo rynek IOT błyskawicznie się rozwija. Do sieci podłączanych jest coraz więcej urządzeń. Rozwiązania tego typu znajdują coraz to szersze zastosowanie w najróżniejszych gałęziach przemysłu, gospodarki oraz na rynku konsumenckim. Moim celem jest zebranie zespołu pasjonatów w celu stworzenia polskiego rozwiązania systemu IOT. Temat jest bardzo rozległy oraz wymaga szerokiego zakresu wiedzy. Nie jestem w stanie zrealizować projektu samodzielnie, co sprawia, że zamieszczam ten wpis na wielu forach w nadziei, że znajdzie się grupa pasjonatów chętna do wspólnej pracy nad wykonaniem tego przedsięwzięcia. Pierwszym etapem projektu jest skompletowanie zespołu. Do współpracy poszukuję osób zajmujących się hobbystycznie: - Elektroniką oraz projektowaniem PCB, - Programowaniem mikrokontrolerów STM32, - Technologiami przesyłania danych, - Cyberbezpieczeństwem, - Technologiami Chmur obliczeniowych, - Bazami Danych, - Analizą danych, - Aplikacjami internetowymi oraz mobilnymi. Nie poszukuję Profesjonalistów lecz zapalonych Hobbystów, których motywuje ciągła chęć rozwoju oraz zgłębiania wiedzy. Wierzę, że uda nam się skomponować zespół Ludzi pełnych pasji, którzy wspólnymi siłami będą dążyć do osiągnięcia wyznaczonych sobie celów. Praca nad projektem będzie świetną okazją do wzajemnej wymiany wiedzy oraz doświadczenia. Uważam, że nieocenione są możliwości grupy Osób pełnych zaangażowania, które łączy wspólne hobby. Zdaję sobie sprawę, że cała koncepcja jest nieco szalona, aczkolwiek myślę, że warto spróbować. Kto wie co jesteśmy w stanie wspólnie osiągnąć :) . Osoby zainteresowane projektem w celu uzyskania informacji lub jakichkolwiek pytań proszone są o kontakt pod adresem e-mail: kamiliotsystem@gmail.com W tytule wiadomości proszę zamieścić swoje imię oraz obszar zainteresowań
  12. Stacja pogodowa z wysyłką danych na stronę WWW Od paru tygodni testuję stację pogodową którą udało mi się zmontować w ostatnim czasie. Stacja oparta o sterownik Lan Kontroler V2.5 firmy Tiny Control. Jest to kompaktowe rozwiązanie zawierające: 5 wejść analogowych: pomiar temperatury, napięcia i prądu, oraz innych wielkości fizycznych, wejście cyfrowe w standardzie 1wire, wejście cyfrowe do obsługi czujnika temperatury i wilgotności DHT22, 4 wejścia logiczne: jako czujnik stanu do monitoringu, jako licznik impulsów z licznika energii, 1 przekaźnik (NZ, NO, C), 1 wyjście tranzystorowe, 4 wyjścia do załączania przekaźników oraz pomiar temperatury i napięcia zasilania na płytce. Do sterownika podłączyłem następujące czujniki: temperatury/wilgotności/ciśnienia - wszystko w jednym: CZUJNIK BME280/SPLITER 1WIRE/ RJ12 Grove - czujnik opadów / wody DFRobot Gravity - analogowy czujnik wilgotności gleby - odporny na korozję DFRobot Gravity - czujnik światła ultrafioletowego UV analogowy Miernik prędkości wiatru (chiński, zamówiony na aliexpress) 🙂 Podłączenie Urządzenie Lan Controler wymaga podłączenia czujników do odpowiednich wejść analogowych/cyfrowych. Czujnik BME280 podłączany jest do złącza wire1. Po zmontowaniu całość prezentuje się następująco: Oprogramowanie Sterownik Lan Controler posiada wbudowany serwer www i panel zarządzania parametrami sterownika oraz wejściami: dodatkowo posiada możliwość wysyłki danych do serwerów protokołem HTTP, taką opcję zbierania danych umożliwia np. serwer Thing Speak (https://thingspeak.com/) ThingSpeak to aplikacja i interfejs API dla urządzeń IoT (opensourcowe) do przechowywania i pobierania danych z urządzeń elektronicznych za pomocą protokołu HTTP i MQTT. tak wyglądają dane z mojej stacji pogodowej na platformie Thinspeak: dzięki opcji zagnieżdżania poszczególnych widgetów z ThingSpeak na innych stronach, zrobiłem własną zawierającą kluczowe parametry pogodowe: Rozwój: w planie dodatkowy czujnik - przepływu wody, mierzący ilość opadów, planuję oprzeć to o czujnik przepływu YF-S402, natomiast przed zimą czujnik zanieczyszczenia powietrza. Oczywiście wszystkie dane będą publikowane na stronie WWW automatycznie.
  13. Chcielibyście, aby urządzenie z ESP8266 wysyłało do Was e-maile lub powiadomienia push? Może zastanawialiście się, jak połączyć najnowsze DIY z asystentem głosowym od Google? Te pozornie trudne zadania można bardzo łatwo rozwiązać za pomocą popularnego IFTTT! [blog]https://forbot.pl/blog/praktyczny-poradnik-laczenia-esp-z-ifttt-if-this-then-that-id41663[/blog] IFTTT_przyklad_IO.zip
  14. Cześć, przymierzam się do projektu inteligentnego domu, mam w głowie zarys planu tego co chcę osiągnąć, ale ze względu na niewielkie doświadczenie z elektroniką będę bardzo wdzięczny za wszelkie rady i sugestie. Ogólna koncepcja jest taka: Serwer na Raspberry Pi zbierający dane z czujników i wysyłający komendy do elementów wykonawczych. Czujniki i elementy wykonawcze rozproszone po całym mieszkaniu (i poza nim 😉) W pierwszej kolejności chciałbym zacząć od kilku prostych czujników i łączenia się do nich z mojego komputera (na razie bez serwera na malince). Myślałem o tym żeby czujniki wyposażać w esp8266 i łączyć się do nich po HTTP. W przyszłości, jak powstałby serwer na Raspberry Pi, to mógłby odpytywać czujniki i zbierać dane. Jeśli chodzi o zasilanie czujników to myślałem o zasilaniu bateryjnym/akumulatorowym. Tu pojawia się pierwszy problem, bo słyszałem, że esp potrzebuje dość sporo energii do zasilania. Myślicie, że taki układ ma prawo działać przez dłuższy czas? A może zamiast esp powinienem spróbować czegoś innego? Z góry dzięki za wszystkie rady 😉
  15. Po przeczytaniu dwóch poprzednich części znamy już pobieżnie zasady działania HTTP, potrafimy już stworzyć prosty (choć prawdopodobnie użyteczny) serwer. Ale przecież serwer to dopiero połowa - drugą, równie ważną jest klient. I znów będziemy próbować przesyłać między serwerem a klientem dane dotyczące stanu wejść naszego Arduino (czyli najprawdopodobniej jakichś czujników). Ten wpis brał udział konkursie na najlepszy artykuł o elektronice lub programowaniu. Sprawdź wyniki oraz listę wszystkich prac » Partnerem tej edycji konkursu (marzec 2020) był popularny producent obwodów drukowanych, firma PCBWay. Spis treści serii artykułów: Protokół HTTP w zastosowaniach IoT - część 1: trochę teorii Protokół HTTP w zastosowaniach IoT - część 2: budujemy serwer Protokół HTTP w zastosowaniach IoT - część 3: tworzymy klienta I tu uwaga: ponieważ musimy użyć dwóch urządzeń, potrzebne byłyby dwie identyczne płytki. Ponieważ nie każdy ma akurat w szufladzie dwa takie same układy - możemy zasymulować działanie takiego serwera używając naszego komputera. W tym celu w załączniku znajduje się krótki program napisany w Pythonie. Serwer działa na porcie 8001 zamast 80. Gdybyśmy jednak chcieli zmienić to zachowanie, musimy pamiętać, że: na naszym serwerze nie może działać żaden inny serwer na porcie 80; w przypadku Linuksa aby serwer mógł działać na porcie 80, musimy go uruchamiać z konta root (np. przez sudo) - normalny użytkownik nie może uruchamiać serwerów na portach niższych niż 1024. Jeśli chcemy, aby nasz serwer uruchamiał się na inym porcie niż 8001 - po prostu musimy znaleźć linijkę: port = 8001 i zamienić liczbę 8001 na numer portu. Serwer uruchamiamy z konsoli po wejściu do katalogu zawierającego program poleceniem: python3 miniserver.py lub odpowiednikiem dla naszego systemu operacyjnego. Jeśli nasz komputer ma zainstalowanego firewalla, należy zezwolić na dostęp z zewnątrz dla naszego serwera. Ponieważ różne firewalle mają różne metody służące do takowego zezwalania - odsyłam do dokumentacji naszego firewalla. Po uruchomieniu serwera możemy sprawdzić jego działanie wchodząc przeglądarką na adres http://ip_naszego_komputera:8001/ lub http://localhost:8001/ - powinny ukazać się trzy liczby. Jako że nasz komputer nie ma najprawdopodobniej podłączonych żadnych potencjometrów czy przycisków - liczba odpowiadająca potencjometrowi jest po prostu brana z generatora losowego, a klawiszowi zmienia się za każdym wywołaniem. Tyle tytułem przydługiego wstępu, możemy wreszcie zabrać się za tworzenie... klienta HTTP Ponieważ do klienta będą potrzebne takie same płytki jak do serwera, przypominam układy połączeń dla Arduino z shieldem Ethernet oraz płytek ESP3266 i ESP32: I znów jak poprzednio użyjemy wspólnego fragmentu kodu. Będzie on bardzo podobny do kodu używanego przy pisaniu serwera. Jedynymi różnicami są brak deklaracji i uruchomienia serwera oraz zdefiniowanie wielkości bufora (różne dla małego Arduino i większych płytek). Należy pamiętać, że w przypadku Ethernetu musimy zapewnić unikalność adresów MAC! #ifdef AVR // część dla Arduino i Ethernet Shield #include <SPI.h> #include <Ethernet.h> #define POT_PIN A1 #define KEY_PIN A0 byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEE }; #define MyServer EthernetServer #define MyClient EthernetClient #define SERIAL_SPEED 9600 void init_network(void) { Ethernet.begin(mac); while (Ethernet.linkStatus() == LinkOFF) { Serial.println(F("Ethernet cable is not connected.")); delay(500); } // dajmy mu trochę czasu na połączenie delay(1000); Serial.print(F("Adres: ")); Serial.println(Ethernet.localIP()); } #else // część dla ESP #ifdef ESP32 #include <WiFi.h> #define POT_PIN 32 #define KEY_PIN 16 #else #include <ESP8266WiFi.h> #define POT_PIN A0 #define KEY_PIN 4 #endif const char* ssid = "My_SSID"; const char* password = "My_Password"; #define MyServer WiFiServer #define MyClient WiFiClient #define SERIAL_SPEED 115200 void init_network(void) { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Połączono z WiFi, adres: "); Serial.println(WiFi.localIP()); } #endif void setup(void) { Serial.begin(SERIAL_SPEED); #ifdef __AVR_ATmega32U4__ while (!Serial); // potrzebne tylko dla Leonardo/Micro #endif init_network(); pinMode(KEY_PIN, INPUT_PULLUP); } #ifdef AVR #define BUFFER_LENGTH 128 #else #define BUFFER_LENGTH 1024 #endif Pierwszy klient będzie bardzo prosty. Nie musimy na razie uruchamiać naszego serwera, zamiast tego połączymy się z serwerem Google: const char Server[]="www.google.pl"; const int ServerPort = 80; void loop() { MyClient client; if (!client.connect(Server, ServerPort)) { Serial.println(F("Brak połączenia z serwerem")); delay(5000); return; } // pytamy googla o stronę główną client.print(F("GET / HTTP/1.1\r\nHost: www.google.pl\r\nCpnnection: Close\r\n\r\n")); // po prostu wypisujemy na monitorze serial // wszystko co dostaliśmy z serwera while (client.connected()) { if (client.available()) { char c = client.read(); Serial.print(c); } else { delay(5); // poczekamy chwilę aż serwer wyśle coś więcej } } client.stop(); while(1) delay(1); // koniec pracy! } Po uruchomieniu - o ile nasz klient ma dostęp do Internetu - powinien połączyć się z serwerem Google, pobrać zawartość strony głównej i wypisać ją na monitorze Serial: Jak widać, klient bardzo grzecznie pobrał wszystko co mu google wysłał i bez wnikania w szczegóły wyrzucił na wyjście. No nic - od klienta powinniśmy oczekiwać czegoś więcej. Przede wszystkim reakcji na błędy... a już na pewno stwierdzenia, czy nie wystąpił błąd. Spróbujmy więc stworzyć... sprytniejszego klienta HTTP Tym razem będziemy łączyć się z naszym serwerem, więc musimy pamiętać, aby go uruchomić! // Podaj właściwy adres i port serwera IPAddress Server(192,168,1,5); const int ServerPort = 8001; void loop() { MyClient client; char bufor[BUFFER_LENGTH]; if (!client.connect(Server, ServerPort)) { Serial.println(F("Brak połączenia z serwerem")); delay(5000); return; } client.setTimeout(5000); client.print(F("GET / HTTP/1.0\r\n\r\n")); // wczytujemy pierwszą linię odpowiedzi serwera int n = client.readBytesUntil('\n',bufor, BUFFER_LENGTH-1); bufor[n]=0; // uzupełniamy kończące '\0' // teraz po prostu sprawdzimy, czy w buforze znajduje się // string " 200 " - jeśli nie, jest to błąd! if (!strstr(bufor, " 200 ")) { client.stop(); // dalej nie gadamy Serial.print(F("Otrzymano odpowiedź: ")); Serial.println(bufor); delay(10000); // czekamy 10 sekund return; } // możemy pominąć pozostałe nagłówki jako mało interesujące: bool naglowki_wczytane = false; while(client.connected()) { n = client.readBytesUntil('\n',bufor,BUFFER_LENGTH-1); bufor[n]=0; Serial.print("Nagłówek: "); Serial.println(bufor); if (n == 1) { // w buforze jest jeden znak '\r' naglowki_wczytane = true; break; } } if (!naglowki_wczytane) { Serial.println(F("Błąd odczytu nagłówków")); client.stop(); delay(10000); return; } // teraz całą resztę wczytujemy do bufora n=client.readBytes(bufor, BUFFER_LENGTH-1); bufor[n]=0; client.stop(); Serial.print(F("Odpowiedź serwera: ")); Serial.println(bufor); delay(5000); } Trochę tu pooszukiwaliśmy - nie sprawdzamy całej pierwszej linii, ale wystarczy aby linia zawierała napis "<spacja>200<spacja>" - możemy to uznać za potwierdzenie. Tym razem zobaczymy, jak działa serwer w połączenia z dwoma klientami. Po ukazaniu się pierwszej informacji wchodzimy przeglądarką na adres: http://adres_ip_serwera/set/123 Po powrocie do monitora widzimy, że serwer zapamiętał tę liczbę i bardzo grzecznie nam ją przekazuje. A więc już teraz możemy zobaczyć, że serwer może służyć jako pośrednik wymiany danych między dwoma klientami! Jeśli jednak przyjrzymy się uważniej temu, co wypisuje monitor serial, zobaczymy że coś jest nie w porządku. Na wszelki wypadek możemy włączyć opcję "pokaż znacznik czasu" w monitorze i... O właśnie. Między odebraniem ostatniej linii nagłówków a odebraniem właściwych danych mija dokładnie 5 sekund. Czyżby serwer opóźniał w jakiś sposób wysyłanie danych? Nie - serwer wysyła tak jak trzeba. Po prostu dla bezpieczeństwa ustawiliśmy w kodzie: client.timeout(5000); i w związku z tym klient nie jest w stanie stwierdzić, czy serwer rzeczywiście się rozłączył - na wszelki wypadek czekając 5 sekund. Jak temu zaradzić? Otóż na razie korzystaliśmy z najprostszej metody: czytamy z klienta aż do końca. Problematyczna jest tu po prostu klasa Stream i jej metoda read(), która nie potrafi jednoznacznie zasygnalizować czy klient już zakończył połączenie, czy namyśla się nad wysłaniem dalszych danych. A readBytes na wszelki wypadek czeka te 5 sekund zanim zwróci wynik... Co w takiej sytuacji? Teoretycznie można by czytać sobie po znaku i w przypadku braku owego sprawdzać, czy klient się przypadkiem nie rozłączył. Nie byłoby to specjalnie efektywne - poza tym metoda "czytaj do końca" ma jedną zasadniczą wadę: tak naprawdę nie wiemy, z jakich powodów ów koniec nastąpił; być może swerwer wysłał już wszystko co ma do wysłania - a być może jakaś awaria (serwera czy któregoś z pośredniczących routerów) uniemożliwiła mu wysłanie wszystkiego do końca. Na szczęście istnieje na to bardzo prosty sposób. Serwer wysyła nagłówek ContentLength informujący, ile bajtów będzie liczyła właściwa odpowiedź. Klient powinien zanalizować ten nagłówek i po odebraniu tylu bajtów nie czekać więcej, tylko zamykać połączenie, a w przypadku przedwczesnego zakończenia transmicji (czyli odebrania mniejszej ilości bajtów od zadeklarowanej i wykrycia końca transmisji) zasygnalizować błąd. Jeśli korzystamy z serwera w pythonie ma on już wbudowaną taką funkcjonalność. Jeśli natomiast jako serwera używamy drugiego egzemplarza płytki - należy nieco zmodyfikować kod serwera. Nowy fragment kodu będzie wyglądać tak: int pot = analogRead(POT_PIN); int key = digitalRead(KEY_PIN); int length = sprintf(bufor, "%d %d %d\n", pot, key, nasza_zmienna); // wypisujemy nagłówki client.print(F("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length=")); client.print(length); client.print(F("\r\n\r\n")); // teraz jeśli zapytanie było GET wysyłamy przygotowane dane if (!isHead) { client.print(bufor); } a kompletny kod dostępny jest w załączniku. Teraz zajmiemy się klientem. Nie możemu już sobie pozwolić na pominięcie wszystkiego aż do pustej linii - musimy analizować wszystkie nagłówki, bo nie wiadomo w którym (i czy w ogóle) będzie interesująca nas wartość. Przyjmijmy, że jeśli nagłówek ContentLength nie wystąpił - wracamy do poprzedniej metody czytania aż do końca połączenia. A więc stwórzmy teraz... szybszego klienta HTTP Nowy kod wygląda w ten sposób: // Podaj właściwy adres i port serwera IPAddress Server(192,168,1,5); const int ServerPort = 8001; #define LENGTH_UNKNOWN -1 void loop() { MyClient client; char bufor[BUFFER_LENGTH]; if (!client.connect(Server, ServerPort)) { Serial.println(F("Brak połączenia z serwerem")); delay(5000); return; } client.setTimeout(5000); client.print(F("GET / HTTP/1.0\r\n\r\n")); // wczytujemy pierwszą linię odpowiedzi serwera int n = client.readBytesUntil('\n',bufor, BUFFER_LENGTH-1); bufor[n]=0; // uzupełniamy kończące '\0' // teraz po prostu sprawdzimy, czy w buforze znajduje się // string " 200 " - jeśli nie, jest to błąd! if (!strstr(bufor, " 200 ")) { client.stop(); // dalej nie gadamy Serial.print(F("Otrzymano odpowiedź: ")); Serial.println(bufor); delay(10000); // czekamy 10 sekund return; } // wczytujemy po kolei nagłówki, szukając Content-Length int ContentLength = LENGTH_UNKNOWN; bool naglowki_wczytane = false; while(client.connected()) { n = client.readBytesUntil('\n',bufor,BUFFER_LENGTH-1); bufor[n]=0; Serial.print("Nagłówek: "); Serial.println(bufor); if (n == 1) { // w buforze jest jeden znak '\r' naglowki_wczytane = true; break; } if (!strncasecmp(bufor,"content-length:", 15)) { ContentLength = atoi(bufor+15); } } if (!naglowki_wczytane) { Serial.println(F("Błąd odczytu nagłówków")); client.stop(); delay(10000); return; } Serial.print(F("Rozmiar danych: ")); Serial.println(ContentLength); // teraz całą resztę wczytujemy do bufora if (ContentLength > BUFFER_LENGTH -1 || ContentLength == LENGTH_UNKNOWN) { ContentLength = BUFFER_LENGTH - 1; } n=client.readBytes(bufor, ContentLength); bufor[n]=0; client.stop(); if (n < ContentLength) { Serial.println(F("Odpowiedź niekompletna")); } else { Serial.print(F("Odpowiedź serwera: ")); Serial.println(bufor); } delay(5000); } Jak widać nieco się skomplikował. Pewnie nie wymaga objaśnień poza jednym drobiazgiem: Zmienna ContentLength jest zdeklarowana jako int. Nasuwałoby się od razu - dlaczego nie unsigned int? Przecież długość nie może być ujemna... Owszem, moglibyśmy zdeklarować ją jako unsigned. Tyle że wtedy musielibyśmy wprowadzić dodatkową zmienną tylko do tego, aby zasygnalizować wystąpienie tego nagłówka (bo wartość zero jest jak najbardziej prawidłowa). W tym przypadku podstawiamy liczbę ujemną przed czytaniem nagłówków i po ich odczytaniu od razu wiemy: jeśli liczba jest nieujemna mamy tam wielkość przesyłanych danych, w przeciwnym przypadku wielkość jest nieznana. Po uruchomieniu wyniki wyglądają następująco: Jak widać nie ma tu żadnego oczekiwania, wyniki pokazują się natychmiast. No, to już wiemy po co są nagłówki (przynajmniej niektóre). Jak widać - niosą one ze sobą różne informacje: w przypadku klienta - np. o tym, w jakiej postaci chaciałby najchętniej mieć podane dane; w przpadku serwera - w jakiej postaci te dane podano. Ale może czas już na coś konkretnego... w końcu do płytki mamy podłączony jakiś potencjometr i przycisk a dotychczas go nie używaliśmy... A więc naprawmy tę sytuację tworząc... prawdziwego klienta HTTP dla IoT Tym razem nie będziemy jednak wrzucać całego kodu do loop(), stworzymy funkcję, której zadaniem będzie: wysłanie na serwer wartości parametru; odebranie danych z serwera i wypisanie ich na monitorze serial; zwrócenie informacji czy operacja się udała. Ta funkcja powinna być wywołana po wciśnięciu przycisku, a argumentem funkcji niech będzie wartość odczytana z wejścia analogowego. Tym razem musimy skonstruować zapytanie. Wbrew pozorom jest to bardzo proste - za pomocą funkcji sprintf umieszczamy w buforze odpowiedni napis i wysyłamy zawartość bufora na serwer. Nowy kod będzie wyglądać następująco: // Podaj właściwy adres i port serwera IPAddress Server(192,168,1,5); const int ServerPort = 8001; #define LENGTH_UNKNOWN -1 bool zapisz(int dane) { // funkcja zwraca true jeśli udało się zapisać dane // false jeśli wystąpił błąd MyClient client; char bufor[BUFFER_LENGTH]; Serial.print(F("Wartość parametru: ")); Serial.println(dane); if (!client.connect(Server, ServerPort)) { Serial.println(F("Brak połączenia z serwerem")); delay(50); return false; } client.setTimeout(5000); // tworzymy zapytanie do serwera sprintf(bufor,"GET /set/%d HTTP/1.0\r\n\r\n", dane); client.print(bufor); // nie stosujemy Serial.println() gdyż w buforze // są już znaki końca linii Serial.print(F("Wysyłam zapytanie: ")); Serial.print(bufor); // wczytujemy pierwszą linię odpowiedzi serwera int n = client.readBytesUntil('\n',bufor, BUFFER_LENGTH-1); bufor[n]=0; // uzupełniamy kończące '\0' // teraz po prostu sprawdzimy, czy w buforze znajduje się // string " 200 " - jeśli nie, jest to błąd! if (!strstr(bufor, " 200 ")) { client.stop(); // dalej nie gadamy Serial.print(F("Otrzymano odpowiedź: ")); Serial.println(bufor); delay(1000); // czekamy sekundę return false; } // wczytujemy po kolei nagłówki, szukając Content-Length int ContentLength = LENGTH_UNKNOWN; bool naglowki_wczytane = false; while(client.connected()) { n = client.readBytesUntil('\n',bufor,BUFFER_LENGTH-1); bufor[n]=0; Serial.print(F("Nagłówek: ")); Serial.println(bufor); if (n == 1) { // w buforze jest jeden znak '\r' naglowki_wczytane = true; break; } if (!strncasecmp(bufor,"content-length:", 15)) { ContentLength = atoi(bufor+15); } } if (!naglowki_wczytane) { Serial.println(F("Błąd odczytu nagłówków")); client.stop(); delay(1000); return false; } Serial.print(F("Rozmiar danych: ")); Serial.println(ContentLength); // teraz całą resztę wczytujemy do bufora if (ContentLength > BUFFER_LENGTH -1 || ContentLength == LENGTH_UNKNOWN) { ContentLength = BUFFER_LENGTH - 1; } n=client.readBytes(bufor, ContentLength); bufor[n]=0; client.stop(); if (n < ContentLength) { Serial.println(F("Odpowiedź niekompletna")); } else { Serial.print(F("Odpowiedź serwera: ")); Serial.println(bufor); } return true; } void loop() { static int lastKey = digitalRead(KEY_PIN); int key = digitalRead(KEY_PIN); // jeśli klawisz został wciśnięty, wysyłamy wartość // odczytaną z wejścia analogowego na serwer if (lastKey && !key) { int i,pot; pot = analogRead(POT_PIN); for (i=0; i<10; i++) { // więcej niż 10 prób wysłania nie ma sensu if (zapisz(pot)) break; } if (i==10) { Serial.println(F("Nie udało się wysłać danych")); } } lastKey=key; } Po uruchomieniu programu możemy zobaczyć, że każde naciśnięcie przycisku powoduje zmianę wartości podawanej przez serwer: Aby to lepiej zobrazować możemy spróbować podejrzeć co się dzieje na serwerze w czasie rzeczywistym. Jeśli mamy jakiegoś linuksa (prawdopodobnie na maku też to zadziała) możemy wpisać po prostu polecenie: watch -n 5 wget -q -O - http://127.0.0.1:8001 Oczywiście jeśli korzystamy z innego serwera niż nasz pythonowy musimy wpisać zamiast 127.0.0.1:8001 właściwy adres i port. Wykonanie tego polecenia spowoduje, że polecenie wget będzie wykonywane co 5 sekund, a wartość odczytana z serwera będzie wyświetlana na ekranie. W przypadku windowsa nie jest to już takie proste... ale od czego mamy nagłówki serwera? W pliku miniserver.py znajdujemy linię zawierającą: #self.send_header("Refresh", 5) i odkomentowujemy polecenie usuwając znak #: self.send_header("Refresh", 5) i oczywiście restartujemy serwer. Spowoduje to wysłanie dodatkowego nagłówka Refresh: 5 Jeśli teraz wejdziemy na ten adres zwykłą przeglądarką - będzie ona odświeżać wyświetlane wyniki co 5 sekund. Oczywiście zadziała to również na Linuksie i Maku! Niezależnie od metody - możemy teraz zobaczyć jak po wciśnięciu przycisku dane wędrują od naszego klienta poprzez serwer do przeglądarki. I to by było na tyle. Miała być co prawda jeszcze czwarta część poświęcona praktycznym rozwiązaniom - okazało się jednak, że ilość materiału który należałoby omówić wymaga osobnego, kilkuczęściowego artykułu. Toteż na tym na razie musimy poprzestać i pożegnać się do następnego spotkania. Kody wszystkich programów dostępne są w załączniku: klient.zip Spis treści serii artykułów: Protokół HTTP w zastosowaniach IoT - część 1: trochę teorii Protokół HTTP w zastosowaniach IoT - część 2: budujemy serwer Protokół HTTP w zastosowaniach IoT - część 3: tworzymy klienta
  16. Z poprzedniej części mogliśmy się dowiedzieć, co to takiego ten cały HTTP -jak się okazało, nic strasznie skomplikowanego. Dzisiaj zajmiemy się praktyczną stroną - czyli napisaniem najprostszego serwera. Oczywiście - jako że temat traktuje o IoT - serwer będzie udostępniał dane odczytane z jakichś czujników. Już słyszę: a dlaczego serwer a nie klient, przecież klient powinien być łatwiejszy? Ten wpis brał udział konkursie na najlepszy artykuł o elektronice lub programowaniu. Sprawdź wyniki oraz listę wszystkich prac » Partnerem tej edycji konkursu (marzec 2020) był popularny producent obwodów drukowanych, firma PCBWay. A dlatego, że aby sprawdzić działanie klienta, należy mieć serwer a takiego na razie nie mamy. Natomiast klienta HTTP ma tu raczej każdy - począwszy od przeglądarek, poprzez przeróżne wgety i curle kończąc na PuTTY i netcacie. Przyjrzyjmy się więc dokładniej, jak działa. Spis treści serii artykułów: Protokół HTTP w zastosowaniach IoT - część 1: trochę teorii Protokół HTTP w zastosowaniach IoT - część 2: budujemy serwer Protokół HTTP w zastosowaniach IoT - część 3: tworzymy klienta Serwer HTTP W najprostszym przypadku serwer po podłączeniu do sieci po prostu czeka na zgłoszenie klienta, wykonuje jakąś tam funkcję obsługi, rozłącza się z klientem i powraca do czekania. Spróbujmy napisać taki program. Ponieważ dla różnych płytek i modułów łączenia z siecią program może być różny, spróbujmy stworzyć przynajmniej dwa (czyli Arduino z Ethernet Shield i dla ESP8266/ESP32). Zacznijmy od jakichś założeń. A więc: Do płytki podłączony jest potencjometr 10 kΩ i przycisk; Serwer w odpowiedzi na zapytanie podaje wartości odczytu potencjometru i przycisku; Serwer nie sprawdza poprawności zapytania, zakłada że jeśli ktoś coś od niego chce to tylko tego co wypisuje; Serwer wypisuje wartości czystym tekstem, bez zabawy w jakieś HTML-e i podobne wymysły. Najpierw więc przygotujmy wszystko, co nam będzie potrzebne. Przede wszystkim musimy sobie przygotować płytkę i podłączyć do niej potencjometr i przycisk. W przypadku Arduino będzie to wyglądać tak: Podobnie dla ESP8266 i ESP32: Zanim zabierzemy się do pisania programu - kilka słów wstępu. Otóż różne biblioteki (WiFi, Ethernet) operujące na połączeniach sieciowych TCP/IP zawsze mają swoje klasy serwera i klienta, będące pochodnymi ogólnych klas Server i Client. Funkcje obsługi połączeń (spoza tych bibliotek) zawsze wymagają właśnie obiektów tych klas, a nie konkretnych (np. EthernetServer). A co robią takie obiekty? To są po prostu elementy najprostszego połączenia TCP/IP. Obiekt klasy Server nasłuchuje na określonym porcie i w przypadku przychodzącego połączenia tworzy nowy obiekt typu Client, już połączony ze stroną nawiązującą połączenie: EthernetClient client=server.available(); if (client) { // obsługa połączenia } Obiekt klasy Client można również utworzyć ręcznie - i wtedy trzeba mu podać adres i port serwera, z którym ma się połączyć: if (client.connect(ADRES_IP_SERWERA, PORT_SERWERA)) { // rozmowa z serwerem } else { Serial.println("Nie mogę nawiązać połączenia"); } Ponieważ Client dziedziczy po klasie Stream (a Stream po Print) - możemy tu używać wszystkich metod pochodzących z tej klasy (podobnie jak w obiektach klasy Serial) Dobra, wystarczy tego wstępu. Zabierzmy się wreszcie za napisanie prostego serwera. Zacznijmy od tego, co będzie się powtarzać we wszystkich następnych programach, czyli od tych wszystkich beginów, initów i innych powtarzalnych rzeczy. Spróbujmy od razu zrobić to tak, aby jak najmniej ingerować w późniejsze programy czy to przy zmianie płytki (Arduino - ESP), czy przy dalszych eksperymentach z programami (serwer i klient). Taki typowy kod (dla Arduino) wygląda tak: #include <SPI.h> #include <Ethernet.h> #define POT_PIN A1 #define KEY_PIN A0 byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; EthernetServer server(80); void setup() { Serial.begin(9600); while (!Serial); // dla Leonardo Ethernet.begin(mac); while (Ethernet.linkStatus() == LinkOFF) { Serial.println(F("Ethernet cable is not connected.")); delay(500); } // dajmy mu trochę czasu na połączenie delay(1000); Serial.print(F("Adres serwera: ")); Serial.println(Ethernet.localIP()); pinMode(KEY_PIN, INPUT_PULLUP); server.begin(); } Oczywiście jest to najprostszy z możliwych sposobów połączenia z siecią, więcej informacji można znaleźć w dokumentacji i przykładach biblioteki Ethernet. Porównajmy to z kodem dla ESP8266 i ESP32: #ifdef ESP32 #include <WiFi.h> #define POT_PIN 32 #define KEY_PIN 16 #else #include <ESP8266WiFi.h> #define POT_PIN A0 #define KEY_PIN 4 #endif const char* ssid = "My_SSID"; const char* password = "My_PASSWORD"; WiFiServer server(80); void setup(void) { Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Połączono z WiFi, adres serwera: "); Serial.println(WiFi.localIP()); pinMode(KEY_PIN, INPUT_PULLUP); server.begin(); } I znów procedura łąćzenia jest maksymalnie uproszczona - ale nie czas to i nie miejsce na omawianie niuansów różnych rodzajów połączeń:) Jak widać kody dla ESP8266 i ESP32 są na tyle podobne, że mogliśmy użyć wspólnego kodu zmieniając tylko bibliotekę i definicje pinów. Spróbujmy jednak pójść dalej i stworzyć wspólny kod dla Arduino/Ethernet i ESP. #ifdef AVR // część dla Arduino i Ethernet Shield #include <SPI.h> #include <Ethernet.h> #define POT_PIN A1 #define KEY_PIN A0 byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; #define MyServer EthernetServer #define MyClient EthernetClient #define SERIAL_SPEED 9600 void init_network(void) { Ethernet.begin(mac); while (Ethernet.linkStatus() == LinkOFF) { Serial.println(F("Ethernet cable is not connected.")); delay(500); } // dajmy mu trochę czasu na połączenie delay(1000); Serial.print(F("Adres serwera: ")); Serial.println(Ethernet.localIP()); } #else // część dla ESP #ifdef ESP32 #include <WiFi.h> #define POT_PIN 32 #define KEY_PIN 16 #else #include <ESP8266WiFi.h> #define POT_PIN A0 #define KEY_PIN 4 #endif const char* ssid = "My_SSID"; const char* password = "My_PASSWORD"; #define MyServer WiFiServer #define MyClient WiFiClient #define SERIAL_SPEED 115200 void init_network(void) { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Połączono z WiFi, adres serwera: "); Serial.println(WiFi.localIP()); } #endif MyServer server(80); void setup(void) { Serial.begin(SERIAL_SPEED); #ifdef __AVR_ATmega32U4__ while (!Serial); // potrzebne tylko dla Leonardo/Micro #endif init_network(); pinMode(KEY_PIN, INPUT_PULLUP); server.begin(); } Możemy teraz zobaczyć, że: wszystkie zależne od płytki wywołania funkcji inicjalizujących połączenie zostały przeniesione do funkcji init_network(); nazwy klas zależnych od płytki i typu połączenia z siecią zostały "zamaskowane" definicjami MyClient i MyServer W ten sposób pisząc dalej program nie będziemy musieli zawracać sobie głowy różnicami między płytkami, bibliotekami i sposobami połączeń z siecią. I teraz mając wszystko przygotowane, możemy na spokojnie zająć się... pierwszą wersją serwera Kod jest tu banalnie prosty i chyba nie wymaga komentarza. Po połączeniu klienta pobieramy od niego wszystko jak leci nigdzie tego nie zapamiętując, aż do napotkania pustej linii. W tym momencie zakładamy, że klient nic więcej wysyłać nie będzie. W odpowiedzi wysyłamy dwa nagłówki: HTTP/1.1 200 OK - potwierdzenie, że wszystko się udało Content-Type: text/plain - czyli, że odpowiadamy czystym tekstem Po nagłówkach wysyłamy linię zawierającą obie odczytane z wejść wartości i zamykamy połączenie. void loop() { // Sprawdzamy, czy klient się połączył MyClient client = server.available(); if (!client) { // nie połączył się return; } Serial.println(F("Nowe połączenie")); // Czytamy znak po znaku aż do napotkania pustej linii: bool linia_pusta = true; bool naglowki_wczytane = false; while (client.connected()) { if (!client.available()) continue; char c = client.read(); Serial.print(c); if (c == '\n') { // koniec linii if (linia_pusta) { naglowki_wczytane = true; // wczytaliśmy wszystko break; } linia_pusta = true; } else if (c != '\r') { // znak '\r' po prostu pomijamy linia_pusta = false; } } // czy na pewno wszystko wczytaliśmy? if (!naglowki_wczytane) { // klient zniknał Serial.println(); Serial.println(F("Klient rozwiał się we mgle")); } else { // Wypisujemy ważne nagłówki i pustą linię client.print(F("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n")); int pot = analogRead(POT_PIN); int key = digitalRead(KEY_PIN); client.print(pot); client.print(' '); client.println(key); } client.stop(); } Możemy teraz w praktyce przetestować nasz serwer. Możemy do tego użyć przeglądarki, wpisując następujący URL: http://adres_ip_serwera/ My jednak chcielibyśmy obejrzeć całą transmisję, więc znów użyjemy PuTTY lub nc: Jak się przekonaliśmy, napisanie prostego serwera nie jest wcale takie trudne, a nawet taki banalny program może byc użyteczny (np. w czujniku temperatury, odpytywanym periodycznie przez główny komputer w domu). Tym niemniej program może się niespecjalnie podobać. Przede wszystkim - czego byśmy nie wpisali, dostaniemy w odpowiedzi wartości z czujników. Podglądając komunikację widzimy, że przeglądarka wysłała dodatkowy request prosząc o ikonkę serwisu - a takiej przecież nie mamy. Spróbujmy więc ulepszyć nieco nasz serwer, tworząc... ulepszoną wersję serwera Poczyńmy znów pewne założenia: Oprócz potencjometru i przycisku serwer podaje wartość jakiejś zmiennej (nazwijmy ją po prostu nasza_zmienna); Serwer oprócz metody GET obsługuje również HEAD; W przypadku otrzymania nieprawidłowej linii serwer odpowiada komunikatem błędu; Z poziomu przeglądarki możemy ustawić wartość "nasza_zmienna" wpisując URL typu http://adres_ip_serwera/set/wartość; Jeśli serwer otrzyma żądanie /favicon.ico, odpowiada błędem 404 (Not Found). #ifdef AVR #define BUFFER_LENGTH 128 #else #define BUFFER_LENGTH 1024 #endif int nasza_zmienna=0; void error400(MyClient& client) { client.println(F("HTTP/1.1 400 Bad Request\r\n\ Content-type: text/plain\r\n\r\n\ Bad request")); client.stop(); } void error404(MyClient& client) { client.println(F("HTTP/1.1 404 Not Found\r\n\ Content-type: text/plain\r\n\r\n\ Not found")); client.stop(); } void error405(MyClient& client) { client.println(F("HTTP/1.1 405 Method Not Allowed\r\n\ Allow: GET,HEAD\r\n\ Content-type: text/plain\r\n\r\n\ Method not allowed")); client.stop(); } void loop(void) { char bufor[BUFFER_LENGTH]; bool isHead; // Sprawdzamy, czy klient się połączył MyClient client = server.available(); if (!client) { // nie połączył się return; } Serial.println(F("Nowe połączenie")); // dajmy sobie czas na wpisanie czegoś do PuTTY client.setTimeout(20000); // Wczytujemy pierwszą linię int n = client.readBytesUntil('\n',bufor, BUFFER_LENGTH-1); if (!n) { // nic nie wczytano? Serial.println(F("Klient się rozmyślił")); client.stop(); return; } bufor[n] = 0; // dopisujemy '\0' na końcu stringu Serial.println(bufor); // teraz pomijamy resztę nagłówków bool linia_pusta = true; bool naglowki_wczytane = false; while (client.connected()) { if (!client.available()) continue; char c = client.read(); Serial.print(c); if (c == '\n') { // koniec linii if (linia_pusta) { naglowki_wczytane = true; // wczytaliśmy wszystko break; } linia_pusta = true; } else if (c != '\r') { // znak '\r' po prostu pomijamy linia_pusta = false; } } // czy na pewno wszystko wczytaliśmy? if (!naglowki_wczytane) { // klient zniknał Serial.println(); Serial.println(F("Klient rozwiał się we mgle")); client.stop(); return; } char *path=strchr(bufor,' '); if (!path) { error400(client); return; } *path++=0; // wstawiamy zero w miejsce spacji // oddzielającej metodę od ścieżki if (!strcmp(bufor, "GET")) { isHead = false; } else if (!strcmp(bufor, "HEAD")) { isHead = true; } else { error405(client); return; } char *proto = strchr(path, ' '); if (!proto) { error400(client); return; } *proto++=0; // nie przesadzajmy, uwierzmy na słowo że to HTTP :) // nie będziemy sprawdzać co siedzi w proto if (!strcmp(path,"/favicon.ico")) { error404(client); return; } if (!strncmp(path, "/set/", 5)) { nasza_zmienna = atoi(path+5); Serial.print(F("Ustawiamy nową wartość zmiennej: ")); Serial.println(nasza_zmienna); } // wypisujemy nagłówki client.print(F("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n")); // teraz możemy odpowiedzieć trzema wartościami: // potencjometr, przycisk i nasza zmienna if (!isHead) { int pot = analogRead(POT_PIN); int key = digitalRead(KEY_PIN); client.print(pot); client.print(' '); client.print(key); client.print(' '); client.println(nasza_zmienna); } client.stop(); } Kod powinien być zrozumiały bez szczegółowego omawiania każdej linii, być może z dwiema uwagami: dotychczas nie mówiliśmy o metodzie HEAD. Działa ona podobnie jak GET, ale serwer kończy transmisję po wysłaniu nagłówków. zamiast błędu 405 (Method Not Allowed) należałoby użyć 501 (Not Implemented). Sugerowałoby to jednak błąd oprogramowania serwera tymczasem to klient przeznaczony do współpracy z urządzeniami IoT powinien wiedzieć jakie pytanie można zadać serwerowi. Dodatkowo obsługa błędu 405 wymaga wysłania dodatkowego nagłówka informującego klienta, jakich metod może używać. A oto wyniki działania naszego serwera: Widzimy więc, że zgodnie z naszymi założeniami: Metoda HEAD powoduje wysłanie tylko nagłówków; Nieznana metoda powoduje wystąpienie błędu 405 Prośba o /favicon.ico powoduje wystąpienie błędu 404 URI o specialnej postaci: /set/<liczba> powoduje zapisanie liczby w pamięci serwera. Możemy również zauważyć, ze odpowiedź informująca o błędzie ma taką samą postać jak prawidłowa, czyli składa się z głównego nagłówka odpowiedzi (tzw. "status line"), nagłówka typu, ewentualnie dodatkowych nagłówków i odpowiedzi w postaci zrozumiałej dla człowieka. Dodatkowym efektem wysłania 404 w odpowiedzi na favicon.ico jest zapamiętanie tej informacji przez przeglądarkę i nie proszenie o nią za każdym razem. Taki serwer może już służyć bardziej praktycznym celom - np. sterowaniem różnych domowych urządzeń za pomocą przeglądarki. I tyle na dziś. Kody obu programów znajdziemy w załączniku: servers.zip - a w następnej części spróbujemy stworzyć klienta, czyli skomunikować się z naszym serwerem. Spis treści serii artykułów: Protokół HTTP w zastosowaniach IoT - część 1: trochę teorii Protokół HTTP w zastosowaniach IoT - część 2: budujemy serwer Protokół HTTP w zastosowaniach IoT - część 3: tworzymy klienta
  17. Protokół HTTP jest bardzo często uzywany w zastosowaniach IoT. Przede wszystkim decyduje tu prostota implementacji, dostępność bibliotek oraz możliwość współpracy z typowymi serwerami WWW bez konieczności instalacji dodatkowego oprogramowania. Szczególnie ta ostatnia cecha może być szczególnie przydatna z uwagi na ilość darmowych usług hostingowych, których nawet ograniczone parametry pozwalają na stworzenie prostej aplikacji niewielkim nakładem środków i przy użyciu minimalnej wiedzy. Niestety - z tą minimalna wiedzą nie jest już tak słodko. Autorzy popularnych bibliotek zakładają pewne minimum wiedzy u użytkowników i pomijają sprawy dla nich oczywiste. Użytkownicy zaś nie mogąc znaleźć jakichś informacji na temat podstaw - próbują coś zrobić metodą prób i błędów, co nikomu na zdrowie nie wychodzi. Spróbuję więc przybliżyć owo "minimum konieczne", aby nie trzeba było przekopywać się przez dokumenty typu RFC po ty tylko, by przesłać do serwera zmierzoną temperaturę na balkonie. Ten wpis brał udział konkursie na najlepszy artykuł o elektronice lub programowaniu. Sprawdź wyniki oraz listę wszystkich prac » Partnerem tej edycji konkursu (marzec 2020) był popularny producent obwodów drukowanych, firma PCBWay. Spis treści serii artykułów: Protokół HTTP w zastosowaniach IoT - część 1: trochę teorii Protokół HTTP w zastosowaniach IoT - część 2: budujemy serwer Protokół HTTP w zastosowaniach IoT - część 3: tworzymy klienta Zacznijmy jednak od czegoś prostszego, mianowicie od tego... czym jest URL. Każdy z pewnością zna to słowo, i większość z czytelników pewnie rozumie (nawet intuicyjnie) co to takiego. Aby jednak uniknąć jakichkolwiek nieporozumień proponuję zapoznać się z budową URL-a. URL (czyli Uniform Resource Locator) to znormalizowany sposób określania położenia dokumentu (zasobu) w Internecie. Ponieważ dla różnych schematów mogą istnieć różne części, skupmy się wyłącznie na protokole HTTP. Taki URL składa się z następujących części: Schemat - sposób, w jaki nasz dokument będzie przesyłany. W naszym przypadku będzie to zawsze http; Użytkownik - nazwa użytkownika, dla zasobów które mogą być serwowane różnie w zależności od użytkownika. Jeśli nie jest potrzebna, można ją opuścić wraz następujący po niej ewentualnym drukropkiem i hasłem oraz znakiem @; Hasło - jak sama nazwa wkazuje, hasło dostępu do dokumentu. Może wystąpić tylko wtedy, gdy podajemy użytkownika, a jeśli nie jest potrzebne, można je opuścić wraz z poprzedzającym dwukropkiem; Host - nazwa (lub adres IP) komputera, na którym umieszczony jest nasz dokument. Mimo, że dopuszczalne jest podanie w tym miejscu adresu IP, nazwa hosta wymagana jest jeśli serwer hostuje więcej niż jedną stronę www na tym samym adresie IP (czyli praktycznie wszędzie oprócz naszego domowego Arduino czy Raspberry); Port - port TCP, na którym nasłuchuje serwer. Jeśli jest to domyślny port (w przypadku http będzie to port 80) - może być pominięty wraz z poprzedzającym dwukropkiem; Ścieżka - czyli miejsce, gdzie na serwerze znajduje się nasz dokument. Zawsze rozpoczyna się znakiem '/', składa się z członów rozdzielonych znakiem '/' i może zawierać wyłącznie 7-bitowe znaki; Zapytanie - czyli parametr przesłany do skryptu na serwerze, jeśli ścieżka wskazuje na wykonywalny program a nie fizyczny dokument. Jeśli nie jest potrzebne, może być opuszczone wraz z poprzedzającym znakiem '?'. Używając popularnej pochodzącej z BNF notacji można to zapisać tak: <schemat>://[<użytkownik>[:<hasło>]@]<host>[:<port>]<ścieżka>?[<zapytanie>] Już słyszę pytanie: a co z fragmentem? Przecież wpisując adres do przeglądarki możemy zakończyć go znakiem '#' i etykietą informującą, który fragment ma się pokazać na ekranie... Otóż trzeba sobie uświadomić że to, co wpisujemy do przeglądarki nie jest tak naprawdę URL-em. Przeglądarki rozumieją pewien swój zapis - np. brak schematu, narodowe znaczki w ścieżce czy właśnie ten '#' i fragment. Pamiętać należy jednak, że w przypadku braku schematu przeglądarka użyje domyślnego 'http', narodowe znaczki zakoduje w pewien określony sposób a fragmentu w ogóle nie wyśle do serwera, będzie on potrzebny wyłącznie przeglądarce przy wyświetlaniu. Ponieważ dalej będziemy posługiwać się uproszczoną wersją URL-i, przyjmijmy że jego składnia będzie w większości przypadków następująca: http://<host><ścieżka>[?<zapytanie>] Należy tu wspomnieć o czymś jeszcze: o ile składnia URL-a nie narzuca żadnego specjalnego formatowania dla części "zapytanie", przyjęto zunifikowany zapis par <nazwa>=<wartość>, oddzielonych znakami '&'. I tu uwaga: w dalszej części będę posługiwał się również pojęciem URI (Unified Resource Identifier). Nie chcę wnikać w szczegóły, przyjmijmy więc uproszczenie, że URI identyfikuje zasób w obrębie serwera i składa się ze ścieżki oraz opcjonalnego zapytania (czyli de facto stanowi fragment URL-a). Tak więc wiemy już z czego się składa adres w Internecie, spróbujmy przyswoić sobie... podstawy protokołu HTTP Przede wszystkim musimy wiedzieć, że połączenie http składa się zawsze z zapytania wysłanego do serwera i odpowiedzi serwera. I koniec. Nie ma żadnej dalszej komunikacji, szczególnie po wysłaniu odpowiedzi serwer nie będzie pamiętać, o co go poprzednio prosiliśmy. Taki protokół nazywany jest bezstanowym, gdyż każde zapytanie rozpatrywane jest przez serwer niezależnie od innych. Oczywiście - serwer może wprowadzać dodatkowe mechanizmy zapewniające jakieś tam zależności (choćby mechanizm ciastek i sesji), ale nie należą one do protokołu ale do konkretnych wykonywanych na serwerze skryptów. Zacznijmy od zapytania - czyli tego, co klient (np. przeglądarka) przesyła do serwera. Zapytanie składa się z linii zakończonych parą znaków CRLF (czyli w notacji C++ "\r\n") i zawsze rozpoczyna się linią w postaci: <metoda> <URI> <protokół> Nas interesuje na razie metoda GET czyli "pobierz zawartość zasobu wskazanego przez URI" oraz obowiązujący protokół HTTP/1.1. Tak więc początkowa zawartość zapytania pobierająca np. zawartość głównej strony forum Forbota będzie miała postać: GET /forum/ HTTP/1.1 Następne linie stanowią nagłówki zapytania. Może ich być wiele - np. podających język przeglądarki czy rozpoznawane kodowanie, zawsze w postaci: <nazwa>: <wartość> Nas interesuje przede wszystkim nagłówek "Host" podający nazwę serwera. Warto również poinformować serwer, że oczekujemy na zakończenie połączenia po przesłaniu dokumentu, co załatwia nam nagłówek "Connection". Co prawda wartość "Close" jest dla tego nagłówka domyślną, ale warto przyswoić sobie zasadę podawania serwerowi wszystkiego co jest potrzebne do zwrócenia odpowiedzi - i niczego poza tym. Tak więc pełne zapytanie będzie wyglądać tak: GET /forum/ HTTP/1.1 Host: forbot.pl Connection: Close Każde zapytanie musi kończyć się pustą linią, aby poinformować serwer że już skończyły się nagłówki i teraz oczekujemy odpowiedzi. Możemy teraz spróbować przetestować najprostsze połączenie. W tym celu musimy jednak zaopatrzyć się w jakieś narzędzie, umożliwiające połączenie z siecią i przesłanie w obie strony tekstowych komunikatów. Mamy tu dwie możliwości. W przypadku Linuksa najwygodniejsze będzie użycie konsolowego narzędzia. Takim narzędziem może być telnet lub netcat. Ponieważ nie we wszystkich dystrybucjach są one zainstalowane na dzień dobry, może wystąpić konieczność doinstalowania. Dla Debiana i pochodnych (Raspbian, Ubuntu, Mint itp.) będzie to: sudo apt install telnet lub sudo apt install netcat I tu uwaga: w większości przypadków ostatnie polecenie zainstaluje nam netcat-traditional, czyli tradycyjną wersję programu. Z różnych przyczyn (szczególnie przy testach połączeń UDP) lepsza może by wersja openbsd, tak więc jeśli mamy taką możliwość warto ją zainstalować: sudo apt install netcat-openbsd Składnia obu poleceń dla naszych celów jest podobna: telnet <adres> <port> lub nc -C <adres> <port> Opcja -C w drugim przypadku włącza tryb przekazywania sekwencji CRLF zamiast LF (czyli po naciśnięciu ENTER) - co jak wspomniałem wcześniej jest wymagane dla połączeń http. Inną możliwością jest użycie znanego programu PuTTY. Nie będę tu opisywać procedur instalacji dla wszystkich możliwych systemów operacyjnych, należy jednak wspomnieć o ustawieniach: W ustawieniach sesji należy zaznaczyć "Connection type: RAW" (czyli "surowe" połączenie, bez żadnych dodatkowych protokołów) i "Close window on exit: Never" (bo nie chcemy, aby okno zamykało się od razu po odebraniu danych nie dając nam czasu na przeczytanie). W ustawieniach terminala natomiast "Implicit CR on every LF" (inaczej linie na terminalu "rozjadą się"). Takie ustawienia możemy sobie zapisać pod jakąś mądrą nazwą (np. tak jak w moim przypadku "raw http") i używać w wielu następnych testach. Spróbujmy więc na początek połączyć się z jakimś ogólnodostępnym serwerem. Na początek spróbujmy serwera Google. Wpisujemy więc następujące polecenie (lub łączymy się poprzez PuTTY z serwerem): nc -C www.google.pl 80 Oczywiście musimy sporo przewinąć w górę aby dojść do początku transmisji (przy okazji widać, że pozornie prosta strona Google wcale nie jest taka prosta). Możemy teraz zobaczyć dwa najważniejsze nagłówki odpowiedzi: HTTP/1.1 200 OK Taki nagłówek będzie zawsze pierwszą linią wysyłaną przez serwer. Składa się z trzech części: Protokół - w tym przypadku HTTP/1.1 Kod odpowiedzi - w tym przypadku 200 (czyli "wszystko w porządku"). Spis wszystkich kodów możemy znaleźć np. w Wikipedii. Opis odpowiedzi - w tym przypadku "OK" Drugi nagłówek to Content-Type. Musi być zawsze obecny w odpowiedzi i zawiera po prostu typ mime przesłanego dokumentu. Spróbujmy teraz wymusić na serwerze inną odpowiedź. Jak wiemy, wpisując do przeglądarki "google.pl" w jakiś magiczny sposób adres zmienia się na "www.google.pl" - zobaczmy dlaczego: Jak widzimy - serwer odpowiedział informacją o tym, że dokumentu należy szukać pod innym adresem, podając go przy okazji w nagłówku Location. A co będzie, jeśli w ogóle nie podamy nagłówka Host? Przecież w wielu przypadkach (np. domowy RPi) na serwerze jest tylko jeden serwis... spróbujmy. Tym razem zamiast z Google (który trochę nietypowo traktuje błędy) połączymy się z serwerem Forbota: Jak widać - serwer traktuje to jako błąd. Ale co z protokołem HTTP/1.0? Wtedy przecież, w początkach Internetu, wystarczył adres IP... Spróbujmy! Widzimy więc, że tym razem serwer nie uznał naszego zapytania za błędne. Natomiast to, że nic konkretnego nam nie napisał oprócz tego, że działa - wynika z faktu, że bez nagłówka Host nie jest w stanie stwierdzić do którego serwisu wysłane jest zapytanie! I to na dziś tyle - w następnej części spróbujemy zrobić prosty serwer. Spis treści serii artykułów: Protokół HTTP w zastosowaniach IoT - część 1: trochę teorii Protokół HTTP w zastosowaniach IoT - część 2: budujemy serwer Protokół HTTP w zastosowaniach IoT - część 3: tworzymy klienta
  18. Pojawiła się potrzeba wykonania prostego sterownika do bramy garażowej, który miałby powiadamiać mieszkańców czy aktualnie garaż jest zamknięty czy otwarty oraz w dowolnej chwili sprawdzić status. Tak powstało niewielkie urządzenie montowane na szynę DIN. Jest zasilane z dowolnej ładowarki od telefonu, posiada zabezpieczenie przed odwrotną polaryzacja zasilania. Sterownik ma kilka wejść/wyjść; IN1 - dolna krańcówka od zamknięcia garażu. IN2 - górna krańcówka od pełnego otwarcia garażu. wyjście przekaźnikowe NO do zdalnego otwierania/zamykania bramy. RS485 - pozwala podłączyć czujnik odległości wykrywający czy auto jest w garażu. czujnik temperatury DS18B20. przycisk do resetowania ustawień WiFi i uruchomienia ponownej konfiguracji. W sterowniku zastosowałem popularny układ ESP8266 w wersji WemosD1 mini. Jak widać za wiele rzeczy tu nie ma, oprócz ESP znajduje się przekaźnik, DS18B20 oraz transceiver RS485. Projekt miał być prosty, szybki i jednostkowy dlatego nie zastosowałem dodatkowych stopni ochrony wejść w postaci np. optoizolacji. Tradycyjnie płytka powstała na żelazku i wytrawiona w kwasie. Polutowana i zabezpieczona lakierem do PCB. Schemat ideowy: Wspomniany wcześniej czujnik odległości jest zbudowany z wykorzystaniem ultradźwiękowego czujnika HC-SR04 i Arduino Nano, które cyklicznie wysyła informacje do głównego sterownika. Schemat czujnika: Sterownik ma zaimplementowany serwer WWW co pozwala na sterowanie praktycznie dowolnym urządzeniem z przeglądarką. A panel sterowania prezentuje się tak: Dodałem obsługę powiadomień push na telefon z wykorzystaniem mechanizmu IFTTT (if this then that). Wystarczy zainstalować tą aplikacje na telefonie, a w sterowniku wprowadzić unikalny klucz aplikacji powiązany z konkretnym telefonem. Aktualizacja oprogramowanie wykorzystuje mechanizm OTA i sprowadza się do wgrania pliku przez panel www. Dodatkowo wystawione jest proste API, które pozwala na integracje z większością systemów smart home typu Domoticz, Home Assistant itp.
  19. Witajcie. Chciałbym Wam przedstawić prosty sposób wykorzystania modułu ESP-32, który użyłem do stworzenia urządzenia, za pomocą którego możecie śledzić poziom zainteresowania wybranym repozytorium GitHub. Dzięki wbudowanym dwóm wyświetlaczom, będziecie na bieżąco informowani o: aktualnej liczbie gwiazdek dziennej liczbie gwiazdek aktualnej liczbie obserwujących aktualnej liczbie forków Początkowo chciałem skorzystać ze starszego modułu ESP8266 12-F, jednak napotkałem problem który objawiał się przy wykonywaniu requesta do api GitHub. Podczas oczekiwania na odpowiedź z serwera, na wyświetlaczach zanikały wszystkie cyfry i wyglądało to jakby coś było nie tak z urządzeniem. Z pomocą przyszedł układ ESP-32, który oparty jest na dwóch rdzeniach. Była to świetna okazja aby zapoznać się z tym modułem, ponieważ wcześniejsze projekty wykonywałem na ESP8266. Użycie nowszej odsłony ESP, pozwoliło mi wysyłać requesty asynchronicznie. Do działania ramki wymagane jest połączenie z siecią poprzez WiFi. Tutaj świetnie sprawdziła się biblioteka "WiFi Manager", która umożliwiła mi szybkie podłączenie ramki do dowolnej sieci. Jeżeli chodzi o zasilanie to jest to napięcie 5V, które podaje poprzez wtyki USB. W obudowie ramki znajdują się trzy przyciski. Pierwszy służy jako włącznik. Pozostałe dwa to włączniki chwilowe. Po wciśnięciu pierwszego na wyświetlaczu prezentowany jest adres IP, który wykorzystujemy przy konfiguracji. Natomiast drugi przycisk resetuje liczbę gwiazdek z dnia. Do układu podłączona jest 3 kolorowa dioda LED, która informuje nas o stanie połączenia: CZERWONY – brak sieci, błąd podczas pobierania danych ZIELONY – połączenie sieciowe ustanowione, dane pobrane poprawnie NIEBIESKI – tryb access pointu ( zanik sieci ) Domyślnie odświeżanie danych odbywa się co 90 sekund. Oczywiście interwał można zmienić. Ale należy uważać, aby nie wykonywać do api GitHub więcej niż 60 requestów na godzinę, ponieważ serwer ma ustawiony RateLimit. Po przekroczeniu ilości zapytań zostaniemy zablokowani na godzinę. Jak już wspomniałem wyżej, pod adresem IP jaki przydzielony został do urządzenia działa prosty web server, który serwuje nam stronę z konfiguracją, gdzie musimy wprowadzić nazwę użytkownika repozytorium oraz nazwę repozytorium które chcemy obserwować. Po zapisaniu konfiguracji w pamięci EEPROM urządzenie jest restartowane i gotowe do użycia. Dodatkowym atutem urządzenia jest automatyczna aktualizacja oprogramowania poprzez HTTPS OTA. Sprawdzanie wersji następuje podczas uruchomienia oraz po północy. Urządzenie jest w pełni bezobsługowe. Gdy wystąpi zanik sieci, ESP cały czas będzie próbowało nawiązać połączenie za pośrednictwem zapamiętanych poświadczeń. Jeśli sieć nie będzie dostępna, przełączy się w tryb access pointu ( ssid: "GITHUB-FRAME"). Jeśli nie zostanie wybrana nowa sieć w menadżerze sieci, to po czasie 3 minut, nastąpi restart i proces się powtórzy. Tak pokrótce, wygląda zasada działania całego układu. Poniżej przedstawię Wam główne etapy budowy całej ramki. A więc zaczynamy. LISTA ELEMENTÓW: ESP-32 WROOM DevKit 1.0 – 1 szt. Wyświetlacz LED 4-Cyfrowy TM1637 – 0.56" – 1 szt. Wyświetlacz LED 4-Cyfrowy TM1637 - 0.36" – 1 szt. 4-pinowy przewód, żeński – żeński – raster 2.54 – 4 szt. Gniazdo + wtyk, JST – JST – 2 szt. Gniazdo + wtyk, mikro JST – mikro JST – 2 szt. Płytka uniwersalna PCB 50x70mm PI-01 – 1 szt. Rezystor węglowy – 220 ohm – 5 szt. Rezystor węglowy – 2,2k ohm – 3 szt. Zworki do płytek stykowych - 1 zestaw Wtyk goldpin kątowy 4 pinowy, raster 2,54mm – 4 szt. Dioda LED 5mm RGB wsp. Anoda – 1 szt. Dioda LED 3mm biała – 3 szt. Przełącznik chwilowy okrągły – 10mm – 2 szt. Przełącznik kołyskowy ON-OFF – 1 szt. Kabel USB A – USB micro – 1 szt. Zasilacz 5V z gniazdem USB A – 1 szt. Rurki termokurczliwe - 1 szt. Ramka IKEA RIBBA – 21x30cm ( ważne żeby była dość głęboka, aby zmieścić elektronkę ) – 1 szt. Papier samoprzylepny do drukarki – 1 szt. Rura elektroinstalacyjna RLM 16 – 1 szt. NARZĘDZIA: Lutownica Cyna Obcążki Wiertarka lub wkrętarka Wiertła: 7, 10, 13 Pistolet do kleju na gorąco Nóż Drukarka ZAŁOŻENIA: Stabilność działania Intuicyjna obsługa Szybka adaptacja w miejscu instalacji Estetyka Plan Początkowo ramka miała powstać pod konkretnie wybrane repozytorium i wyświetlać tylko liczbę gwiazdek. Ale stwierdziłem, że i tak pobieram inne dane z endpointa api to czemu miałbym ich nie wyświetlić. Postanowiłem, że dodam dwa nowe klucze: "forks" oraz "watchers" i wyświetlać je kolejno w 5 sekundowym odstępie czasowym. Jeżeli chodzi o repozytorium, to dając możliwość wprowadzenia własnych ustawień url-a, zwiększyłem tym samym skalowalność przedsięwzięcia. Do tego doszły automatycznie aktualizacje software-u. Więc taką ramkę może stworzyć każda osoba, która chociaż trochę ma pojęcie o informatyce i niekoniecznie zna się na programowaniu. BUDOWA Prace nad ramką rozpocząłem od budowy prototypu metodą na "pająka". W tym celu skorzystałem z płytki prototypowej, przycisków typu "tact switch", paru zworek oraz kilkunastu przewodów do połączenia wszystkiego w całość. Całe oprogramowanie zostało napisane za pośrednictwem Arduino IDE czyli w języku C. Gdy miałem już działający prototyp, rozpocząłem prace nad przeniesieniem całości na uniwersalną płytkę PCB. Zadanie wydawałoby się proste ale wymagało procesu planowania. W tym celu wykorzystałem oprogramowanie Fritzing. Oprogramowanie to umożliwia stworzenie całej dokumentacji projektu. Na tę chwilę wykorzystałem tylko narzędzie do stworzenia szkicu płytki prototypowej. Mając gotowy już projekt z rozlokowaniem wszystkich elementów i połączeń. Mogłem przystąpić do lutowania. Jak widać na zdjęciach, podczas montażu elementów używałem uchwytu, który stabilnie trzyma płytkę w miejscu. Bardzo ułatwił mi pracę. Po przylutowaniu wszystkich elementów elektronicznych, wlutowałem przewody z gniazdami, dzięki którym będę mógł odłączyć układ od samej konstrukcji ramki Teraz przyszedł czas na najtrudniejszy etap jakim było dostosowanie drewnianej ramki do potrzeb projektu. W programie Photoshop stworzyłem szablon do wiercenia i wycinania potrzebnych otworów. Szablony te znajdziecie również w repozytorium projektu. Po wydrukowaniu szablonu przykleiłem kartkę do “pleców ramki” i wyciąłem wszystkie otwory. Trochę trzeba się do tego przyłożyć i mieć sporo cierpliwości. Cięcie, pasowanie, cięcie, pasowanie aż do skutku. Ufff. W końcu mogłem przystąpić do zamontowania wyświetlaczy oraz diod LED. Z pomocą przyszedł mi klej na gorąco. Trzyma mocno i pewnie, wystarczająco do tego typu prac. Trzy diody LED umieściłem w przyciętych krążkach z białej rury pcv ( tych do prowadzenia przewodów po ścianach ) a górę zaślepiłem kawałkiem plastiku w którym zamocowałem diody. A tak całość prezentuje się od frontu Za pomocą 4 żyłowych przewodów zakończonych wtykami żeńskimi, połączyłem wszystkie elementy z główną płytką. W celu szybszej identyfikacji przewodów, oznaczyłem każde połączenie za pomocą lakierów do paznokci ( pozdrawiam swoją żonę Magdalenę ). Główny układ przykleiłem do pleców ramki również za pomocą kleju na gorąco. Na koniec pomalowałem cały front na biało farbą emulsyjną, ponieważ papier który używa się w drukarkach ma małą gramaturę co sprawia, że jest półprzezroczysty. Dzięki podbiciu koloru tła biel będzie intensywniejsza. W ostatecznej wersji grafikę wydrukowałem na papierze fotograficznym, który jest na tyle gruby, że malowanie okazało się być zbędne. SOFTWARE Cały program opiera się na dwóch pętlach. Pierwsza pętla Task1, sprawdza czy użytkownik wprowadził url repozytorium z którego mają zostać pobrane dane. Jeżeli konfiguracja została wprowadzona, program wywołuje funkcję getData(), która odpowiedzialna jest za pobranie danych z API. Interwał tej pętli definiuje zmienna requestInterval, która domyślnie posiada wartość 90 ( czyli 90 sekund). Druga pętla Task2, służy do wyświetlania odpowiednich danych na wyświetlaczach oraz podświetlania ikon. Tutaj również sprawdzany jest stan na pinach 27 i 15 ( przyciski BUTTON_IP oraz BUTTON_RESET_TODAY). Interwał tej pętli to około 15 sekund. Po północy następuje sprawdzenie dostępności nowszej wersji oprogramowania oraz resetowany jest licznik gwiazdek otrzymanych w ciągu całego dnia. Poniżej znajdziecie link do repozytorium z projektem: OPROGRAMOWANIE + SZABLONY DO DRUKU PODSUMOWANIE Przyznam się szczerze, że prototyp urządzenia miałem już gotowy rok temu. Ale ze względu na gruntowny remont mieszkania musiałem odsunąć hobby na dalszy plan. Rozciągnięcie projektu w czasie sprawiło, że przy każdym powrocie zawsze coś zmieniałem, rozbudowywałem. Wszystko wtedy można przemyśleć kilka razy i na spokojnie zastanowić się nad rozwiązaniem jakiegoś problemu. Na co dzień zajmuję się programowaniem front-endu, ale dzięki takim projektom mogę połączyć moje główne zainteresowania: majsterkowanie, elektronikę, grafikę i jak już wcześniej wspomniałem, programowanie i stworzyć coś namacalnego i cieszącego oko. Zachęcam wszystkich do twórczego działania i poszerzania swojej wiedzy. Tego typu projekty dadzą Wam satysfakcję, świetną zabawę oraz sporo nauczą. Także klawiatury, lutownice, piły, śrubokręty, wiertarki w dłoń i do działania! Instrukcję już macie 😀 Do następnego projektu! 🤘 Pozdrawiam.
  20. Parę lat temu podczas remontu u rodziców postanowiłem usprawnić im parę rzeczy w mieszkaniu. Stworzyłem sterownik, który integrował ze sobą sterowanie oświetleniem i ogrzewaniem. Pomimo prostej obsługi nie przewidziałem tego, że moi rodzice będą bali się tego używać, myśleli że coś zepsują... niestety są na bakier z elektroniką. Tak powstała druga wersja, która jest dla nich absolutnie bezobsługowa w kwestii ustawień czy sterowania ogrzewaniem. Podstawowe cechy komunikacja Wifi integracja z Firebase zdalny dostęp z dowolnego miejsca na świecie Odtwarzanie komunikatów głosowych sterowanie za pomocą asystenta Google pilot radiowy 2,4GHz sterowanie oświetleniem sterowanie ogrzewaniem dwupunktowy pomiar temperatury detekcja otwartego okna automatyczne aktualizowanie czasu z serwera NTP łatwe dokładanie bezprzewodowych czujników, np. zalania duży i czytelny wyświetlacz Tym razem uwzględniłem obawy rodziców i w każdej chwili mam dostęp do wszystkich ustawień. Dla niecierpliwych filmik (polecam włączyć dźwięk) 🙂 Główny moduł Całe sterowanie oparłem o układ ESP12. Jest to układ o bardzo dużych zasobach z wbudowanym wifi. Niestety ma on bardzo mało wyprowadzeń, dlatego aby obsłużyć wszystkie dodatkowe elementy takie jak: LCD ST7565 DS18B20 Enkoder Trzy przyciski Kilka wyjść Moduł MP3 (JQ8400-FL) RFM73 Wykorzystałem 16 bitowy ekspander na SPI MCP23S17. Wymagało to dostosowania kilku bibliotek np. LCD czy enkodera, aby zamiast pinów ESP używały MCP23S17. To było największym wyzwaniem dla mnie, aby zrealizować obsługę trzech układów SPI, gdzie dwa z nich były częściowo sterowane poprzez pierwszy czyli MCP23S17. Dodatkowo przydzielanie czasu na poszczególny element trzeba było odpowiednio rozdzielić, aby obsługa enkodera nie gubiła kroków. Zdalny dostęp Sterownik działa jako klient w sieci wifi, nie chciałem przekierowywać portów na routerze, ani stawiać dodatkowego VPNa. Wykorzystałam tutaj RealTime Database od Google czyli Firebase. Jest darmowe, względnie proste w obsłudze wprost z układu ESP, a dostęp do Firebase posiada każdy kto ma konto w Google. Wykorzystanie tego mechanizmu daje nam bardzo duże możliwości. Możemy hostować tam własną stronę, która będzie naszym frontendem, możemy wykorzystać “cloud functions”, które są praktycznie zasobami node.js, jest to bardzo fajne, bo możemy mieć własny serwer node.js w chmurze. Mając cloud functions i bazę danych, możemy z łatwością podpiąć pod to inne usługi, np. asystenta Google, który pozwoli sterować urządzeniem naturalną mową a nie nagranymi wcześniej próbkami komend. Pod nasz system możemy podpiąć również IFTTT (if this then that) i jeszcze bardziej zautomatyzować obsługę urządzenia. ESP co kilka sekund odczytuje bazę danych z Firebase i sprawdza czy stan jakiegoś elementu uległ zmianie, jeśli tak to znaczy, że zdalnie zmieniliśmy parametry pracy. W drugą stronę, jeśli to sterownik lokalnie zmieni jakąś wartość to od razu aktualizuje ją w Firebase. Asystent Google i komunikaty głosowe Rodzice swoje lata już mają i pamięć nie ta, dlatego dołożyłem system komunikatów głosowych. Pilot, który będzie zawsze pod ręką tuż obok pilota od TV, będzie (czeka na obudowę) jasno opisany i wystarczy nacisnąć jeden przycisk a sterownik powie jaka jest temperatura w mieszkaniu, jaka jest na zewnątrz, czy ogrzewanie aktualnie pracuje albo ile razy dzisiaj było włączane. Pomyślałem, że fajnym dodatkiem będzie wgranie również instrukcji obsługi, gdzie sterownik powie do czego służą poszczególne przyciski. Ze sterownikiem można rozmawiać w sposób naturalny z wykorzystaniem asystenta Google, nie musimy być nawet w pobliżu sterownika, mówimy do telefonu czy komputera znajdując się w dowolnym miejscu na świecie. Sam Google asystent nie steruje bezpośrednio urządzeniem, z wykorzystaniem mechanizmu Dialogflow, który bazuje na machine learning zmienia parametry pracy w Firebase, a urządzenie docelowe pobiera je i wykonuje. Zależności między komponentami pokazałem na poniższej grafice. Moduł radiowy 2,4GHz W sterowniku znajduje się RFM73, jest to niewielki układ radiowy wykorzystany tutaj jako pilot, we wcześniejszej wersji tradycyjny pilot na podczerwień kiepsko się sprawdzał, nie zawsze było się w polu widzenia odbiornika. Dodatkowe czujniki Posiadając na pokładzie układ RFM73 możemy dowolnie rozbudować system o dodatkowe czujniki, np. czujnik zalania w łazience, czujnik oświetlenia czy dodatkowe czujniki temperatury. Dokładanie dodatkowych czujników nie wiąże się z przeprogramowaniem głównego sterownika, ponieważ wszystkie reguły obsługi możemy umieścić w Firebase, a sterownik je pobierze. Poniżej kilka fotek, a jako trzecią rękę polecam taki stojak:
  21. Witajcie Chciałbym przedstawić tu swój projekt Sterownika do akwarium pracującego w systemie automatyki domowej opartej na Domoticzu. Pomysł i historia projektu Ponad rok temu wpadliśmy z żoną na pomysł założenia akwarium, pomysł jak pomysł, ale im więcej czytałem tym więcej kombinowałem, a jako że jestem elektronikiem to i pomysłów przybywało mi z każdym dniem 🙂 Pierwsza próba budowy sterownika polegała na wykorzystaniu Arduino nano i stworzeniu samodzielnego układu pracującego lokalnie. Tyle że już w trakcie pisania kodu stwierdziłem że sterowanie sterowaniem, ale fajnie by było móc zrobić coś zdalnie, a przede wszystkim móc sprawdzić zdalnie co dzieje się w akwarium (temperatura, stan lamp itp). Tak zaczęła się moja przygoda z Domoticzem i szeroko pojętym IoT. Opis projektu Sam sterownik zbudowany jest na Arduino Pro mini, natomiast do komunikacji z serwerem wykorzystana jest platforma Mysensors zbudowana na modułach NRF24L01. Jak wcześniej pisałem sterownik pracuje w systemie mojej domowej automatyki opartej o serwer Domoticza, pracujący na RaspberryPi 3B. Na chwile obecną sterownik realizuje następujące funkcje: cztery kanały ON/OFF (przekaźniki 10A 250V), sterowanie: grzałka, filtr, falownik, chłodzenie (wentylatory w pokrywie). Z uwagi na bezpieczeństwo mieszkańców akwarium filtr podpięty jest pod styki NC przekaźnika, tzn, domyślnie jest on włączony, a możemy go wyłączyć. 16 kanałów PWM (z tranzystorami IRF520 jako elementy wykonawcze), sterowanie lampami LED, ściemnianie podświetlenia wyświetlacza, sterownie pracą falownika, rezerwa. funkcja "Karmienie" i "Serwis", załączane z lokalnej klawiatury, pierwsza umożliwia czasowe (5min) wyłączenie filtra, falownika i wentylatorów, druga umożliwia wyłączenie trybu automatyki i przejście w tryb serwisowy (przydatne przy pracach porządkowych w zbiorniku). pomiar temperatury w akwarium (czujniki DS18B20), układ zrobiony jest tak że może obsłużyć nieskończenie wiele termometrów, w praktyce w mniejszym zbiorniku mam 2 w większym 3 i to wystarcza. Dodatkowo można wybrać czy automatyka sterowania grzaniem/chłodzeniem korzysta z jednego z termometrów czy z wartości średniej wszystkich wskazań. zabezpieczenie przed przegrzaniem i wychłodzeniem wody (bezwarunkowe wyłączenie/włączenie grzania/chłodzenia w temperaturach skrajnych) pomiar temperatury zewnętrznej pomiar poziomu oświetlenia zewnętrznego Schemat blokowy sterownika przedstawiam na poniższym rysunku Jak widać większość układów pracuje na magistrali I2C, sprawia to że całość jest projektem rozwojowym, a wszelkie możliwe dodatkowe funkcje ograniczone są tylko wyobraźnią i.... pojemnością pamięci wykorzystanego Arduino 😉 Kanały PWM zrealizowane są na module PCA 9685, a jako elementy wykonawcze służą tranzystory IRF520 (oczywiście można zastosować inne MOSFETy), moduł z przekaźnikami podłączony jest do magistrali I2C poprzez expander PCF8574. Zarządzanie sterownikiem Podstawowe sterowanie pracą sterownika możliwe jest z lokalnej klawiatury, natomiast i informacje o pracy sterownika (temperatura, czas, poziom światła poszczególnych kanałów LED, stan urządzeń wykonawczych itp) wyświetlane są bezpośrednio na wyświetlaczu LCD 20x4. Pełne sterownie możliwe jest poprzez serwer Domoticza, za pośrednictwem strony www (poniżej przykładowy zrzut ekranu) i/lub aplikacji na androida. Teraz czas na kilka zdjęć elementów sterownika: Moduł arduino z przetwornica 3,3V, expanderem PCF 8574 i modułem NRF24L01 zmontowane są na płytce uniwersalnej, która osadzona jest na module przekaźników tworząc "kanapkę" Widok "kanapki" płytki uniwersalnej z arduino i modułu z przekaźnikami Moduł wykonawczy na tranzystorach IRF (sześć kanałów do sterowania lampami LED) Moduł przekaźników Wyświetlacz LCD Problemy napotkane przy realizacji projektu konieczne było wymuszenie niższej częstotliwości pracy platformy mysensors z powodu zegara jakim taktowane jest zastosowane arduino niestabilna praca modułów NRF, konieczne było dodanie elektrolitów (dałem 330u bo takie miałem :)) jak najbliżej pinów zasilających modułu (lutowałem bezpośrednio na tych pinach) okresowe zawieszanie i/lub przekłamanie odczytu temperatury z termometrów DS18B20, nie doszedłem przyczyny, wiec dodałem możliwość zdalnego resetu arduino (wiem że to tylko zaleczenie problemu a nie rozwiązanie, ale tymczasowo zdaje egzamin) zbyt małą pamięć programu zastosowanego modułu arduino, zastanawiam się czy nie przejść na ESP8266 z EasyESP konieczność zbudowania obudowy 😉
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.