Skocz do zawartości

Przeszukaj forum

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

  • 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

  1. 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.
  2. Ten artykuł jest częścią serii "Kurs? Raspberry Pi Pico" #0 - Wstęp, spis treści #1 - GPIO #2 - UART #3 - PWM, ADC, IRQ na GPIO W tym rozdziale Dowiesz się czym jest magistrala I2C oraz SPI oraz nauczysz się jak z nich korzystać przy wykorzystaniu wygodnego API 😉 I2C Jest to drugi często spotykany rodzaj magistrali. Z niej często korzystają interfejsy dla wyświetlaczy alfanumerycznych (takich małych 16 znaków w 2 liniach) oraz np pamięci EEPROM (programowalna pamięć, którą można modyfikować z poziomu zewnętrznych urządzeń wysyłających do niej dane). To właśnie ten drugi element będzie naszym pacjentem badawczym. W moim przypadku będzie to konkretnie model AT24C64A. Zajmiemy się zapisem i odczytem jednego bajta z pamięci, do czego według specyfikacji producenta należy przesłać poprzez I2C 2 bajty adresu oraz dane do zapisania lub 2 bajty adresu i zażądać odczytu danych. 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 » Adres? Tak - I2C jest jedną z magistrali pozwalających adresować urządzenia. Do dyspozycji użytkownika jest 128 adresów (0 - 127). Wyżej wymieniona pamięć ma adres 0x50. Oprócz tego ma 3 piny pozwalające dodać do adresu maksymalnie 7 - czyli zakres adresowy jest od 0x50 do 0x57. Zajmijmy się więc podłączeniem pamięci - do GP4 podłączamy SDA, do GP5 podłączamy SCL. Do samej kości pamięci podłączamy zasilanie oraz masę. Oprócz tego do masy zwieramy pin WP. Piny A1,A2,A3 zostawiamy “wiszące”. Teraz omówmy funkcje - tutaj również powstała wygodna biblioteka - IIC.h. void begin(i2c_inst_t *inst, int baudRate); // Inicjacja I2C void end(); // koniec I2C void setBaud(int baudRate); // ustawianie baudrate I2C int available(); // sprawdzanie czy są dane ;) uint8_t* read(uint8_t address, size_t amount); // Odczyt danych - odczytujemy sekwencję danych binarnych void write(uint8_t address, uint8_t* data, size_t amount); // Zapis sekwencji binarnej na SPI void free_memory(); // Wyczyść pamięć - po wykorzystaniu odczytanych danych zalecam wykonać tę metodę - w celu oszczędności RAM’u ;) void set_slave(bool mode, uint8_t addr); // ustawia tryb niewolnika No i kurs został zdemonetyzowany (żart). Pewnie zastanawiasz się co to jest “tryb niewolnika” - otóż w magistrali I2C (oraz SPI, które omówimy za chwilę) istnieje master i slave. Master wysyła żądanie do slave’a, który na nie odpowiada. Czyli za początek komunikacji zawsze odpowiada master. W przypadku slave - który w naszym przykładowym programie to kość pamięci EEPROM oczekujemy na żądanie i na nie odpowiadamy. Jeżeli ustawimy tryb na true możemy ustawić adres naszego pico i wysyłać do niego żądania tak samo jak do pamięci. Tego tematu nie będziemy poruszać w podstawowej wersji kursu, gdyż jest rzadko stosowany. Tymczasem zajmijmy się naszym programem 😉 Na początku polecam poczytać o funkcjach oraz tablicach w C/C++. Teraz za zadanie mamy zgodnie ze specyfikacją producenta odczytać bajt do pamięci i zapisać bajt do pamięci. By zapisać bajt zapisujemy na I2C adres komórki pamięci (dwa bajty) oraz jeden bajt, który jest wartością. W celu odczytu zapisujemy na I2C adres komórki pamięci (dwa bajty) oraz odczytujemy jeden bajt (po czasie t, który ustalmy na 10ms - jest on znacznie nad wyrost, ale i tak nie zauważymy tego, a pozwoli uniknąć błędów wynikających z tego, że kość nie zdążyła przygotować danych dla magistrali). Przykładowy kod: #include <cstdio> #include "pico/stdlib.h" #include "IIC.h" #include <string> void i2c_eeprom_write_byte( uint8_t addr, uint16_t mem_addr, uint8_t data ) { // Przekonweruj adres ze słowa na bajty uint8_t addr_msb = mem_addr >> 8; uint8_t addr_lsb = mem_addr & 0xFF; // Zbuduj tablicę do wysłania uint8_t data_to_write[] = {addr_msb, addr_lsb, data}; // Zapisz bajt do pamięci EEPROM i2c.write(addr, data_to_write, 3); } uint8_t i2c_eeprom_read_byte( uint8_t addr, uint16_t mem_addr) { // Konwertuj adres do bajtów uint8_t addr_msb = mem_addr >> 8; uint8_t addr_lsb = mem_addr & 0xFF; // Zbuduj tablicę do wysłania uint8_t data_to_write[] = {addr_msb, addr_lsb}; i2c.write(addr, data_to_write, 2); // Odczekaj chwilę sleep_ms(10); // Odczytaj zwrócone dane... uint8_t* data = i2c.read(addr, 1); return data[0]; // Zwróć pierwszy bajt } Kod sam się opisuje, więc za bardzo nie będę w to wnikał 😉 Na koniec przykładowy program wykorzystujący nasze funkcje: int main() { stdio_init_all(); i2c.begin(i2c0, 100*1000); // Zainicjuj I2C i2c_eeprom_write_byte(ADDR, 0, 0x47); // Zapisz do EEPROM[0] wartość 0x47 sleep_ms(10); // Odczekaj chwilę ;) // Można i tak :D puts(std::to_string(i2c_eeprom_read_byte(ADDR, 0)).c_str()); // Wyślij na UART wartość EEPROM[0] puts(std::to_string(i2c_eeprom_read_byte(ADDR, 1)).c_str()); // Wyślij na UART wartość EEPROM[1] puts(std::to_string(i2c_eeprom_read_byte(ADDR, 2)).c_str()); // Wyślij na UART wartość EEPROM[2] // EEPROM[X] - bajt X w pamięci EEPROM } Jak widzimy korzystam z innej wersji wysyłania danych na UART - wersji bez biblioteki. Jeżeli masz kość EEPROM z serii AT24C możesz sam spróbować / sama spróbować i zobaczyć czy zwracane dane będą poprawne (0x47, 0x0, 0x0) lub (0x47, 0xFF, 0xFF). Oczywiście dane będą tak wyglądać, o ile nikt wcześniej nic nie zapisał na kości pamięci 😉 SPI Trzeci rodzaj magistrali, niestety tutaj będzie bez przykładu, gdyż nie mam żadnego chipu, który mogę podpiąć pod Pico z tą magistralą (chipy mam, ale niestety wszystkie są wlutowane w płytki). No to czas pokazać, że czegoś możesz się nauczyć nawet bez praktyki. SPI jest magistralą dupleksową, która ZAWSZE przesyła dane w obie strony. Czyli nawet jak odczytujesz dane to w tym momencie przesyłasz dane przez SPI. W specyfikacji dostępu poprzez tę magistralę do chipu producent zwykle wymienia ustawienia: CPOL, CPHA, kolejność bitów oraz ich ilość. API też pomaga z tymi ustawieniami. Po prostu ustawiasz wartości takie jak podaje producent i nie musisz wnikać w szczegóły. Jednakowoż jeżeli chcesz wnikać - CPOL określa czy zegar w stanie standardowym ma stan wysoki czy niski, a CPHA przy jakim rodzaju zbocza zegara pobierane są dane. Warto nadmienić, że w SPI również możemy obsłużyć wiele urządzeń, do tego służy pin CS, który musi mieć (zazwyczaj) stan niski, by dany slave był aktywowany (odbierał wiadomości). Pamiętaj o tym podczas podłączania swojego chipu/urządzenia do Pico. Dodatkowo Pico w trybie Slave ma dodatkowe piny (patrz pinout w rozdziale #0), które określają czy ma odbierać wiadomości. Patrz poniższa grafika: W naszym API pinami SPI0 są odpowiednio piny GP16 - MISO, GP18 - SCK, GP19 - MOSI. I tego możemy się trzymać. Funkcje dostępne w API to: SPI* begin(spi_inst_t *inst, int baudRate); // Inicjuje SPI ;) SPI* cpha(bool isHigh); // Ustawia CPHA SPI* cpol(bool isHigh); // Ustawia CPOL SPI* data_bits(uint8_t bits); // Ustawia ilość bitów SPI* msb_first(); // MSB_FIRST SPI* lsb_first(); // LSB_FIRST void setup(); // aktualizuje ustawienia void end(); // Konczy pracę SPI void setBaud(int baudRate); // Ustawia baudrate uint8_t read(); // odczytuje bajt wysyłając 0x0 void write(uint8_t data); // wysyła bajt uint8_t read_write(uint8_t data); // równocześnie wysyła i odczytuje bajt (dupleks) void set_slave(bool mode); // Włącza tryb slave Przykładowy syntetyczny program może wyglądać następująco: #include <cstdio> #include "pico/stdlib.h" #include "SPI.h" SPI spi; int main() { stdio_init_all(); spi.begin(spi0, 12000000)->cpol(false)->cpha(false)->data_bits(8)->msb_first()->setup(); // Inicjacja SPI ;) spi.write(0x40); // Wyślij komendę 0x40 for(int q = 0; q < 4; q++) { spi.write(0x0); // Odczekaj 4 cykle wysyłając pustą treść } uint8_t data = spi.read(); // Odczytaj dane } Jest to syntetyczny program, który nie robi zupełnie nic, ale pokazuje jak korzystać z API. Teraz wystarczy, że znajdziesz w swojej szufladzie coś korzystającego z magistrali SPI, podążysz za wskazaniami producenta i gotowe - komunikacja SPI z Pi Pico. To by było na tyle w tym rozdziale Jeżeli masz jakieś pytania śmiało je zadawaj, od tego tutaj jesteśmy 😉 Zadania domowe Przećwicz magistralę I2C Przećwicz magistralę SPI
  3. Bot głosowy Marian Każdy chyba słyszał o asystentach głosowych, czy to Alexa czy Asystent Google. Ale czy ktoś z was próbował stworzyć własnego asystenta głosowego? Ja natchniony nowo zakupioną Alexą, postanowiłem spróbować swoich sił w napisaniu prostego bota sterowanego głosem, który może przeprowadzić proste interakcje z użytkownikiem. Sięgnąłem moim zdaniem po najodpowiedniejsze narzędzie do tego projektu, czyli po pythona, co zaowocowało powstaniem Mariana. 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 » Do stworzenia własnego bota głosowego wykorzystałem pythona w wersji 3.8.5, środowisko Spyder oraz cztery biblioteki pythona. import speech_recognition as sr #biblioteka odpowiedzialna za rozpoznawanie naszego głosu import pyttsx3 as tts #biblioteka odpowiedzialna za tworzenie głosu bota from playsound import playsound #biblioteka odpowiedzialna za odtwarzanie muzyki przez bota import time Poniżej komendy których należy użyć do pobrania niezbędnych bibliotek. pip install SpeechRecognition pip install pyttsx3 pip install playsound Następne linie kodu prezentują się następująco: version = "1.0" author = "AvirFrog" sleep_time= 3 r = sr.Recognizer() engine = tts.init() engine.setProperty('rate', 150) Zmienne version oraz author są wykorzystywane w dalszej części programu i nie są niezbędne do jego działania (jeśli zdecydujemy się na pominięcie tych zmiennych w kodzie to powinniśmy też usunąć jedną część o której wspomnę później). Zmienna sleep_time = 3 odpowiada za czas oczekiwania na wywołanie uruchomionego bota. Po włączeniu bota jeśli nie usłyszy on swojego imienia przez najbliższe 3 sekundy wyłączy się. Zmienna r = sr.Recognizer() pozwala nam na wczytywanie naszego głosu. Zmienna engine = tts.init() inicjalizuje silnik zmiany tekstu na mowę i właśnie na tej zmiennej możemy operować i modyfikować możliwości głosowe naszego bota. engine.setProperty('rate', 150) - jest to ustawienie prędkości z jaką nasz asystent głosowy mówi. Jeśli chcielibyśmy zmienić głos naszego bota to możemy wykorzystać poniższy kod voices = engine.getProperty('voices') engine.setProperty('voice', voices[0].id) Zmiana voices[0] na voices[1] sprawi że nasz bot będzie mówił głosem męskim po angielsku. Ja wolałem zostać przy podstawowym głosie żeńskim mówiącym po Polsku, ze względu na to że na razie jest to prototyp bota głosowego. def speak(text): engine.say(text) engine.runAndWait() def get_Text(): with sr.Microphone() as source: try: print("Słucham...") audio = r.listen(source) text = r.recognize_google(audio, language='pl-PL') if text != "": return text return 0 except: return 0 Pierwsza funkcja speak jest bardzo prosta, przyjmuje ona parametr text (czyli to co nasz bot będzie miał powiedzieć) i za pomocą engine.say(text) nasz bot jest w stanie powiedzieć to co mu kazaliśmy. Druga funkcja get_Text polega na zarejestrowaniu naszego głosu. W pierwszej linii za pomocą with sr.Microphone() as source: ustawiamy nasz mikrofon jako rejestrator dźwięku. Następnie mamy wychwytywanie błędu w rozpoznawaniu, czasami zdarzyć się może tak że nasz mikrofon nie zostanie wykryty i z tego powodu by zabezpieczyć program przed błędami używamy try/except. By mieć pewność że wszystko dobrze działa program informuje nas komunikatem "Słucham...", kiedy jest gotowy na przyjęcie polecenia. Niżej do zmiennej audio przekazujemy nasz zarejestrowany głos i zmieniamy go na tekst (korzystając z rozpoznawania mowy google). Poniższa funkcja pozwala mi zaokrąglać liczby po dzieleniu, nie ma w niej nic szczególnego i jest jednym z wielu możliwych rozwiązań (wykorzystamy tę funkcję w dalszej części kodu). def rounding_of_numbers(n, decimals=0): multiplier = 10 ** decimals return int(n * multiplier) / multiplier Nareszcie czas na mózg naszego bota głosowego. W tym miejscu wywołujemy powyższe funkcje i decydujemy na co i w jaki sposób reaguje nasz bot. while True: try: txt = get_Text().lower() txt = txt.split() if txt != 0 and txt[0] == 'marian': print(txt) speak("Tak") if txt != 0 and txt[1] == 'witaj': print(txt) speak("Witaj, co mogę dla Ciebie zrobić?") continue elif txt != 0 and txt[1] == 'włącz' and txt[2] == 'muzykę': print(txt) speak("Włączam muzykę") playsound('WikingFoxMusic_music.mp3') speak("Mam nadzieję że muzyka Ci się podobała, bo mi bardzo") elif txt != 0 and txt[1] == 'wyloguj': print(txt) speak("Dozobaczenia") break elif txt != 0 and txt[1] == 'policz': print(txt) speak("Zaraz policzymy") if txt != 0 and txt[3] == 'dodać': print(txt) speak("dodawanie") n1, n2 = int(txt[2]), int(txt[4]) wynik = n1 + n2 mwynik = str(wynik) speak(f'{str(n1)} dodać {str(n2)} jest równe {mwynik}') continue elif txt != 0 and (txt[3] == '-' or txt[3] == 'minus'): print(txt) speak("odejmowanie") n1, n2 = int(txt[2]), int(txt[4]) wynik = n1 - n2 mwynik = str(wynik) speak(f'{str(n1)} minus {str(n2)} jest równe {mwynik}') continue elif txt != 0 and txt[3] == 'na': print(txt) speak("dzielenie") n1, n2 = int(txt[2]), int(txt[4]) wynik = rounding_of_numbers((n1 / n2), 2) mwynik = str(wynik) speak(f'{str(n1)} podzielić {str(n2)} jest równe {mwynik}') continue elif txt != 0 and (txt[3] == 'x' or txt[3] == 'razy'): print(txt) speak("mnożenie") n1, n2 = int(txt[2]), int(txt[4]) wynik = n1 * n2 mwynik = str(wynik) speak(f'{str(n1)} razy {str(n2)} jest równe {mwynik}') continue else: print(txt) speak(f"Potrafię tylko dodawać, odejmować, dzielić i mnożyć dlatego nie wiem co to {txt[3]}") continue elif txt != 0 and txt[1] == 'informacje': print(txt) speak(f"Informacje o bocie głosowym Marian. Wersja {version}, Autor {author}.") continue else: print(txt) continue else: print(txt) continue except: time.sleep(sleep_time) speak("Skoro nic nie mówisz to wyłączam się. Bywaj") break Teraz postaram się wytłumaczyć jak działają poszczególne rzeczy w naszym bocie: txt = get_Text().lower() txt = txt.split() if txt != 0 and txt[0] == 'marian': print(txt) speak("Tak") Powyższy fragment kodu zmienia nasza mowę, rozpoznaną przez bota tak by była stringiem składającym się z małych liter. Wolałem mieć pewność że program nie będzie miał problemu z rozpoznaniem danego słowa kluczowego (czasami niektóre słowa zapisywał sobie z wielkiej litery co powodowało błędy jeśli nie zostało to uwzględnione w dalszej części kodu, dlatego ten mały zabieg zmiany wszystkich liter w wykrytym tekście na małe litery oszczędza nam wielu problemów). Cały string splitujemy dzięki czemu otrzymujemy tablicę ze słowami. Jeśli pierwszym naszym słowem jest "marian" to nasz bot podejmie z nami interakcję, w przeciwnym razie nie będzie reagował. Zainspirowała mnie do tego Alexa i Asystent Google. Po usłyszeniu swojego imienia bot odpowiada "tak". elif txt != 0 and txt[1] == 'włącz' and txt[2] == 'muzykę': print(txt) speak("Włączam muzykę") playsound('WikingFoxMusic_music.mp3') speak("Mam nadzieję że muzyka Ci się podobała, bo mi bardzo") W powyższym fragmencie można zauważyć że staramy się rozpoznać dwa słowa "włącz muzykę", przez zaproponowany przeze mnie sposób wykrywania komend, musimy właśnie w taki sposób operować na komendach składających się z dwóch lub większej ilości słów. Tutaj widzimy wykorzystanie biblioteki playsound, która uruchamia muzykę będącą w tym samym folderze co nasz skrypt z botem. elif txt != 0 and txt[1] == 'wyloguj': print(txt) speak("Dozobaczenia") break Wyżej natomiast mamy komendę do wyłączenia bota, za co odpowiada break, który wychodzi nam z nieskończonej pętli while. elif txt != 0 and txt[1] == 'policz': print(txt) speak("Zaraz policzymy") if txt != 0 and txt[3] == 'dodać': print(txt) speak("dodawanie") n1, n2 = int(txt[2]), int(txt[4]) wynik = n1 + n2 mwynik = str(wynik) speak(f'{str(n1)} dodać {str(n2)} jest równe {mwynik}') continue Tutaj znajduje się nasz prosty kalkulator, w momencie gdy nasz bot jako drugie słowo usłyszy "policz", zacznie liczyć to co mu każemy. Do wyboru mamy dodawanie, odejmowanie, mnożenie i dzielenie. Istotne tutaj jest to że rodzaj działania rozpoznajemy za pomocą czwartego słowa, czyli gdy powiemy "Marian policz cztery dodać pięć" bot podany tekst zmieni na "marian policz 4 dodać 5" z czego otrzymamy taką tablice słów: ["marian", "policz", "4", "dodać", "pięć"] Pamiętając że elementy w tablicy numerowane są od zera, element 0 uruchamia nam bota, element 1 pozwala na obliczanie prostych rzeczy, element 2 i 5 to podane przez nas liczby, zapisane za pomocą cyfr (dlatego w dalszej części kodu konwertujemy "4", które jest typu string na 4, które jest typu int. Uwaga na pułapki. Mówiąc minus, czasem bot da nam znak "-" a czasem słowo "minus" dlatego musimy w skrypcie umieścić informację że jesli chcemy odejmowac to jeśli czwarte słowo jest myślnikiem lub słowem minus. elif txt != 0 and (txt[3] == '-' or txt[3] == 'minus'): jeśli kogoś zastanawia czym jest txt != 0 to chodzi tu o to że nasz rozpoznany tekst nie może być pusty. Na koniec, wyłączanie bota po naszej nieaktywności głosowej. except: time.sleep(sleep_time) speak("Skoro nic nie mówisz to wyłączam się. Bywaj") break Jeśli nasz asystent głosowy nie wykrył naszego głosu przez ustawioną przez nas wcześniej ilość czasu, to się automatycznie wyłączy. Na samym końcu chciałbym jeszcze wspomnieć o kilku bolączkach tego bota, nie jest on idealny, i ma kilka wad. Głównym problemem jest to że bot w takim stanie w jakim jest obecnie (wersja 1.0), potrafi płynnie wykonać 3 lub 4 polecenia, następnie jego reakcje na kolejne polecenia drastycznie spadają. Marian na samym początku miał posłużyć do automatyzacji pewnych zadań, głosem dlatego też zdecydowałem się na taką formę interakcji za pomocą if else jak widzicie wyżej. W trakcie pisania kodu, pomysł ewoluował i powstał amatorski asystent głosowy, który przerósł moje najśmielsze oczekiwania, dlatego pomimo tego że płynnie wykonuje 3 lub 4 polecenia to nadal jest wart uwagi. Kolejnym problemem jest rozpoznawanie mowy, nie wiem jak to wygląda w przypadku innych języków, ale po polsku czasem zaczyna wariować, np. w przypadku podawania komendy "Marian policz dwa dodać dwa", rozpoznawanie głosu zapisuje "dwa" słownie a nie cyfrą, co powoduje problem. Nie jest to oczywiście wina bota, tylko specyfika działania biblioteki z której korzysta. Myślę że samo zmienianie tekstu na mowę oraz rozpoznawanie głosu może mieć szereg zastosowań, np. interaktywna stacja pogodowa, lub mówiący czujnik ruchu, dlatego liczę na to że artykuł pobudzi waszą kreatywność i wpadniecie na pomysł stworzenia czegoś na podstawie powyższego asystenta głosowego, a może uda wam się rozwiązać jego problemy i umieścić na popularnej malinie, co pozwoli na stworzenie własnej wersji Alexy? Kto wie? Podsyłam również link do filmu na YT, prezentującego aktualne możliwości bota ####################### Coś o mnie: Nazywam się Kacper, jestem studentem Bioinformatyki, programuję w pythonie i interesuje się analizą danych. Na Forbocie nazywam się Ruby a na Githubie i YT nazywam się AvirFrog. Zapraszam na na Github: AvirFrog/Marian, jeśli spodobał Ci się mój pomysł na asystenta głosowego zostaw gwiazdkę. Zapraszam również na kanał YT: AvirFrog, aktualnie znajduje się tam film z prezentacją możliwości przedstawionego tu bota głosowego Marian, ale w przyszłości pojawią się kolejne filmy z nowymi projektami, oraz omówienia kolejnych wersji Mariana. Dziękuję za przeczytanie całego artykułu, w razie pytań odnośnie bota, piszcie pod artykułem, chętnie na nie odpowiem. Pozdrawiam Ruby #######################
  4. Ten artykuł jest częścią serii "Tworzenie gier" #1 - Praca w game-dev'ie (właśnie to czytasz) #2 - Zanim zaczniesz pisać kod... #3 - Przygotowania #4 - Wreszcie coś piszemy? #5 - API i praktyki O czym jest ta seria artykułów? Ta seria artykułów opisuje wiele informacji z zakresu branży GameDev (głównie gry mobilne) - relacje programisty z klientem, relacje z wydawcą, silniki gier, stosowane praktyki czy wygląd zespołu dla danego tytułu. O mnie Cześć, jestem Patryk, od 17 sierpnia 2017 roku pracuję jako game developer, początkowo jako supporter, aktualnie jako lead. Aktualnie jestem samo-zatrudniony (bo to znacząco ułatwia rozliczenia i ewentualne zlecenia "z doskoku" dla innych klientów). W międzyczasie prowadzę kursy programowania czy korepetycje z określonych zagadnień (język C#). 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 » Moja historia Słynny paragraf każdego takiego artykułu... - jedna osoba mnie prosiła o to, bym opisał, więc proszę 🙂 Moja historia z programowaniem zaczyna się w połowie 2012 roku, kiedy pobrałem i zainstalowałem grę MineCraft. Po krótkim czasie zacząłem pobierać mody i natrafiłem na mod o nazwie "ComputerCraft" (LUA), który jak nazwa wskazuje dodaje do gry programowalne komputery. Miałem wtedy 13/14 lat i była to niezwykła zabawa by stworzyć sobie jakiś system wyświetlający np. stan danego przedmiotu w skrzynkach. Potem poznałem Mateusza (z którym niestety się pokłóciłem, zdarza się), który nakłonił mnie do sprawdzenia VB.NET, którego liznąłem, ale nie polubiłem, po czym błyskawicznie przeskoczyłem na C#. Tutaj początki były trudne - z języka takiego jak LUA przenieść się na C#, który jest skrzatem powstałym z połączenia C i Javy... no cóż nie szło najlepiej... Wreszcie znalazłem silnik Unity3D, który dał mi coś, co łatwo mogło obrazować efekt mojej pracy. Trzeba było wymyślić jakiś prosty projekt na początek. Tak to słowo zostało celowo pogrubione... Tak jest to mocny sarkazm... Ogólnie padło na mocno rozbudowany system ekwipunku RapidVentory. Był to pierwszy pseudo-komercyjny projekt wydany na Unity Asset Store (obecnie niedostępny). Warto też nadmienić, że kompletnie nie potrafiłem pisać w C# i to były moje początki. Projekt zajął około dwóch miesięcy, po czym został wydany. Uzyskał kilkanaście tysięcy pobrań i średnią ocenę na poziomie 4 gwiazdek (pierwszy projekt!). To dało mi motywację, by uczyć się dalej. Po RapidVentory postanowiłem zrobić sobie system do emulacji elektryczności (podobny z Fallouta 4) - tak powstało uLectricity, obecnie również zdjęte ze sklepu. Projekt powstawał aż półtora tygodnia... Tak przeczytaj jeszcze raz - PÓŁTORA TYGODNIA. Posiadał podstawowe narzędzia - przełączniki, baterie, kable (przewody), potem doszedł do niego system CCTV czy pseudo-wifi. No dobra ale co dalej? Dalej długo nic... pustka - głównie robiłem pomniejsze projekty w czystym C# - czy to jakieś prototypowe API do ogarniania tłumaczeń itp, większość wyrzucona do kosza, gdyż nie spełniały moich nowych "wymagań jakościowych". Potem matura... Dla dociekliwych - 77% z rozszerzonej fizyki i 74% z rozszerzonej informatyki (nie żeby ktoś pytał, ale jak już się tak rozpisałem to warto wspomnieć). Teraz nastaje magiczny sierpień 2017 roku - na profilu społecznościowym Unity dostaję wiadomość, czy nie chcę pracować w GameDevie. Niestety planowałem iść na studia, ale zainteresowałem się ofertą (jako, że miałem dopiero 16 lat, to prawa jazdy nie było). Okazało się, że Mirek, który do mnie napisał (i do tej pory jest moim głównym klientem) ma firmę w Sochaczewie i postanowił, że poszuka kogoś z okolic. Przyjechał do mnie do domu, gdzie podpisaliśmy NDA i omówiliśmy warunki współpracy. Moja początkowa pensja wynosiła minimalną krajową (13zł/h), ale to zawsze własny grosz 🙂 Poza tym warto było, bo mogłem się rozwijać, a przy okazji zarobić na hobby 🙂 No i tak przez pewien czas pracowaliśmy, potem przez chwilę pracowaliśmy w trybie projektowym, potem znów wróciliśmy do rozliczenia godzinowego, bo łatwiej zarządzać funduszami. W międzyczasie były osobne projekty (głównie strony internetowe - wewnętrzne) dla kilku klientów. Teraz pracuję głównie jako Lead Game Developer, a moim głównym zajęciem jest projektowanie prototypów Hyper Casual'i. I moja stawka to 35 zł za godzinę plus premie (ale o tym kawałek dalej). Znudzeni? No to trzeba znaleźć coś ciekawszego... No to może o pieniądzach? Jak już wcześniej wspomniałem początkowa stawka to zwykle minimalna krajowa (o ile nie masz wykształcenia wyższego - ja dopiero byłem świeżo po maturze), aczkolwiek jak szefem nie jest (tutaj wpisz nazwę pewnej polskiej firmy której nazwa zaczyna się na A a kończy na sseco Polska), czy jakiś inny korpo-[cenzura], który wyzyskuje pracowników, to możesz liczyć na godziwe warunki pracy i płacy. Potem poprosiłem Mirka o podwyżkę i obecnie dostaję 35 zł + VAT (B2B). Do tego dochodzi premia od zysku z wydanych tytułów. A tutaj bywa różnie - zależnie od wydawcy. Przykładowe wydane tytuły to Juice Farm, Idle Fish Aquarium czy Crash Drivers. O relacjach z wydawcami będzie w kolejnym artykule z tej serii. W skrócie można powiedzieć, że wydawca dzieli się z klientem zyskiem, a ja od tego zysku mam procent jako premię (w dużym uproszczeniu). Technicznie wychodzi około 100 zł B2B za godzinę pracy 🙂 (rekord to było szybkie zlecenie na debug kodu, za które zarobiłem 50$ w ciągu 2 minut, ale to wyjątek potwierdzający regułę). Zespół game-dev Zespół game-dev dzieli się u nas na programistę, grafika i animatora / testera (jako, że ma najmniej roboty to też odpowiada za testowanie). Każdy ma swój zakres obowiązków (o który czasami się kłócimy 😄). Ogólna atmosfera panująca u nas jest przyjazna, a jak ktoś kogoś przysłowiowo wku*** to można spokojnie mu wygarnąć i nikt nie będzie zły. Cenimy szczerość - jak ktoś coś zepsuje to należy go skrytykować, jak zrobi dobrze to pochwalić. W ten sposób powstaje motywacja, by pracować jak najlepiej. Źródło: Photo by fauxels from Pexels Teraz chwilę o podziale obowiązków - programista odpowiada za pisanie kodu - głównie skryptów oraz części animacji (o tym będzie w omówieniu tworzenia przykładowej gry). Grafik odpowiada za grafikę - tekstury i modele. Animator - animacje i testowanie, czasem umieszczanie grafiki na scenie (niekiedy programista to robi, niekiedy animator zależy od złożoności gry i czasu potrzebnego na pisanie kodu). Nad wszystkim czuwa szef, który nadzoruje postęp nad wydaniem gry. W większości małych firm, takich jak ta, dla której pracuję jest około 10-20 pracowników. Dlatego też wszyscy dobrze się znamy, co daje nam przyjacielską atmosferę, czasem ogarnie się jakąś imprezę (czy to sylwestra, czy jakąś inną). Ogólnie im mniejsza firma tym mniejszy wyzysk pracownika - podobnie jak w każdym innym zawodzie. Jak wygląda praca z wydawcą? Na początku należy stworzyć prototyp gry (niekoniecznie musi działać). Na podstawie tego prototypu należy nagrać reklamę pod CTR (click-through-rate) test. Jeżeli reklama będzie miała odpowiednią klikalność (dla wydawcy z którym obecnie współpracujemy to ok. 3%+), to przechodzimy to etapu CPI, który jest śmiercią i pożogą dla tytułów. Przejście jego praktycznie gwarantuje wydanie gry. Testy CPI polegają na określeniu kwoty, którą należy wydać na reklamę, by uzyskać jedną instalację aplikacji. Tutaj idealny wynik to około 0.2-0.3$/instalację, jednakowoż często tytuły z 0.5$/instalację również są wydawane. Oczywiście w tym przypadku gra musi już działać. Po teście CPI (o ile uda się go przejść) jest test CPI + D1, który określa jak bardzo gracze przywiązują się do gry - ile osób kontynuuje rozgrywkę po pierwszym dniu. Tutaj najlepiej, by było to minimum 30% retencji. Podsumowując - CTR 3%+, CPI 0.2$ i D1 30% i możesz podejmować współpracę z wydawcą. Oczywiście dostęp do panelu ze statystykami jest na bieżąco, niestety z "wiadomych powodów" nie mogę udostępnić żadnych zrzutów ekranu etc. 🙂 No chyba, że kogoś stać by opłacić NDA... Podział zysków - jak wygląda premia? Wcześniej wspomniałem o premii od wydania gry na podstawie jej zarobków - nie każda firma to stosuje, aczkolwiek u nas rekompensuje to niskie wynagrodzenie (i to dość solidnie). Ogólnie wydawca z klientem dzieli się 50/50. Jest to standard branżowy, czasem można spotkać podział 30/70 (w każdą ze stron). Potem mój klient dzieli się z każdą osobą zaangażowaną w projekt określoną stawką procentową z zysków - dla przykładu 5% (nie podam rzeczywistych z "wiadomych powodów"). Wtedy można łatwo przeliczyć, że wynikowa stawka to ok. 2.5% z pieniędzy, które zarobi gra. Teraz przechodzimy do zarobków tytułów - proste tytuły zarabiają ok. 1 000$/tydzień (mało pobrań). A dla przykładu popularny hyper-casual może zarabiać tyle, że wydawca miesięcznie na reklamę przeznacza około 1e6$ (specjalnie zapisane w notacji). Czyli zakładając że zysk wydawcy przewyższa koszt reklamy to możesz otrzymać nawet 25000 USD premii miesięcznie. Niestety życie nie jest kolorowe i zrobienie takiego tytułu jest jak wygranie w totka. Dodatkowo też nie każda firma game-dev stosuje takie praktyki, niektóre po prostu płacą 100 zł za godzinę i idź pan sobie pracuj gdzie indziej jak chcesz. Sprzęt Tutaj też warto nadmienić, że w pracy często wykorzystuje się własny sprzęt, aczkolwiek pensje są na tyle przyjazne, że się to zwraca. Dodatkowo nie odpowiada się za cudze przedmioty 🙂 W przypadku np. gdy potrzebna jest kompilacja aplikacji na iOS (która może być wykonana praktycznie tylko na OSX, z paroma trikami na Windowsie), to musisz mieć np. Maca Mini (3700 PLN) oraz licencję Apple Developer (400 PLN). To są spore koszty jak na jedną platformę. W tym przypadku u nas funkcjonuje zasada, że wpisujemy do task listy "wykonać builda na iOS" i wysyłamy wiadomość do osób z sprzętem Apple, które robią to "przy okazji" (jak np. idą zrobić obiad). Jak wygląda dalszy proces tworzenia gry? Jeżeli gra na siebie zarabia, to wydawca zleca jej rozbudowę - dodawanie filarów rozgrywki, nowych mechanik, poziomów etc. Jeżeli nie zarabia to przechodzi w tzw. "organikę" i zarabia wyłącznie z naturalnego ruchu graczy (brak reklam gry, brak rozwoju etc.) Jeżeli gra jest popularna to wydawca powołuje sztab analityków i osób, które na bieżąco tworzą content do rozgrywki, ale o tym w kolejnych rozdziałach, gdyż te wiadomości będą bardziej pasowały do nich. W tym rozdziale to wszystko Jeżeli posiadasz jakieś sugestie, pytania do danego rozdziału możesz je śmiało zadawać w komentarzach lub w prywatnej wiadomości (niektóre nie nadają się na komentarze). Postaram się na wszystkie odpowiedzieć, ale nic nie gwarantuję... Przecież też muszę kupić chleb i masło... no od biedy wystarczą bułki 🙂
  5. Wstęp Tak, właśnie ukończyliśmy kurs elektroniki, a może nawet inne kursy. Oto mamy przed sobą pierwszy projekt elektroniczny zbudowany na płytce prototypowej. Wszystko pięknie działa i chcielibyśmy jakoś ten projekt przenieść z płytki prototypowej na coś trwalszego. A jeśli mamy na strychu trochę zbędnego kartonu, to bierzemy nożyczki i taśmę klejącą, aby zrobić „obudowę” na nasz projekt. Po chwili stoi przed nami owoc naszej twórczości. I czujemy, że nie oto chodziło. A gdyby tak zaprojektować obudowę dla naszego projektu w jakimś programie do grafiki 3D. A potem wydrukować. Ten poradnik jest właśnie po to, abyśmy się tego nauczyli. 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 » Nauczymy się Blendera. Po sieci krąży opinia, że blender jest trudny do nauki. Jest w tym sporo prawdy, ale jeśli zaprzyjaźnimy się z klawiaturą, to może okazać się, że nie taki blender straszny, jak go malują. Blender jest potężnym narzędziem, ale my w tym poradniku skupimy się na elementarnych funkcjach tego programu. Nie będziemy generować obrazów ani tworzyć animacji. Nauczymy się podstaw, które pozwolą tworzyć modele trójwymiarowe i to tak, abyśmy mogli wydrukować je w drukarce 3D. Zaczynamy? Ten artykuł składa się z dwóch części. W pierwszej nauczymy się obsługi podstaw interfejsu Blendera. W drugiej wykorzystamy zdobyte umiejętności i zaprojektujemy prosty model 3D, który będziemy mogli wydrukować. Część 1 Instalacja i poruszanie się w przestrzeni trójwymiarowej. Instalacja Blendera jest prosta. Wpisujemy w wyszukiwarce „blender download” i po chwili powinniśmy zobaczyć stronę podobną to tej poniżej. Pobieramy instalator i instalujemy. Jeśli potrzebujemy instalatora dla innego systemu niż wybrany domyślnie to klikamy troszkę niżej i wybieramy odpowiadającą nam opcję. Przy pierwszym uruchomieniu wita nas okno „Quick Setup”. Wybieramy odpowiedni język: Language >> Polski (Polski) i motyw kolorystyczny: Theme >> White. W tym poradniku użyty jest motyw biały, dzięki temu poszczególne animacje są czytelniejsze. Jeśli milszy dla naszych oczu jest ciemniejszy motyw, to możemy go wybrać w preferencjach. Poniższa animacja pokazuje, jak zmienić motyw kolorystyczny programu. Dla naszej wygody możemy zmieniać szerokość poszczególnych okien aplikacji. Jeśli umieścimy kursor myszy na linii dzielącej dwa okna, to zmieni się on w ikonę z dwiema strzałkami. Kliknijmy i przytrzymajmy lewy klawisz myszy (LKM), aby zmienić rozmiar sąsiadujących ze sobą okien. Od tego momenty zaczniemy używać skrótów: LKM – Lewy Klawisz Myszy, PKM – Prawy Klawisz Myszy, ŚKM – Środkowy Klawisz Myszy. Przesuwanie okien zapewne nie jest dla nas zaskakujące. Poszukajmy czegoś ciekawszego. Złapmy prawy górny narożnik. Gdy przesuniemy go w lewo lub w dół to podzielimy okna na dwa. A zatem do dzieła. Podzielmy okna na dwa. A potem jeszcze raz. Każde okno jest niezależne. Jeśli trochę nas poniosło i mamy za dużo okien, to możemy je z powrotem połączyć w jedno. Łączenie okien działa, gdy krawędzie sąsiednich okien mają tą samą długość. Animacja poniżej demonstruje te zagadnienia. Jak już pobawiliśmy się dzieleniem i łączeniem okien to, rozejrzyjmy się trochę w przestrzeni 3D. W głównym oknie widzimy trzy elementy. Jeśli troszkę narozrabialiśmy i mamy za dużo okien, to najprościej będzie utworzyć nowy plik. W tym celu klikamy: Plik>Nowy>General. A zatem, w głównym oknie interesuje nas sześcian – nasz pierwszy obiekt 3D. Pozostałe dwa elementy to kamera i światło. Służą one do generowania obrazów i animacji, więc w tym poradniku ignorujemy je. Kliknijmy ŚKM i przytrzymując go poruszajmy myszką. Och! Tego orbitowania nie da się opisać słowami. Kolejną użyteczną funkcją jest przybliżanie i oddalanie widoku (tzw. Zoom). Uzyskujemy tę możliwość obracając rolką myszy. Gdyby nasza myszka nie była wyposażona w rolkę, to kombinacja Ctrl+ŚKM i ruch myszy w górę oddala widok, a w dół przybliża. Następna przydatna funkcja to kadrowanie. Kombinacja Shift+ŚKM i ruch myszy przesuwa cały widok. Gdyby nam zabrakło myszki, bo na przykład pracujemy na laptopie, to wszystkie poprzednie funkcje możemy uzyskać poprzez ikonki widoczne w prawym górnym rogu. Ikonka lupy to przybliżanie i oddalanie widoku. Ikonka dłoni to kadrowanie. Ikona sfery z zaznaczonymi kierunkami to orbitowanie. Kliknięcie w tej sferze poszczególnych kierunków (x, y lub z) ustawia cały widok w tym kierunku. Do ustawiania podglądu z różnych stron możemy wykorzystać klawiaturę numeryczną. Przetestujemy zachowanie się tych klawiszy klikając po kolei: Numpad_1 – widok od przodu, Numpad_3 – widok od prawej, Numpad_7 – widok od góry. Gdy użyjemy dodatkowo klawisza Ctrl efekty będą następujące: Ctrl+Numpad_1 – widok od tyłu, Ctrl+Numpad_3 – widok od lewej, Ctrl+Numpad_7 – widok od dołu. Mniej agresywne obroty wykonamy klawiszami: Numpad_8 i Numpad_2 – obracanie w pionie, Numpad_4 i Numpad_6 – obracanie w poziomie. Zaś gdy dodatkowo użyjemy Ctrl to będziemy mogli kadrować widok: Ctrl+Numpad_8 i Ctrl+Numpad_2 – przesuwanie widoku w pionie, Ctrl+Numpad_4 i Ctrl+Numpad_6 – przesuwanie widoku w poziomie. Przybliżanie i oddalanie widoku zrealizujemy przy pomocy klawiszy: Numpad_- – oddalanie widoku, Numpad_+ – przybliżanie widoku. Po opanowaniu sztuki poruszanie się w przestrzeni trójwymiarowej należy się nam łyk kawy i chwila przerwy. Obiekty w przestrzeni trójwymiarowej. Zanim zaczniemy tworzyć i edytować obiekty 3D to musimy zrozumieć czym jest kursor 3D. Otóż jest on pewnego rodzaju analogiem do migającej kreski w edytorze tekstu. Tak jak migająca kreska wskazuje miejsce edycji, tak kursor 3D jest punktem odniesienia dla wielu działań. Nowe obiekty powstają właśnie w miejscu, gdzie znajduje się kursor 3D. Aby ustawić kursor 3D w danym punkcie w przestrzeni trójwymiarowej, musimy go najpierw wybrać w lewym panelu. Gdy teraz klikniemy LKM, to ustawimy kursor 3D w określonym punkcie przestrzeni. Możemy też zmieniać położenie kursora 3D bardziej precyzyjnie, podając jego współrzędne. Dostęp do tych współrzędnych będziemy mieli w prawym bocznym panelu. Aby pojawił się, należy nacisnąć klawisz N. A tak przy okazji klawisz T ukrywa lewy panel. Przetestujmy ustawianie kursora 3D wykonując po kolei następujące czynności. Uruchommy „na świeżo” Blendera i naciśnijmy klawisz T (właśnie ukryliśmy lewy panel). Podzielmy główne okno na cztery mniejsze. W lewym górnym oknie ustawmy widok od przodu naciskając klawisz Numpad_1. W prawym górnym oknie ustawmy widok od prawej naciskając klawisz Numpad_3. W lewym dolnym oknie ustawmy widok od góry naciskając klawisz Numpad_7. Kliknijmy LKM w prawe dolne okno, a potem naciśnijmy klawisz T (pojawił się lewy panel) Kliknijmy LKM w ikonę kursora 3D. Teraz klikając LKM gdziekolwiek ustawimy tam kursor 3D. Dokładne położenie możemy obserwować na pozostałych widokach. Upewnijmy się, że nadal aktywne jest lewe dolne okno i naciśnijmy klawisz N (pojawił się prawy panel). Szukamy zakładki „View” i na niej informacje o kursorze 3D a konkretnie o Lokalizacji czyli o współrzędnych x , y i z. Możemy zmienić wartości tych współrzędnych na trzy sposoby: Klikając w strzałki po bokach każdej z nich, Klikając i przytrzymując LKM na wartości pola – przesuwamy w prawo lub w lewo aby zmienić wartość, Klikając wartość i wpisujemy ją ręcznie tzn. z klawiatury. Poniżej znajduje się animacja przedstawiająca omawiane zagadnienia. W miejscu gdzie jest ustawiony kursor 3D możemy dodać nowy obiekt. Dodajmy go poprzez komendę Shit+A. Następnie z menu, które się pojawi wybieramy obiekt, który chcemy dodać. Możemy też, dodać nowy obiekt korzystając z polecenia „Add” widocznego w górnym pasku głównego okna. Jeśli w wyniku euforii kreacji dodamy zbyt dużo obiektów, to możemy je usnąć. Niepotrzebne obiekty usuwamy w ten sposób, że najpierw je zaznaczamy, a potem naciskamy klawisz X lub delete. Jednakże, jeśli nadal mamy aktywną ikonkę kursora 3D, to nie możemy znaczyć żadnego obiektu. Ponieważ klikając LKM na danym obiekcie, ustawiamy tam kursor 3D, a nam chodzi o zaznaczenie obiektu. Musimy więc w lewym panelu kliknąć LKM ikonkę wyboru lub na klawiaturze naciskami klawisz W. Uzyskamy wtedy możliwość zaznaczania obiektów klikając na nie LKM. Jeśli dodatkowo przytrzymamy klawisz Shift, to możemy zaznaczyć kilka obiektów naraz. Gdybyśmy nacisnęli klawisz A, to zaznaczylibyśmy wszystkie obiekty. Podwójne naciśnięcie klawisza A, powoduje anulowanie zaznaczenia, czyli nic nie będzie zaznaczone. Przećwiczmy dodawanie i usuwanie obiektów wykonując poniższe ćwiczenia. Uruchommy Blendera „na czysto”. W lewym panelu klikamy LKM na kursor 3D. W pustym miejscu klikamy LKM aby tam ustawić kursor 3D. Naciskamy Shift+A i wybieramy obiekt który chcemy dodać. Powtarzamy krok 3 i 4 kilka razy. Możemy za każdym razem wybierać inny obiekt. W lewy panelu klikamy LKM na ikonkę wyboru lub na klawiaturze naciskamy klawisz W. Zaznaczamy jeden z obiektów (klikając na niego LKM) i naciskami klawisz X, aby go usunąć. Zaznaczamy kolejny z obiektów i naciskamy klawisz delete, aby go usunąć. Naciskamy klawisz A (zaznaczamy wszystko), a potem klawisz X (usuwamy wszystko). Przechodzimy do kroku 2 i rozpoczynamy zabawę od nowa, aż do znudzenia (tzn. do momentu, aż opanowaliśmy dodawanie i usuwanie obiektów). O i tu należy się nam przerwa na kawę lub kakao. W trakcie przerwy możemy popatrzeć na animację poniżej. Przesuwanie obiektów. Przed przerwą nauczyliśmy się dodawać i usuwać obiekty. Teraz nadszedł czas abyśmy do naszych umiejętności dodali trzy nowe sztuczki, które pozwolą nam modyfikować obiekty. Pierwszą z nich będzie przesuwanie obiektu, drugą obracanie obiektu a trzecią skalowanie obiektu. Przesuwanie obiektu możemy zrealizować na kilka sposobów. Najbardziej naturalny, to zaznaczenie obiektu i naciśnięcie klawisza G. Ruch mysz spowoduje przesunięcie obiektu. Kliknięcie LKM lub klawisza enter spowoduje zatwierdzenie nowej pozycji. Gdybyśmy chcieli zrezygnować z przesuwania (oczywiście przed zatwierdzeniem) to powinniśmy nacisnąć klawisz Esc. Po naciśnięciu klawisza G rozpoczynamy pływanie w trzech wymiarach, a nasza myszka porusza się tylko w dwóch. W zawiązku z tym, przesuwanie obiektu może być nieprecyzyjne. Aby dokładniej kontrolować ruch obiektu możemy ograniczyć się do jednej z trzech osi. Jeśli po naciśnięciu klawisza G naciśniemy jeden z klawiszów X, Y lub Z, to ograniczymy ruch właśnie do jednej osi. Podobny skutek możemy uzyskać, gdy po naciśnięciu klawisza G przytrzymamy ŚKM. Ruch myszki pozwoli nam wybrać jedną oś. Jeśli chcemy precyzyjnie przesunąć obiekt to możemy wpisywać wartości liczbowe z klawiatury. Obiekt przesunie się dokładnie o taką odległość. Wpisanie wartości ujemnej spowoduje przesunięcie do tyłu (tzn. w kierunku ujemnym). Możemy również przesuwać obiekt po wybranej płaszczyźnie np. XY. Wystarczy tylko po naciśnięci klawisza G nacisnąć kombinację Shift+Z. Analogicznie wybranie kombinacji Shift+X (lub Shift+Y) wyłączy oś X (lub oś Y) i będziemy poruszać się po płaszczyźnie YZ. Ugruntujmy zdobytą przed chwilą wiedzę wykonując poniższe ćwiczenia. Uruchamiamy „na świeżo” Blendera. Dzielimy główne okno na cztery mniejsze. W lewym górnym oknie ustawiamy widok od przodu naciskając klawisz Numpad_1. W prawym górnym oknie ustawiamy widok od prawej naciskając klawisz Numpad_3. W lewym dolnym oknie ustawiamy widok od góry naciskając klawisz Numpad_7. Upewniamy się, czy jest zaznaczona kostka w prawym dolnym oknie. Naciskamy klawisz G i przesuwamy gdzieś naszą kostkę potwierdzając LKM. Naciskamy klawisz G a potem klawisz X i przesuwamy naszą kostkę, a następnie zatwierdzamy przesunięcie LKM. Powtarzamy krok 8 dla pozostałych osi (Y i Z). Dodajemy nowy obiekt naciskając kombinację Shiht+A. Jeśli nie ruszaliśmy kursora 3D, to nowy obiekt jest w położeniu (0,0,0). Upewniamy się, czy jest zaznaczony nowy obiekt i klikamy następującą sekwencję klawiszy: G, X, 2, Eneter, G, Y, -3, Enter, G, Z, 2, Enter. Tym sposobem umieszczamy nowy obiekt w położeniu (2,-3,2). Dodajemy nowy obiekt i przesuwamy go używając kombinacji G, Shift+Z, LKM. Powtarzamy krok 12 dla pozostałych płaszczyzn, czyli używając kombinacji G, Shift+Y (lub G, Shift+X). Poniżej znajduje się przykładowa realizacja tych ćwiczeń. Drugim sposobem na przesuwanie obiektu to wykorzystanie lewego panelu. Jeśli klikniemy na trzecią ikonę, tą poniżej ikony kursora 3D, to na obiekcie pojawi się „manipulator”. Używając LKM możemy przesuwać obiekt wzdłuż wybranej osi lub płaszczyzny. Najprościej to zrozumiemy na przykładzie, więc przechodzimy to czystego Blendera i postępujemy zgodnie z animacją poniżej. Trzeci sposób na zmianę położenia obiektu to wykorzystanie prawego panelu. Gdy naciśniemy klawisz N, to pojawi się prawy panel. Szukamy na nim zakładki Item, a na tej zakładce pola Lokalizacja. Możemy teraz zmieniać położenie obiektu analogicznie jak zmienialiśmy położenie kursora 3D. Zwróćmy uwagę na ikonki kłódek. Zamknięcie którejś z nich blokuje możliwość ruchu obiektu wzdłuż danej osi przy użyciu klawisza G lub „manipulatora”. Jednakże nadal możemy tą wartość zmieniać w zakładce. Jeden obraz zastępuje tysiąc słów, więc obejrzyjmy poniższą animację, aby wszystko się stało dla nas zrozumiałe. Obracanie obiektów. Kolejna niezbędna umiejętność, to obracanie obiektów. Podobnie jak przesuwanie obiektów mogliśmy realizować na kilka sposobów, tak obracanie możemy wykonać na kilka analogicznych sposobów. Pierwszy sposób to użycie skrótu klawiaturowego, czyli naciskamy klawisz R. Podobnie jak przy przesuwaniu mogliśmy ograniczyć się do jednej osi, tak i przy obrotach kliknięcie klawisza X (lub Y, lub Z) ogranicza obrót do jednej osi. A gdy po wybraniu osi wpiszemy wartości liczbowe z klawiatury to spowodujemy obrót o zadany kąt. Jest jednak trochę różnic między przesuwaniem a obracaniem. Po pierwsze: intensywność obrotów zależy od odległości od osi obrotu. Po drugie: gdy naciśniemy klawisz R, Blender wybierze za nas oś obrotu. Możemy ponownie nacisnąć klawisz R, wtedy przełączymy się tryb obrotu dowolnego (zwróćmy uwagę, że wtedy kursor myszy zmienia się w kolorowe strzałki). Drugi sposób na obracanie obiektu to wykorzystanie lewego panelu. Jeśli klikniemy na czwartą ikonę, to na obiekcie pojawi się „manipulator”. Używając łuków manipulatora możemy obracać naszym obiektem. Trzeci sposób w jaki możemy obrócić obiekt to wykorzystanie prawego panelu. Dla przypomnienia: naciskamy klawisz N. Teraz na zakładce Item szukamy pola Rotacja. I chyba już jasne co dalej. Jeśli nie to obejrzyjmy animację poniżej. Skalowanie obiektów. Skalowanie obiektów to ostania niezbędna umiejętność, którą musimy poznać przed tworzeniem projektów. Aby skalować obiekty należy nacisnąć klawisz S. Podanie wartości liczbowej powoduje ustalenie skali. Na przykład wpisanie 0.5 spowoduje dwukrotne zmniejszenie obiektu, zaś wpisanie 3 zwiększy trzykrotnie obiekt. Analogicznie jak poprzednio klawisze X, Y, Z ograniczają oś skalowania. Bardzo przydatna może się okazać kombinacja Shfit+X (Shift+Y, Shift+Z). Użycie tych kombinacji wyłączy daną oś. Będziemy wtedy skalować dwa wymiary pozostawiając trzeci nie zmieniony. Podobnie jak przesuwanie i obracanie, skalowanie możemy zrealizować wykorzystując lewy panel. Kliknięcie LKM na ikonkę skalowania (piąta od góry) włączy odpowiedni „manipulator”, który pozwoli nam zmniejszać lub powiększać nasz obiekt. Także na prawym panelu w zakładce Item możemy zmieniać rozmiar obiektów wpisując odpowiednie wartości w polu Skala. Animacja poniżej przedstawia różne metody skalowania. Część 2 Pierwszy projekt. Naszym pierwszym projektem będzie coś praktycznego, mianowicie pudełko na płytkę stykową. Nasze pudełko powinno mieć jeszcze przegródki w których moglibyśmy chować przewody. Na obrazku poniżej jest wizualizacja naszych marzeń. W lewej części naszego pudełka jest miejsce na płytkę stykową. Użyjemy płytki stykowej o 830 otworach. Jej wymiary wynoszą 165mm długości i 53mm. Uwzględniając pewne wypustki z jednej strony płytki oraz dodając odrobinę luzu na niedokładności, możemy przyjąć, że wymiary wewnętrzne lewej komory powinny wynosić 165mm długości i 57 mm szerokości. Ustalmy grubość ścianek na 1mm. Wysokość pudełka ustawimy na 16mm. Domyślnie Blender podaje wymiary w metrach, ale możemy to zmienić. Jeśli chcemy ustawić wymiarowanie na milimetry, to przechodzimy z głównego okna na prawe dolne okno i szukamy zakładki „Scene Proporties”. Następnie na tej zakładce rozwijamy „Units” i w polu „Length” wybieramy odpowiednią opcje, czyli „Milimeters”. Od tej pory wymiary podawane są w milimetrach. Pora zacząć budować nasze pudełko. Zaczniemy od ścianki z przodu. Zbudujemy ją z kostki. Kostka ma domyślnie wymiary 2000mm x 2000mm x 2000m. Przeskalujemy więc ją tak, aby uzyskać ściankę o wymiarach 1mm x 58mm x 16mm. Następnie umieścimy ją w odpowiednim położeniu. A zatem do dzieła: Uruchamiamy na świeżo Blendera. Zmieniamy jednostki z metrów na milimetry. Zaznaczamy kostkę. Skalujemy ją wzdłuż osi X, aby nadać jej grubość 1mm (musimy użyć skali równej 0,0005, gdyż 1mm/2000mm=0,0005). Tak, więc sekwencja klawiszy będzie następująca: S X 0,0005 Enter. Następnie skalujemy ją wzdłuż osi Y (skala = 58/2000=0,029): S Y 0,029 Enter. Zmieniamy wysokość ścianki na 16mm: S Z 0,008 Enter. Fajnie, ale gdzie się podziała nasza zmieniona kostka? Ach tak teraz jest malutka, więc musimy się do niej przybliżyć. Przesuwamy ją do odpowiedniego położenia: G X 83 Enter, G Z 8 Enter. Zmieniamy nazwę z „Cube” na „Ściana przód”. Aby zmienić nazwę obiektu, klikamy dwukrotnie LKM w prawym górnym oknie na nazwie. Zapisujemy nasz projekt na dysku. Naciskamy Ctrl+S i w oknie dialogowym wybieramy miejsce i nazwę pliku np. Pudełko. Po tych operacjach powinniśmy otrzymać coś takiego jak na grafice poniżej. Ściana z tyłu ma takie same wymiary jak ściana z przodu. Możemy więc skopiować przednią i przesunąć w odpowiednie miejsce. Do kopiowania, a raczej DUPLIKOWANIA służy skrót Shift+D. Po jego wykonaniu uzyskujemy obiekt, który jest w trybie przesuwania. Możemy więc od razu ustawić nowy obiekt w odpowiednim miejscu. Zaznaczmy obiekt „Ściana przód”. Duplikujemy ja i od razu przesuwamy do odpowiedniej pozycji. Shift+D X -166 Enter. Zmieniamy nazwę na „Ściana tył”. Zapisujemy projekt. Zanim przejdziemy dalej należy nam się króciutkie wyjaśnienie dlaczego przesuwamy ściany wzdłuż osi X do pozycji 83mm i –83mm. Długość wewnętrzna lewej komory powinna wynosić 165mm, a tu mamy 83mm – (–83mm) = 166mm. Otóż musimy uwzględnić grubość ścianek 1mm. No dobrze, to dlaczego dodajemy tylko 1mm skoro są dwie ścianki i każda ma 1mm grubości? Ponieważ Blender określa położenie danego obiektu podając jego punkt Origin. Dla kostki ten punkt jest domyślnie w środku. Zatem odległość między punktami Origin wynosi 166mm, a odległość między brzegami ścianek wynosi: 166mm – 1mm/2 – 1mm/2 = 165mm. A taką chcieliśmy mieć długość wewnątrz pudełka. Na tej samej zasadzie przesuwamy ścianki do góry o 8mm (wzdłuż osi Z), aby spód tych ścianek leżał na poziomie zerowym. Lewa ścianka powinna mieć wymiary 166mm x 1mm x 16mm. Dodajmy lewą ścianę wykonując poniższe kroki. Umieszczamy 3D Cursor w punkcie (0,0,0). Dodajemy nową kostkę. Shift+A. Skalujemy ją do odpowiednich wymiarów; S X 0,083 Enter, S Y 0,0005 Enter, S Z 0,008 Enter. Przesuwamy na lewo i podnosimy ją do góry: G Y -29 Enter, G Z 8 Enter. Zmieniamy jej nazwę na „Ściana lewa”. Zapisujemy projekt: Ctrl+S. Prawa ścianka będzie trochę niższa od lewej, gdyż na niej ustawimy „prowadnice”, dzięki którym będziemy mogli zamykać lewą komorę. Co zabezpieczy przewody przed wysypaniem z pudełka. Wymiary prawej ścianki będą zatem równe 166mm x 1mm x 14mm. Umieszczamy 3D Cursor w punkcie (0,0,0). Dodajemy nową kostkę. Shift+A. Skalujemy ją do odpowiednich wymiarów; S X 0,083 Enter, S Y 0,0005 Enter, S Z 0,007 Enter. Przesuwamy na prawo i podnosimy ją do góry: G Y 29 Enter, G Z 7 Enter. Zmieniamy jej nazwę na „Ściana prawa”. Zapisujemy projekt: Ctrl+S. Podłoga pod lewą komorą jest ażurowa, pozwoli nam to swobodnie wkładać i wyjmować płytkę stykową. Podłoga ta składa się z sześciu elementów. Dodajmy je po kolei wykonując poniższe instrukcje. Umieszczamy 3D Cursor w punkcie (0,0,0). Dodajemy nową kostkę. Shift+A. Skalujemy ją do odpowiednich wymiarów (10mm x 58mm x 1mm); S X 0,005 Enter, S Y 0,029 Enter, S Z 0,0005 Enter. Przesuwamy ją do przodu i podnosimy odrobinkę do góry: G X 78 Enter, G Z 0,5 Enter. Zmieniamy jej nazwę na „Podłoga przód”. Upewniamy się, czy zaznaczony jest obiekt „Podłoga przód”. Duplikujemy ją i nowy obiekt przesuwamy na odpowiednie miejsce. Shift+D, X -156 Enter. Zmieniamy jej nazwę na „Podłoga tył”. Zaznaczamy obiekt „Podłoga przód”. Duplikujemy ją i nowy obiekt przesuwamy na środek. Shift+D, X -78 Enter. Zmniejszamy jej szerokość. S X 0,6 Enter. Zmieniamy jej nazwę na „Podłoga środek”. Zapisujemy projekt: Ctrl+S. Dodajemy nową kostkę. Shift+A. Skalujemy ją do odpowiednich wymiarów (166mm x 5mm x 1mm); S X 0,083 Enter, S Y 0,0025 Enter, S Z 0,0005 Enter. Przesuwamy ją na lewo i podnosimy odrobinkę do góry: G Y -26,7 Enter, G Z 0,5 Enter. Zmieniamy jej nazwę na „Podłoga lewa”. Upewniamy się, czy zaznaczony jest obiekt „Podłoga lewa”. Duplikujemy ją i nowy obiekt przesuwamy na prawo. Shift+D, Y 53,4 Enter. Zmieniamy jej nazwę na „Podłoga prawa”. Zaznaczamy obiekt „Podłoga lewa”. Duplikujemy ją i nowy obiekt przesuwamy na środek. Shift+D, Y 26,7 Enter. Zmieniamy jej nazwę na „Podłoga środek poprzeczny”. Zapisujemy projekt: Ctrl+S. Nasza pudełko zaczyna nabierać konkretnych kształtów. Sprawdźmy czy wszystko wygląda tak jak poniżej. Jeśli wszystko jest prawidłowo, to przejdźmy to prawej komory. Zacznijmy od podłogi. Ustalmy jej szerokość na 4cm, wtedy jej wymiary będą wynosić: 166mm x 41mm x 1mm. Dodatkowy milimetr jest po to by podłogi z lewej i prawej komory się połączyły. Umieszczamy 3D Cursor w punkcie (0,0,0). Dodajemy nową kostkę. Shift+A. Skalujemy ją do odpowiednich wymiarów: S X 0,083 Enter, S Y 0,0205 Enter, S Z 0,0005 Enter. Przesuwamy ją na prawo i troszkę do góry: G Y 49,5 Enter, G Z 0,5 Enter. Zmieniamy jej nazwę na „Podłoga przewody”. Zapisujemy projekt: Ctrl+S. Ścianka z przodu prawej komory musi być trochę niższa niż pozostałe ścianki, gdyż będziemy z tej strony wsuwali „wieczko” zamykające tą komorę. Ponadto jej szerokość zwiększymy o 1mm w porównaniu z podłogą. Dzięki temu ściany będą na siebie nachodzić. Ustalmy zatem jej wymiary na 1mm x 41mm x 12mm. Tylna ścianka będzie się różnić wysokością: 1mm x 41mm x 16mm. Boczna ścianka (ta najbardziej po prawo) będzie taka sama jak „ściana prawa” tylko przesunięta jeszcze bardziej na prawo. Dodajmy zatem te ścianki wykonując kroki poniżej. Umieszczamy 3D Cursor w punkcie (0,0,0). Po raz kolejny dodajemy kostkę. Shift+A. Skalujemy ją do wymiarów 1mm x 41mm x 12mm: S X 0,0005 Enter, S Y 0,0205 Enter, S Z 0,006 Enter. Przesuwamy ją w odpowiednie miejsce: G X 83 Enter, G Y 49,5 Enter, G Z 6 Enter. Zmieniamy jej nazwę na „Ściana przód przewody”. Ponownie dodajemy kostkę. Shift+A. Skalujemy ją do wymiarów 1mm x 41mm x 16mm: S X 0,0005 Enter, S Y 0,0205 Enter, S Z 0,008 Enter. Przesuwamy ją w odpowiednie miejsce: G X -83 Enter, G Y 49,5 Enter, G Z 8 Enter. Zmieniamy jej nazwę na „Ściana tył przewody”. Zaznaczamy obiekt: „Ściana prawa”. Duplikujemy ją i nowy obiekt przesuwamy na prawo. Shift+D, Y 40,5 Enter. Zmieniamy jej nazwę na „Ściana prawa przewody”. Zapisujemy projekt: Ctrl+S. Przegrody w prawej komorze muszą być takiej samej wysokości jak „ściana przód przewody”. Obowiązkowa jest środkowa przegroda. Gdyby jej nie było to „wieczko” by się zapadało. Te mniejsze przegrody nie są konieczne. Dodamy tylko jedną. Wymiary przegrody środkowej mają wymiary: 166mm x 1mm x 12mm, zaś mniejsza ma wymiary: 1mm x 20mm x 12mm. Dodajmy zatem te przegrody. Umieszczamy 3D Cursor w punkcie (0,0,0). Po raz ósmy dodajemy kostkę. Shift+A. Skalujemy ją do wymiarów 166mm x 1mm x 12mm: S X 0,083 Enter, S Y 0,0005 Enter, S Z 0,006 Enter. Przesuwamy ją na środek prawej komory: G Y 50 Enter, G Z 6 Enter. Zmieniamy jej nazwę na „Przegroda”. I jeszcze raz dodajemy kostkę. Shift+A. Skalujemy ją do wymiarów 1mm x 20mm x 12mm: S X 0,0005 Enter, S Y 0,01 Enter, S Z 0,006 Enter. Przesuwamy ją gdzieś w okolicach prawej przegrody np: G X 16 Enter, G Y 59,5 Enter, G Z 6 Enter. Zmieniamy jej nazwę na „Przegroda mała”. Zapisujemy projekt: Ctrl+S. Pora na „Prowadnice”, czyli elementy trzymające „wieczko”. Te prowadnice to belki o wymiarach 164mm x 2mm x 2mm. A zatem bez zbędnych słów: Umieszczamy 3D Cursor w punkcie (0,0,0). Po raz dziewiąty dodajemy kostkę. Shift+A. Skalujemy ją do wymiarów 164mm x 2mm x 2mm: S X 0,082 Enter, S Y 0,001 Enter, S Z 0,001 Enter. Ustawiamy ją na „Ściana prawa”: G X -1 Enter, G Y 29,5 Enter, G Z 15 Enter. Zmieniamy jej nazwę na „Prowadnica pierwsza”. Zaznaczamy obiekt „Prowadnica pierwsza”. Duplikujemy ją i przesuwamy na prawo. Shift+D, Y 39,5 Enter. Zmieniamy jej nazwę na „Prowadnica druga”. Zapisujemy projekt: Ctrl+S. Pięknie, sprawdźmy czy wszystko wygląda tak jak poniżej. Jeśli tak to pora na łyk kawy (jeśli jeszcze nie jest zimna) i na ostatni element czyli „wieczko”. Składa się ono z dwóch obiektów. Pierwszy do przykrywka o wymiarach 165mm x 38,7mm x 1,2mm. Drugi to „rączka” o wymiarach 2mm x 38,7mm x 3,6mm. A więc do dzieła: Umieszczamy 3D Cursor w punkcie (0,0,0). Po raz dziesiąty dodajemy kostkę. Shift+A. Skalujemy ją do odpowiednich rozmiarów: S X 0,0825 Enter, S Y 0,01935 Enter, S Z 0,0006 Enter. Ustawiamy ją gdzieś z lewej strony: G Y -50 Enter, G Z 0,6 Enter. Zmieniamy jej nazwę na „Przykrywka”. Ostatni raz dodajemy kostkę. Shift+A. Skalujemy ją do właściwych rozmiarów: S X 0,001 Enter, S Y 0,01935 Enter, S Z 0,0018 Enter. Ustawiamy ją na brzegu „Przykrywki”: G X 82,5 Enter, G Y -50 Enter, G Z 1,8 Enter. Zmieniamy jej nazwę na „Rączka”. Zapisujemy projekt: Ctrl+S. I jeśli wszystko wygląda tak jak poniżej, to właśnie skończyliśmy pierwszy projekt w Blenderze. Co dalej? Możemy nasz projekt wyeksportować i wydrukować. Aby wyeksportować projekt klikamy w menu Plik>Export>Stl(.stl). Jeśli dysponujemy drukarką 3D, to możemy go wydrukować. Poniżej są zdjęcia przedstawiające wydrukowany projekt. Podsumowanie Udało nam się zaprojektować funkcjonalny gadżet. Zapoznaliśmy się z program służącym do tworzenia grafiki 3D. Warto mieć jednak świadomość, że w tym poradniku są omówione elementarne zagadnienia. Prawdopodobnie profesjonalni użytkownicy tworząc takie pudełko jak my, wykazaliby się większą finezją. Na przykład do stworzenia jednego modelu użyliśmy kilkunastu obiektów. A należałoby to pudełko wymodelować z jednego obiektu. Tak więc ten poradnik należy traktować jako wstęp do nauki Blendera. Autor: Grzegorz Gągała Pudełko.zip
  6. 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. 😉
  7. Ten artykuł jest częścią serii "Budowa frezarki CNC na stalowej ramie sterowanej z komputera PC" #1 - Mechanika #2 - Sterowanie i uruchomienie Wstęp Link do drugiej części na końcu tego artykułu. W niniejszym artykule chciałbym opisać proces budowy frezarki CNC, którą zbudowałem kilka lat temu jeszcze podczas studiów. Zdecydowałem się na budowę takiej maszyny ze względu na częstą potrzebę wytworzenia jakichś części lub na precyzyjne wycinanie otworów w obudowach do czego nie mam cierpliwości, a te parę lat temu druk 3D dopiero się rozkręcał i jakość ówczesnych drukarek mnie nie satysfakcjonowała. Ponadto budowa frezarki była swojego rodzaju sprawdzeniem własnych umiejętności jak i wyobraźni. Między innymi umiejąc już wtedy projektować w Autodesk Inventor umyślnie ograniczyłem projektowanie do pojedynczych części, chcąc się sprawdzić na ile jestem w stanie poukładać sobie projekt w głowie nie wspomagając się niczym więcej. Po zakończeniu budowy oczywiście znalazłoby się coś do poprawy natomiast efekt końcowy i tak przerósł moje oczekiwania oraz cieszył się dużym uznaniem innych osób. Podczas budowy wykonywałem na bieżąco kosztorysy, zapisywałem linki do części oraz zapisałem sobie wszystkie potrzebne rzeczy dzięki czemu mogłem wykorzystać ten projekt do napisania pracy inżynierskiej. Na potrzeby pracy inżynierskiej wykonałem projekt 3D zbudowanej już frezarki. Niniejszy artykuł nie stanowi kompletnej instrukcji budowy frezarki od podstaw natomiast ma na celu przedstawić jak ten proces budowy wyglądał, dlatego w tym artykule mogę częściej korzystać z czasu przeszłego pierwszej osoby w liczbie pojedynczej. 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 » Początkowo artykuł ten miałem napisać w jednej części, natomiast wyszedł obszerniejszy niż sądziłem i wyszło mi tutaj więcej zdjęć, a boję się że zaraz może się wyczerpać limit miejsca na zdjęcia zatem artkuł został podzielony na 2 części - mechaniczną oraz elektroniczną z konfiguracją Mach3 oraz przygotowaniem frezowania na tej maszynie. Wstępne założenia Pierwszym i najważniejszym założeniem były gabaryty całej maszyny. Początkowo miała ona dysponować polem roboczym ok. 300x300x150 mm kolejno w osiach X, Y, Z. Jednak po wstępnym kosztorysie okazało się iż najkosztowniejsze podzespoły takie jak nakrętki śrub kulowych, czy łożyska liniowe wystąpią w takiej samej ilości również w maszynie o większych gabarytach, a materiał potrzebny do budowy ramy, bramy i innych elementów konstrukcyjnych jest na tyle tani, że można sobie pozwolić na budowę większej maszyny nie zwiększając znacząco budżetu. Zatem nowe założenie obejmowało wymiary zewnętrzne maszyny w przybliżeniu 1000x1000x1000 mm oraz możliwie jak największe pole robocze kształtem zbliżone do kwadratu. Wiele konstrukcji ma pole robocze o proporcjach boków zbliżonych do 2:1 co często ogranicza rozmiar możliwego do zamontowania na maszynie materiału. Planując budowę takiej maszyny zawsze powinniśmy zaczynać od takiego wstępnego kosztorysu ponieważ jak widać założenia mogą się szybko zmienić. Pozostałe założenia Możliwość obróbki materiałów takich jak drewno, materiały drewnopodobne, tworzywa sztuczne, kompozyty i metale kolorowe 3 stopnie swobody Możliwie maksymalnie duże pole robocze przy danych wymiarach zewnętrznych Solidna konstrukcja oparta o profile stalowe Możliwość prostej separacji bramy od podstawy maszyny w celu łatwiejszego transportu Sterowanie z komputera klasy PC Zachowanie estetyki konstrukcji Kartezjański układ współrzędnych Wybór sposobu poruszania się w osi Y Do wyboru mamy dwie opcje: Ruchomy stół i statyczna brama Statyczny stół i ruchoma brama W moim przypadku wybór padł rozwiązanie ze statycznym stołem i ruchomą bramą ze względu na mniejszą wymaganą przestrzeń gdzie stoi maszyna, ponieważ stół ruszając się wychodzi poza obrys ramy podobnie jak w drukarkach 3D z takim rozwiązaniem. Dużą zaletą tego rozwiązania jest również nie wpływanie masy materiału na bezwładność podczas pracy w osi Y, co ma miejsce przy pierwszym sposobie. Ostatnią zaletą statycznego stołu jest możliwość zamontowania materiału znacząco wychodzącego poza obrys maszyny w osi Y i podparcia go w razie potrzeby. Przed przystąpieniem do projektowania własnej konstrukcji CNC pamiętajmy aby się dobrze zastanowić nad różnymi rozwiązaniami oraz wybrać najbardziej odpowiadające dla naszych potrzeb. Projekt 3D w Autodesk Inventor Jak wspomniałem we wstępie podczas budowy frezarki nie wspierałem się zanadto projektowaniem w Inventorze, ale sama praca inżynierska tego projektu już wymagała, zatem projekt 3D został wykonany post factum. Do zaprojektowania maszyny posłużył program Autodesk Inventor, który umożliwia zaprojektowanie pojedynczych części, ich zespołów, eksport rysunków potrzebnych do wykonania części na maszynach CNC, a ponadto umożliwia sprawdzenie kolizji oraz przeprowadzenie symulacji wytrzymałościowych. Model zawiera zarówno elementy dostępne na rynku jak i elementy potrzebne do wykonania. Części dostępne na rynku były modelowane symbolicznie zachowując tylko wymiary kluczowe dla konstrukcji i były nazywane słownie. Natomiast części wymagające wykonania były numerowane według schematu AAA-BBB, gdzie „AAA” jest numerem zespołu do którego należy część, a „BBB” jest numerem części danego zespołu. Przy czym plik 000-000 jest plikiem głównym zespołu i w jego skład wchodzą dwa zespoły (rama i brama) oraz śruby i podkładki je łączące. W przypadku pracy z większymi projektami zawierającymi więcej niż kilkanaście elementów pamiętajmy aby ustandaryzować nazewnictwo/numerację kolejnych modeli, ponieważ na słowne nazywanie każdej części szybko skończy się nam wyobraźnia, a nazwy typu "ercvevrtwh" szybko się na nas zemszczą bałaganem w plikach. Projekt nie zawiera wszystkich elementów maszyny ze względu na dużą ilość czasu ile wymagają te elementy do zamodelowania. Brakuje między innymi przewodów łączących wrzeciono, silniki i czujniki ze sterownikiem jak i łańcuchów prowadzących, mimo to model pozwolił dobrze dobrać odpowiednie łańcuchy prowadzące i zaplanować miejsca prowadzenia przewodów. Pamiętajmy aby podczas projektowania przewidzieć sposób ułożenia wszystkich przewodów, ponieważ nasz projekt nawet najlepszej konstrukcji z pajęczyną wiszących kabli będzie zwyczajnie nieestetyczny. Może to i trywialne, natomiast bardzo dużo osób o tym zapomina. Projektując tak dużą maszynę powinniśmy zastanowić się nie tylko nad spełnieniem kryteriów pracy tej maszyny, ale również powinniśmy przemyśleć rozwiązania zwiększające ergonomię użytkowania bądź transportu maszyny. Model mojej frezarki został podzielony na dwa podzespoły – ramę i bramę. Dzięki temu jest możliwość rozmontowania frezarki na dwa duże elementy co umożliwia łatwiejsze przenoszenie i transport maszyny. Dzięki przemyślanej konstrukcji wystarczy odkręcenie 10 śrub, aby frezarka została rozłączona na dwa niezależne elementy, co nie trwa więcej niż 10 minut. Poniższa grafika prezentuje śruby, które powinniśmy odkręcić, aby było możliwe zdjęcie bramy. Taka sama liczba śrub znajduje się po drugiej stronie maszyny. Natomiast grafika niżej reprezentuje już bramę odseparowaną od dolnej ramy. W przypadku takiego projektu bardzo ważne jest również powstrzymywania naszej wyobraźni przed zaprojektowaniem zbyt "fikuśnych" części, co może skutkować bardzo dużym kosztem wykonania. Zdarzają się przypadki kiedy ktoś poświęci bardzo dużo czasu na projekt, którego koszt wykonania przekracza wszelkie wyobrażenia i projekt należy powtórzyć. W tym przypadku detale były projektowane z myślą o wykonaniu ich metodą cięcia wodą. Jest to najtańsza i najszybsza metoda obróbki ubytkowej. Metoda wykorzystuje strumień wody pod ogromnym ciśnieniem, który z dodatkiem ścierniwa jest w stanie ciąć wszelkie materiały, a precyzja wykonania jest wystarczająca na potrzeby niniejszej budowy. Ponadto została również wykorzystana metoda druku 3D do wykonania odsysu, który może być montowany na mocowaniu wrzeciona i blokowany przez samo wrzeciono. Do montażu i demontaż potrzebny jest tylko jeden klucz imbusowy potrzebny do poluzowania uchwytu wrzeciona. Odsys zapewnia podłączenie odkurzacza oraz odprowadzenie wiórów i pyłu powstającego podczas frezowania. Jest to jedyna część wykonana techniką druku 3D w niniejszym projekcie ze względu skomplikowany kształt odsysu. Budowa części mechanicznej Rama Funkcję nośną dla całej maszyny pełni dolna rama, do której przymocowane są między innymi śruba osi Y, brama oraz gumowe nóżki, na których spoczywa cała konstrukcja. Jako materiał do budowy ramy posłużył profil stalowy gięty na zimno w kształcie prostokąta o wymiarach 100x50 mm i najgrubszej ściance dostępnej w hurtowni stali – 5 mm. Najbardziej kluczowy był etap spawania, ponieważ łączenie elementów łukiem elektrycznym powoduje powstawanie bardzo wysokiej temperatury w miejscu łączenia, po czym spoiwo stygnąc powoduje kurczenie materiału. Aby jak najskuteczniej przeciwdziałać temu zjawisku profile zostały przykręcone do stołu na czas spawania, same spawy najpierw były nakładane punktowo, aby ograniczyć nagrzewanie, a w następnej kolejności było nakładane spoiwo wypełniające, przy czym każdy następny punkt spawania znajdował się po przeciwległej stronie profilu, aby przeciwdziałać ściąganiu materiału. Przez cały ten proces kontrolowane były wymiary ramy. Mimo wszelkich starań zawsze będą obecne niedokładności w procesie budowy. Wymiary ramy były zadowalające, jednak stalowe profile gięte na zimno nie mają idealnie płaskich ścian, zawsze są one delikatnie wypukłe. Aby zapewnić jak największą dokładność samej maszyny powinniśmy zapewnić wspólną płaszczyznę dla podpór wałków liniowych, po których porusza się oś Y. Rozwiązaniem problemu może być planowanie powierzchni profilów w miejscach gdzie będą przymocowane prowadnice. Planowanie powierzchni zostało zlecone firmie dysponującej frezarką CNC o wystarczającym polu roboczym. Poniższe zdjęcie prezentuje równą płaszczyznę, na której możemy już przymocować prowadnice. Następnym etapem był montaż prowadnic dla ruchomej bramy. Wybór padł na wałki liniowe o średnicy 20 mm z podporami na całej długości prowadnicy co gwarantuje stabilne podparcie dla konstrukcji w każdym położeniu bramy. Przy montowaniu prowadnic powinniśmy przykuć szczególną uwagę, aby zamontowane prowadnice były względem siebie równoległe, ponieważ jakiekolwiek przekrzywienie wpłynie negatywnie na lekkość jak i dokładność pracy już o nadmiernym zużywaniu łożysk nie wspominając. Dla zapewnienia równoległości obu wałków względem siebie możemy najpierw przymocować jedną z prowadnic, a dla zachowania równoległości drugiej prowadnicy możemy się wspomóc np. płaskownikiem, do którego będą przykręcone łożyska obu prowadnic, następnie powinniśmy ten płaskownik z łożyskami przesunąć w obie skrajne pozycje oraz przymocować drugą prowadnicę. W moim przypadku sprawdziło się to świetnie. Następnie do ramy przyspawano uchwyty, w które zostały wkręcone gumowe nóżki. W sklepach z częściami do maszyn przemysłowych rozwiązania nóg zazwyczaj bywają strasznie drogie dlatego warto się rozejrzeć za innymi rozwiązaniami. Rolę nóżek w tym przypadku spełniły gumowe odboje z zawieszenia samochodu terenowego. Bardzo dobrze tłumią wszelkie wibracje maszyny i nie przenoszą ich na stół na którym stoi maszyna. Brama Brama podobnie do ramy pełni funkcję nośną dla pozostałych dwóch osi – X oraz Z. Do budowy bramy został użyty taki sam profil stalowy jak w przypadku ramy. Zostały docięte 3 odcinki o wymaganych długościach, a w profilu docelowo będącym poziomo zostały obrobione płaszczyzny na tokarni aby zapewnić prostopadłość płaszczyzny cięcia z bokiem profilu. Dzięki temu zabiegowi możliwe było zaciśnięcie profili w pożądanej pozycji, a następnie przystąpienie do spawania. Nie dysponowałem w tamtej chwili tak dużym zaciskiem zatem do unieruchomienia bramy przy spawaniu ponownie została wykorzystana tokarnia jako docisk tak jak widać na poniższym zdjęciu 🙂 W tym przypadku sposób okazał się doskonały, ponieważ wymiary bramy wyszły idealne z zachowaniem kątów prostych. Następnie został przyspawany drugi profil poziomo, tym razem o przekroju kwadratu 50x50 mm i również o ściance grubości 5 mm. W następnej kolejności zostały przyspawane stopy bramy do mocowania łożysk. Zostały one wycięte technologią cięcia wodą ze stali o grubości 15 mm. Do owych stóp zostały przykręcone podwójne łożyska otwarte i aby uniknąć montowania dodatkowych wzmocnień zdecydowałem się na tak dużą grubość stóp. Końcówki profili bramy wymagały wycięcia otworów w narożnikach, aby był możliwy dostęp do śrub mocujących łożyska liniowe. Rozwiązanie niezbyt eleganckie i ogranicza trochę dostęp do śrub mocujących, ale nie wpływa to na sztywność konstrukcji. Profil nie mógł być przyspawany na środku stopy, ponieważ musimy pamiętać, że wrzeciono frezarki będzie trochę odsunięte od bramy, dlatego brama musiała być możliwie cofnięta na stopie, aby wrzeciono i pozostała konstrukcja bramy znajdowała się w miarę możliwości w obrysie stóp. Wsunięcie bramy na prowadnice ramy udowodniło powodzenie w dotychczasowych staraniach. Brama mogła się poruszać bez oporów w całym zakresie ruchu, a nawet wymagała zabezpieczenia, aby uniemożliwić samoistne zsunięcie się z prowadnic. Podobnie jak w osi Y, płaszczyzny pod podpory prowadnic osi X również wymagały planowania powierzchni. Następnie zostały przymocowane prowadnice osi X w analogiczny sposób jak miało to miejsce dla osi Y. Na gotowe prowadnice został wsunięty wózek osi X wykonany z aluminiowej płyty o grubości 10 mm, na którym realizowany jest również ruch w osi Z. Do płyty przymocowanych jest 8 pojedynczych łożysk otwartych oraz obudowy nakrętek kulowych dla osi Y oraz Z. Ruch w osi Z odbywa się po prowadnicach zamontowanych na ostatnim elemencie nośnym, również wykonanym z 10 mm płyty aluminiowej. Do tego elementu jest przykręcony uchwyt umożliwiający montaż wrzeciona. Powyższe aluminiowe płyty również zostały wycięte wodą. Poza kształtem wycięto również otwory montażowe, zatem pod odbiorze takiego elementu możemy założyć gwintownik maszynowy na wkrętarkę, błyskawicznie nagwintować otwory i zamontować gotowy element. Dlatego też na etapie projektowania warto przewidzieć sposób wykonania niektórych elementów. Uwzględnienie wszystkich otworów w projekcie i wycięcie ich wodą spokojnie zaoszczędziło mi kilka cennych godzin i nerwów. Napęd i czujniki Do napędu frezarki użyłem silników krokowych sprzężonych ze śrubami kulowymi. Wybrałem silniki krokowe ze względu na niski koszt zakupu, łatwość w sterowaniu oraz powszechność wykorzystania w podobnych zastosowaniach. Wykorzystałem model bipolarny NEMA23 o momencie trzymającym 1,89 Nm. Istotnym parametrem przy doborze silników był też prąd jednej fazy wynoszący 2,8 A, co mieści się w ograniczeniu prądowym zastosowanego sterownika. Do zamiany ruchu obrotowego na ruch liniowy wykorzystałem śruby kulowe o średnicy 16 mm i skoku 5 mm. Mimo znaczącej ceny zestawu jest to korzystny wybór w zastosowaniu do napędu maszyn CNC. Śruby kulowe charakteryzują się brakiem luzu oraz znikomymi oporami toczenia dzięki budowie nakrętki przypominającej łożysko. Wadą skomplikowanej budowy nakrętek jest ich wysoka cena oraz niższa odporność na zabrudzenia, które mogą się przyklejać do śrub napędowych. Śruby zostały połączone z silnikami za pośrednictwem sprzęgieł mieszkowych. Sprzęgła pełnią dwie funkcje – przeniesienie napędu z silnika na śrubę oraz wprowadzają delikatną elastyczność, która jest niezbędna ze względu na niemożliwe do całkowitego usunięcia nieosiowości osi silnika i śruby. Sztywne połączenie silnika ze śrubą mogłoby skutkować wibracjami w trakcie pracy lub nawet uszkodzeniem łożysk silnika. Łożyska podtrzymujące śruby kulowe w osi Y zostały przymocowane za pomocą elementów umożliwiających regulację wysokości łożysk. Mocowania łożysk pozostałych osi zostały przytwierdzone bezpośrednio do ramy lub płyty aluminiowej. Cienkie stalowe elementy mocujące zostały wycięte wodą, natomiast aluminiowe mocowanie silnika osi Z zostało wyfrezowane ze względu na grubość 10 mm, przy takiej grubości płaszczyzna cięcia w przypadku wody może nie być do końca prostopadła. Przez zbyt duży rozmiar silników niemożliwe było zamontowanie jednego z nich współosiowo ze śrubą osi X ponieważ nie zmieściłby się między profilem bramy a wózkiem osi X. Aby rozwiązać ten problem przesunięto silnik poza obrys maszyny, a napęd przeniesiono przez dwa sprzęgła mieszkowe oraz wał o średnicy 6mm. Zastosowane rozwiązanie nie nadwyręża elastycznych możliwości sprzęgieł oraz nie ogranicza pola roboczego w osi X. Na koniec zostały przymocowane ostatnie najmniejsze detale takie jak uchwyty czujników, mocowanie wrzeciona czy mocowania łańcuchowych prowadnic na przewody. Przy umiejscowieniu czujników wziąłem pod uwagę ukrycie ich z dala od pola roboczego oraz miejsca, w którym znajduje się operator, aby nie narażać czujników na uszkodzenia oraz ze względów estetycznych. Po zamontowaniu na swoich miejscach silników i czujników maszyna była gotowa na pierwsze testowe uruchomienie. Tymczasowo został podłączony sterownik, komputer oraz wstępnie skonfigurowany program sterujący, aby zweryfikować poprawność działania wszystkich mechanizmów. Po chwili testów zostało zamontowane wrzeciono i maszyna mogła wykonać pierwsze otwory w obudowie przeznaczonej do skrycia wewnątrz sterownika. Po pomyślnie przeprowadzonych testach wszystkich mechanizmów konstrukcja była niemal gotowa do użytku. Ostatnią czynnością pozostało pomalowanie wszystkich stalowych elementów konstrukcyjnych, aby zabezpieczyć je przed korozją. Zdecydowałem się na malowanie metodą proszkową, która zapewnia odporność na korozję i czynniki chemiczne we wszystkich zakamarkach konstrukcji. Przed malowaniem należało całą konstrukcję rozebrać a następnie ponownie złożyć. Na koniec został zamontowany stół maszyny, którego funkcję pełni płyta ze sklejki liściastej. Materiał ten jest dość tani w zakupie i wystarczająco sztywny dla tej konstrukcji. Stół składa się z dwóch części: dolnej płyty o grubości 37 mm, do której jest przymocowany zasadniczy stół o wymiarach pola roboczego i o grubości 36 mm. W stole wykonano niemal 2100 otworów, które już samodzielnie wywierciła maszyna wiertłem o średnicy 3 mm. Na wykonanie takiej ilości otworów wystarczyło zaledwie 45 minut ciągłej pracy. Otwory będą wykorzystywane do zamocowania obrabianego materiału w polu roboczym. Po zakończeniu wiercenia stół został poddany planowaniu, aby zapewnić równoległość płaszczyzny stołu do płaszczyzny pracy maszyny. Podsumowanie części mechanicznej Jak widać proces budowy przebiegł całkiem pomyślnie z zachowaniem funkcjonalności i estetyki. Chciałbym natomiast przyłożyć dużą uwagę do etapu projektowania. Pamiętajmy, że im więcej czasu spędzimy na etapie projektowania i rozwiązywania problemów tym mniej czasu, potu, nerwów i pieniędzy będzie nas kosztowała budowa naszego projektu. W moim przypadku upiekło się bez istotnych wpadek, ale niektóre rzeczy na pewno zaprojektowałbym inaczej przed przystąpieniem do budowy. Nie będę nikogo czarował, że taka budowa to nic takiego, ponieważ jak widać wymaga to pewnej wiedzy i doświadczenia z mechaniką oraz zaplecza narzędzi, którymi nie każdy dysponuje, a zlecenie takiej budowy może być dość kosztowne. W kolejnej części artykułu opiszę budowę części elektronicznej, podłączenia wszystkich elementów, prowadzenie kabli, zmontowanie sterownika, sposób sterowania przez komputer PC oraz na sam koniec pokażę jak wygląda użytkowanie takiej maszyny wraz z materiałami wideo. Link do drugiej części artykułu: Budowa frezarki CNC na stalowej ramie sterowanej z komputera PC - Sterowanie i uruchomienie [2/2]
  8. Transmisja danych przy użyciu portu szeregowego ciągle jest bardzo popularnym sposobem komunikacji używanym w rozmaitych układach kontrolno-pomiarowych. Dotyczy to zarówno projektów hobbistycznych jak również profesjonalnych rozwiązań. Obecnie mamy do wyboru wiele programów, które pozwalają obsługiwać port szeregowy z poziomu naszych komputerów. Wspomniane terminale posiadają wiele użytecznych opcji włącznie z możliwością graficznej wizualizacji odbieranych danych. Zdarza się jednak, że nawet w amatorskich zastosowaniach brakuje pewnych opcji, które pozwalały by na obsługę portu szeregowego adekwatnie do celu budowanego układu. W takim przypadku należy stworzyć własne oprogramowanie co czasami może być dosyć trudne i czasochłonne. Bardzo pomocnym rozwiązaniem może okazać się więc środowisko programistyczne LabVIEW. 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 » Dlatego w tym artykule przedstawię: idee środowiska LabVIEW minimalne podstawy, które pomogą stworzyć pierwszy program przykład obsługi portu szeregowego dla pomiaru temperatury przy użyciu Arduino Uno i termometru DS18B20 rozwojowe możliwości środowiska LabVIEW LabVIEW jest środowiskiem programistycznym stworzonym przez firmę National Instruments. To co wyróżnia to środowisko to graficzne podejście do tworzenia programów. Kod składa się z wizualnych reprezentacji funkcji oraz zmiennych połączonych ze sobą i wykonujących się w określonej kolejności. LabVIEW używane jest przede wszystkim do prowadzenia pomiarów, akwizycji danych i współpracy z różnymi urządzeniami. Dzieje się tak, ponieważ w tym środowisku znajdziemy szeroką paletę gotowych funkcji i bibliotek, które pozwalają na szybką implementację wspomnianych funkcjonalności. Do niedawna LabVIEW było dostępne komercyjnie bądź dla studentów uczelni technicznych w ramach akademickich licencji. Jednak od 2020 roku dostępna jest wersja LabVIEW Community Edition, którą można wykorzystywać do niekomercyjnych zastosowań. Jedynym wymaganiem będzie założenie konta na platformie NI. Oprogramowanie można pobrać z pod tego linku. Instalacja jest bardzo prosta, nie trzeba nic zmieniać a jedynie postępować wedle domyślnych ustawień. W telegraficznym skrócie zaprezentuję jak obsłużyć to środowisko i stworzyć pierwszy program. Po uruchomieniu wybieramy File -> New VI i naszym oczom ukażą się dwa okna. Jest to front panel – okno z interfejsem użytkownika dostępne dla niego w trakcie działania programu oraz block diagram – miejsce w którym „projektujemy” kod naszego programu. Klikając prawym przyciskiem myszy na front panelu otwiera nam się paleta z dostępnymi kontrolkami, z których możemy budować interfejs. Reprezentacje tych kontrolek pojawią się na drugim oknie. Przechodząc na to drugie okno czyli block diagram będziemy mogli przeprowadzić na nich pożądane operacje. Klikając prawym przyciskiem myszy ukaże nam się paleta, w której będziemy mieli do wyboru wartości stałe i funkcje, z których będziemy tworzyli kod programu. Najprostszy program posłuży nam do dodawania dwóch liczb. W tym celu na front panel-u klikamy prawym przyciskiem myszy i wybieramy paletę Numeric. Wybieramy stamtąd dwa razy Numeric Control i raz Numeric Indicator. Klikając dwa razy na domyślne nazwy możemy je zmienić wedle własnego upodobania. Przechodzimy na block diagram, również klikamy prawym i z palety Numeric wybieramy funkcję Add. Kiedy najedziemy na nią pojawią się z jej lewej strony dwa wejścia a z prawej jedno wyjście. Kiedy zbliżymy się do nich kursorem zamieni się on w małą „szpulkę”. Klikając lewym przyciskiem myszy możemy podłączyć dwa Numeric Control do wejść funkcji Add i jedno Numeric Indicator do wyjścia funkcji Add. Wracając na front panel możemy wpisać jakieś wartości liczbowe w Numeric Control i kiedy wciśniemy biała strzałkę na pasku narzędzi (zaznaczona na czerwono na poniższym obrazku) uruchomimy nasz program i zobaczymy, że w Numeric Indicator pojawił się wynik dodawania wpisanych liczb. Uogólniając programowanie w LabVIEW sprowadza się do wybierania kontrolek i łączenia ich z różnymi funkcjami, strukturami i innymi kontrolkami tak, aby uzyskać zamierzony cel. Należy pamiętać przede wszystkim o tym, że można łączyć tylko te same typy danych a wiele funkcji jest przeznaczonych dla jednego typu zmiennej. Wizualnie dane oznaczone są różnymi kolorami. W dużym uproszczeniu można powiedzieć, że łączymy bloczki czy wejścia/wyjścia funkcji o tych samych kolorach. Zachęcam do zapoznania się z paletami kontrolek i funkcji oraz eksperymentów z pierwszymi prostymi programami. To co może się przydać na początek to: skrót do okna pomocy [Ctrl+H] – po najechaniu na dany bloczek wyświetli jego „dokumentacje” skrót do pozycjonowania okien [Ctrl+T] – ułoży block diagram i front panel obok siebie dopasowując się do ekranu czyszczenie block diagram-u [Ctrl+B] - usuwa niewykonywalne połączenia powstałe po usunięciu bloczków paleta narzędzi [View->Tools Palette] – pozwala wybrać obecną funkcje kursora, domyślnie funkcje przełączają się automatycznie w zależności od jego położenia Po tym wprowadzeniu minimalnych podstaw przejdźmy do meritum artykułu. Aby zademonstrować obsługę portu szeregowego w LabVIEW skorzystałem z Arduino Uno konstruując poniższy układ: Składa się on naturalnie z płytki Uno, diody LED wraz z rezystorem 220Ω i termometru cyfrowego DS18B20 wraz z wymaganym rezystorem 4,7kΩ pomiędzy linią danych a zasilaniem. Na Arduino został wgrany następujący program: #include <OneWire.h> //biblioteki potrzebne do obsługi czujnika temperatury #include <DallasTemperature.h> #define ledPin 3 // definiujemy pin do obsługi diody LED #define oneWireBus 2 // definiujemy pin do obsługi protokołu OneWire bool ledState=false; // zmienne do przechowywania aktualnej temperatury, stanu diody i progu "alarmowego" float temperature=0.0; float threshold=20.0; OneWire oneWire(oneWireBus); //inicjalizacja OneWire DallasTemperature sensors(&oneWire); void setup() { pinMode(ledPin,OUTPUT); // inicjalizacja pinu na wyjście, stan niski Serial.begin(9600); // otworzenie komunikacji szeregowej while (!Serial) { ; // oczekiwanie na połączenie } sensors.begin(); //inicjalizacja czujnika temperatury } void loop() { sensors.requestTemperatures(); //pobranie wartości temperature=sensors.getTempCByIndex(0); Serial.println(temperature); //informacja o temperaturze na port szeregowy w postaci kodów ASCII Serial.println(ledState); //informacja o stanie diody na port szeregowy w postaci kodów ASCII delay(500); while(Serial.available()>0) // jeżeli dane w buforze to czytaj kolejną prawidłową wartość float { threshold=Serial.parseFloat(); } if(temperature<threshold) //jeżeli temperatura niższa od progu to zapal diode alarmową { digitalWrite(3,HIGH); ledState=true; } else { digitalWrite(3,LOW); ledState=false; } delay(500); } Kod został opatrzony odpowiednimi komentarzami dlatego nie ma sensu go szczegółowo omawiać. Warto jednak zaznaczyć ogólną idee. Jego zadaniem jest pomiar temperatury i alarmowanie o jej spadku poniżej pewnej progowej wartości przez zapalenie diody LED. Informacje o temperaturze i stanie diody wysłane są szeregowo do danego terminala tak aby można je było wizualizować. Dodatkowo program sprawdza czy terminal nie przesyła informacji o nowej wartości progowej. Tak dobrane funkcjonalności pozwolą na pokazanie implementacji dwustronnego przesyłu danych. Teraz przejdźmy do naszego programu jaki stworzyłem w LabVIEW. Podstawową paleta jaką wykorzystałem do tego programu jest paleta Serial, którą znajdziemy w funkcjach, w kategorii Instruments I/O. Odpowiada ona za wszystkie operacje jakie możemy przeprowadzić na porcie szeregowym. Stworzony program posiada wszystkie opcje jakie narzuca oprogramowanie wgrane na Arduino. Front panel tego programu prezentuje się następująco: Patrząc od lewej strony - mamy tutaj wybór portu, który służy nam do połączenia z Arduino i miejsce do wpisania nowego progu dla temperatury. Poniżej - graficzna wizualizacja stanu diody LED i przycisk służący do zamknięcia komunikacji oraz programu. Obok znajduje się pasek, który pokazuje ostatnio odczytaną temperaturę. Dalej znajduje się wykres, który pokazuje zebrane kolejno wartości temperatury oraz obecny próg. Nad wykresem znajduje się legenda i narzędzia do nawigacji wykresu -przybliżania, zaznaczania itd. Kod tego programu znajduje się poniżej. Przy braku wcześniejszej styczności z LabVIEW program może się okazać skomplikowany ale posiada on stosowne komentarze. Po jego pobraniu i sprawdzeniu poszczególnych bloczków z użyciem okna pomocy (skrót [Ctrl+H]) można w miarę szybko przyswoić jego konstrukcję i zmodyfikować do swoich zastosowań. To co najważniejsze to odpowiednie ustawienie parametrów komunikacji. Otwierając połączenie w LabVIEW należało podać te same parametry jakie zostały przyjęte w układzie pomiarowym. Chodzi tu o prędkość transmisji, liczbę bitów danych, parzystość i bity stopu. Wszystkie ustawienia portu szeregowego związane z inicjalizacją połączenia przez Arduino można wyczytać z dokumentacji znajdującej się pod tym linkiem. Istotne było też użycie w kodzie Arduino funkcji println(), która dodaje znak nowej linii do każdego wysłanego ciągu znaków ASCII. W trakcie otwierania komunikacji w LabVIEW zaznaczyłem opcję, że każdy komunikat będzie się kończył takim znakiem co znacznie ułatwia późniejszy odczyt. Funkcja nie musi wtedy znać dokładnej ilości bajtów jakie maja nadejść tylko czyta wszystko do znaku „/n”, bez ryzyka wystąpienia jakiegoś timeout-u czy ucięcia danych. Zwłaszcza w przypadku gdy nadchodzący łańcuch danych jest zmienny. Program co 500ms sprawdza czy w buforze pojawią się dane, zgodnie z kolejnością wysyłu (temperatura, stan diody) dokonuje odczytu, konwertuje z łańcucha znakowego na odpowiedni typ danych i wyświetla. Następnie przesyła obecną wartość temperatury progowej, sprawdza czy nie wystąpił błąd albo czy ktoś nie wcisnął wyłączenia programu. Potem cykl zaczyna się od nowa. Po wyłączeniu programu połączenie się zamyka, wykres zostaje wyczyszczony i wszystko wraca do domyślnych wartości. Jest to program, który demonstruje głównie sam koncept transmisji szeregowej w LabVIEW. Sam układ nie jest mocno funkcjonalny ale pokazuje dwustronną komunikację z możliwością wizualizacji i kontroli. Biorąc pod uwagę łatwość w konstrukcji interfejsu użytkownika, mnogość rozmaitych funkcji i w miarę niski próg wejścia w naukę tego środowiska LabVIEW może okazać się bardzo przydatne. Już domyślne kontrolki pozwalają tworzyć przejrzyste i profesjonalnie wyglądające GUI a wiele rozwiązań od społeczności znacznie poszerza możliwości. Same dane mogą zostać poddane bezproblemowej obróbce, jeżeli chcemy zrobić FFT czy dopasowanie liniowe to bez problemu znajdziemy takie funkcję. Mam nadzieję, że artykuł zainteresował Was tematem i w przyszłości włączycie LabVIEW do palety narzędzi używanych w swoich projektach. Oczywiście mam świadomość, że dla osoby zupełnie nieobeznanej z tym oprogramowaniem tworzenie swojego terminalu szeregowego może okazać się niepotrzebnym wysiłkiem. Jednak gwarantuje, że podstawy można opanować bardzo szybko, a zamieszczony przeze mnie przykład łatwo przerobić na swoje potrzeby. Dodatkowo istnieją specjalne biblioteki dedykowane stricte pod Arduino, Raspberry Pi a samo środowisko wspiera współprace z Matlabem, Pythonem czy C. LabVIEW może okazać się również bardzo przydatne w innych zastosowaniach, nie tylko tych związanych z elektroniką i pomiarami. Dlatego, zachęcam do nauki i skorzystania z szerokiej gamy materiałów jakie można znaleźć na ten temat w internecie. SerialMonitorExample.rar
  9. Dla zielonych 😉 (BASIC CRASH COURSE) O czym w tym artykule? W tym artykule poznasz podstawy pracy z Raspberry Pi Pico w języku C++. Obsłużysz podstawowe narzędzia - PWM, delay, piny, UART. Ten poradnik jest skierowany dla osób, które są kompletnie zielone i chcą postawić swoje pierwsze kroki z RP2040. Na wstępie Uwaga: jeżeli korzystasz z Windowsa zaparz sobie szklankę melisy, gdyż instalacja środowiska na Windowsie to horror 😉 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 » Instalacja środowiska - Linux sudo apt update sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi build-essential // To powinieneś mieć sudo apt install gcc g++ // A to dla fanatyków Debiana :) sudo apt install libstdc++-arm-none-eabi-newlib Tutaj odeślę do oficjalnej dokumentacji 🙂 Instalacja środowiska - Windows Alternatywa: skrypt instalujący Pi Pico Toolchain na Windowsie 😉 (nie testowałem) README dla tego programu. Dobrze no to czas na tę tragedię - przede wszystkim zaparz sobie melisę... To jest bardzo ważne 🙂 Wybierz środowisko programistyczne Visual Studio Code Clion Zainstaluj CMake Pamiętaj by podczas instalacji zaznaczyć opcję "Add the CMake to system PATH for all users" Pobierz i zainstaluj Build Tools for Visual Studio, pamiętaj by zaznaczyć "C++ build tools" w sekcji "Workloads" Zainstaluj Python3, pamiętaj by zaznaczyć opcję by dodać go do PATH 😉 Zainstaluj Git - również pamiętając o dodaniu do PATH 😉 Zainstaluj kompilator ARM 😉 Tak samo należy pamiętać o dodaniu go do zmiennej PATH 😉 Zmienna PATH powinna wyglądać w sposób zbliżony: Myślałeś, że to koniec? 😄 Teraz czas pobrać i zainstalować SDK Pobieramy archiwum (zielony przycisk) Wypakowujemy do jakiegoś folderu na dysku Kopiujemy ścieżkę dysku Wchodzimy w "Mój komputer" > "Ten Komputer" > PPM > "Właściwości" > "Zaawansowane ustawienia systemu" > "Zmienne środowiskowe" Dodajemy nową zmienną o nazwie PICO_SDK_PATH i wartości skopiowanej ścieżki (np. "D:\SDK\Pi Pico") Dodajemy nową zmienną o nazwie PICO_TOOLCHAIN_PATH i ścieżce do ARM Toolchain (np. "C:\Program Files (x86)\GNU Arm Embedded Toolchain\10 2020-q4-major\bin") Do osobnego folderu pobieramy i wypakowujemy następujące narzędzie, znacząco ułatwi nam tworzenie projektów 😉 Wszystko powinno być już zainstalowane, jeżeli masz problemy odeślę Cię do tego poradnika, który opisuje wszystko krok po kroku dla Visual Studio 😉 Tworzenie projektu Tutaj nie będę poruszał tworzenia pliku Makefile, gdyż dla większości osób jest to strasznie skomplikowane, a ma być prosto nieprawdaż? Stąd pobraliśmy Pico Project Generator. Jest to bardzo wygodne narzędzie, dzięki któremu możemy tworzyć projekty w interfejsie graficznym. By uruchomić narzędzie uruchamiamy "Wiersz Poleceń" (cmd.exe) i przechodzimy do folderu, w którym znajduje się nasz Pico Project Generator (np. "D:\pico-project-generator-master"). Następnie używamy komendy: python pico_project.py --gui by uruchomić nasz program. Przykład: Używanie Pico Project Generator Pojawia nam się następujące okienko: W zakładce "Project Name" wpisujemy nazwę projektu, UWAGA: NAZWA PROJEKTU NIE MOŻE ZAWIERAĆ SPACJI W sekcji Location możemy wybrać folder nadrzędny dla naszego projektu (w nim znajdzie się folder z projektem), ja go wrzucę razem z generatorem. Oprócz tego możemy wybrać dodatkowe biblioteki, ustawienia konsoli czy ustawienia kodu. Jako, że ja będę korzystał z CLiona użyję standardowych ustawień i kliknę przycisk OK. Czekamy aż projekt się wygeneruje i zamykamy generator 😉 Teraz uruchamiamy środowisko... Jak widzimy nasz program wygenerował standardowy plik. Nie będę tutaj tłumaczył podstaw języka C, aczkolwiek powiem jak podmienić go na C++. W tym celu zmieniamy rozszerzenie pliku na .cpp Oraz musimy poprawić plik CMakeLists.txt - czasami nie jest on poprawiany automatycznie. Szukamy linijki "add_executable(Forbot Forbot.c)" i podmieniamy nazwę "Forbot.c" na "Forbot.cpp" - linijka powinna wyglądać tak: "add_executable(Forbot Forbot.cpp)" 😉 Uwaga: dla Visual Studio Code zalecane jest zainstalowanie pluginu CMake Tools. Dokumentacja Warto sobie dodać do zakładek dokumentację C++ dla Raspberry Pi Pico - jest tam wszystko dokładnie i elegancko opisane 😉 Warto sobie też zapisać gdzieś pinout, gdyż na górze płytki nie ma oznaczeń i jak ją wepniemy w płytkę stykową... będzie problem 😉 GPIO - Pierwszy program... No to co? Może napiszemy pierwszy program? W tym celu do GPIO16 podpinamy diodę LED z rezystorem ograniczającym prąd. Jak na załączonym obrazku 😉 Tak wiem, że rezystor nie jest najpiękniej zamontowany, ale kto by się przejmował... To teraz czas na miganie diodą. #include <stdio.h> #include "pico/stdlib.h" int main() { stdio_init_all(); // inicjacja STDIO gpio_init(16); // inicjacja pinu GP16 gpio_set_dir(16, true); // ustawienie pinu GP16 jako wyjścia // loop() while(true){ gpio_put(16, true); // ustaw stan wysoki na pinie GP16 sleep_ms(250); // Odczekaj 250ms gpio_put(16, false); // ustaw stan niski na pinie GP16 sleep_ms(250); // Odczekaj 250ms } return 0; } Jak widzimy kod w Pico nie ma loop() ani setup(), a ma wyłącznie main(). W tym celu sami musimy stworzyć sobie loop'a - po inicjacji I/O dodajemy niekończącą się pętlę while. W niej możemy wykonywać kod tak samo jak w loop() w Arduino, a wszystko powyżej będzie działało jak setup(). Funkcje gpio_init(<id>) - inicjuje pin, PIN MUSI BYĆ ZAINICJOWANY! gpio_set_dir(<id>, <wartość>) - ustawia kierunek pinu: false - wejście, true - wyjście gpio_pull_up(<id>) - podciągnięcie pinu do VCC gpio_pull_down(<id>) - podciągnięcie pinu do GND gpio_put(<id>) - ustawia stan pinu: false - niski, true - wysoki sleep_ms(<time>) - czeka określoną ilość milisekund sleep_us(<time>) - czeka określoną ilość mikrosekund 😉 gpio_set_function(<gpio>, <func>) - ustawia funkcję GPIO GPIO_FUNC_SPI GPIO_FUNC_UART GPIO_FUNC_I2C GPIO_FUNC_PWM GPIO_FUNC_SIO (default) - zwykły pin GPIO_FUNC_PIO0 GPIO_FUNC_PIO1 GPIO_FUNC_GPCK GPIO_FUNC_USB GPIO_FUNC_NULL - brak funkcji (wyłączone) GPIO_FUNC_XIP To są podstawowe funkcje, z których będziesz najczęściej korzystać. Wgrywanie programu. By wgrać program na Pi Pico musisz go skompilować. Jeżeli wygenerowałeś go przy użyciu podanego wcześniej narzędzia powinno to pójść bez większych problemów. Alternatywnie komenda do zbudowania programu wygląda następująco: cmake -DCMAKE_BUILD_TYPE=Debug -G "CodeBlocks - MinGW Makefiles" <ŚCIEŻKA> cmake --build <ŚCIEŻKA> --target all -- -j <ILOŚĆ RDZENI> // Pamiętaj by podmienić <ŚCIEŻKA> i <ILOŚĆ RDZENI> na odpowiednie parametry! // UWAGA: te komendy nie gwarantują poprawnego builda :( Niestety cmake to narzędzie, które jest toporne i zwykle wyrzuca miliard błędów zanim łaskawie coś skompiluje ;) // W przypadku powyższych komend musisz jeszcze ustawić kompilatory C/C++ i ASM ;) // Dlatego zalecam używanie CLion'a, który to wszystko automatyzuje // Alternatywa (często sypie błędami cmake) cd <ŚCIEŻKA>/build nmake Wreszcie gdy uda nam się to skompilować wgrywamy program. By to zrobić odłączamy Pi Pico od USB, przytrzymujemy przycisk BOOTSEL oraz podłączamy Pi Pico do USB (lub używamy przełącznika w HUBie) 😉 Następnie wchodzimy do folderu ze skompilowanymi plikami i szukamy pliku o nazwie <coś>.uf2 i ten plik przenosimy na dysk przenośny, który powstał z naszego Raspberry Pi Pico. Dysk powinien zniknąć, a nasza dioda powinna zacząć migać. UART Obsługa UART'a jest banalna. W Pi Pico mamy do dyspozycji dwa UART'y. uart0 i uart1. Przykład będzie korzystał ze standardowego uart0. #include <stdio.h> #include "pico/stdlib.h" int main() { stdio_init_all(); // Init GPIO gpio_init(0); gpio_init(1); // Ustaw funkcje GPIO jako UART gpio_set_function(0, GPIO_FUNC_UART); gpio_set_function(1, GPIO_FUNC_UART); // Zainicjuj uart0 z transmisją 115200 bps uart_init(uart0, 115200); // Jeżeli na uart0 są jakieś dane if(uart_is_readable(uart0)){ // tablica do odczytu pojedynczego bajtu uint8_t data[1]; // odczytaj bajt uart_read_blocking(uart0, data, 1); // wyślij bajt spowrotem na uart0 uart_write_blocking(uart0, data, 1); } return 0; } Funkcje uart_init(<uart>, <baud>) - inicjuje UART (Serial.begin(<baudrate>)) uart_set_baudrate(<uart>, <baud>) - zmiana baudrate UART'a w locie 😉 uart_is_writable(<uart>) - sprawdza czy bufor zapisu ma miejsce uart_is_readable(<uart>) - sprawdza czy w buforze odczytu są dane uart_write_blocking(<uart>, <data>, <length>) - wysyła bajty na UART, data to wskaźnik do tablicy z danymi uart_read_blocking(<uart>, <data>, <length>) - oczytuje bajty z UART, data to wskaźnik do tablicy z danymi uart_putc(<uart>, <char>) - wysyła znak na UART uart_puts(<uart>, <string>) - wysyła tekst na UART uart_getc(<uart>) - odczytuje znak z UART I2C (IIC) By zainicjować I2C musimy się chwilę natrudzić - po pierwsze musimy dodać bibliotekę hardware_i2c do pliku CMakeLists.txt. Szukamy linijki "target_link_libraries(<NAZWA_PROJEKTU> pico_stdlib)" i dodajemy pod nią "target_link_libraries(<NAZWA_PROJEKTU> hardware_i2c)". Teraz musimy wykonać całą procedurę inicjacji - zainicjować I2C, podłączyć pullupy oraz ustawić piny w tryb I2C. Przykład poniżej: // Inicjacja I2C i2c_init(i2c0, 1000*100); gpio_set_function(4, GPIO_FUNC_I2C); gpio_set_function(5, GPIO_FUNC_I2C); // I2C wymaga pull-upów na liniach ;) gpio_pull_up(4); gpio_pull_up(5); Jak widzimy moje I2C jest podłączone do pinów 4 i 5. Pin 4 to SDA, pin 5 to SCL. Baudrate to 100kbit. By odczytać dane używamy funkcji i2c_read_blocking, a by zapisać i2c_write_blocking. Widać podobieństwo do UARTa? #include <stdio.h> #include "pico/stdlib.h" #include "hardware/i2c.h" int main() { stdio_init_all(); printf("Witaj I2C!\n"); // Inicjacja I2C i2c_init(i2c0, 1000*100); gpio_set_function(4, GPIO_FUNC_I2C); gpio_set_function(5, GPIO_FUNC_I2C); // I2C wymaga pull-upów na liniach ;) gpio_pull_up(4); gpio_pull_up(5); uint8_t data[4]{'t', 'e', 's', 't'}; // Wyślij dane poprzez I2C i2c_write_blocking(i2c0, 0x01, data, 4, false); i2c_read_blocking(i2c0, 0x01, data, 4, false); } Przykładowy kod powyżej 😉 - wysyła słowo "test" na adres 0x01 po czym pobiera 4 bajty z adresu 0x01. Funkcje i2c_init (<i2c>, <baudrate>) - inicjuje I2C 😉 i2c_set_baudrate(<i2c>, <baudrate>) - zmiana baudrate w locie i2c_set_slave_mode(<i2c>, <status>, <adres>) - przełącza Pi Pico w tryb slave dla I2C - jeżeli status jest true to urządzenie jest slave, jeżeli false to jest masterem. Adres jest adresem urządzenia slave, na którym ma działać nasze Pico 😉 i2c_write_blocking(<i2c>, <adres>, <dane>, <długość>, <nostop>) - wysyła dane poprzez I2C - dane to wskaźnik do tablicy bajtów (uint8_t), jeżeli nostop jest true to po wysłaniu nie zostanie wysłany sygnał stop (z reguły false) i2c_read_blocking(<i2c>, <adres>, <tablica>, <długość>, <nostop>) - pobiera dane poprzez I2C - tablica to wskaźnik do miejsca, gdzie mają one zostać zapisane w pamięci (np. tablica uint8_t). Parametr nostop podobnie jak w górnym przypadku opisuje czy program ma pominąć sygnał STOP. i2c_get_write_available(<i2c>) - sprawdza ilość bajtów dostępnych do zapisu na danej magistrali I2C i2c_get_read_available(<i2c>) - sprawdza ilość bajtów dostępnych do odczytu na danej magistrali I2C. SPI Musimy na początku dołączyć bibliotekę hardware_spi. Po tym możemy stworzyć już swój program z obsługą SPI 😉 Przykład: #include <stdio.h> #include "pico/stdlib.h" #include "hardware/spi.h" int main() { stdio_init_all(); // Inicjacja SPI spi_init(spi0, 12000000); // 12 MHz spi_set_format(spi0, 8, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST); gpio_set_function(6, GPIO_FUNC_SPI); // SCK gpio_set_function(7, GPIO_FUNC_SPI); // TX gpio_set_function(8, GPIO_FUNC_SPI); // RX uint8_t write_data[4] = {'t', 'e', 's', 't'}; uint8_t read_data[4]; // Wyślij 4 bajty na SPI spi_write_blocking(spi0, write_data, 4); // Wyślij 4 razy 0 na SPI do odczytu 4 bajtów. spi_read_blocking(spi0, 0x0, read_data, 4); // Pełny duplex SPI ;) spi_write_read_blocking(spi0, write_data, read_data, 4); } Jak widzimy program inicjuje SPI z prędkością 12Mbit/s, po czym przesyła słowo 'test' na magistralę, odczytuje 4 bajty (przesyłając 4 razy 0x0 by odczytać treść) oraz dokonuje transmisji duplex - wysyła słowo 'test' oraz odczytuje w tym samym czasie 4 bajty. Funkcje spi_init(<spi>, <baudrate>) - inicjuje SPI spi_set_format(<spi>, <data_bits>, <cpol>, <cpha>, <order>) - ustawia tryb SPI - ilość bitów oraz parametry CPOL, CPHA i określa czy przesyłamy najpierw MSB czy LSB. spi_set_slave(<spi>, <aktywny>) - jeżeli parametr "aktywny" ustawimy na true to nasze Pi Pico stanie się "slave" SPI zamiast "masterem". spi_is_writable(<spi>) - sprawcza czy na SPI możemy zapisać dane (ilość miejsca w buforze) 😉 spi_is_readable(<spi>) - sprawdza ilość danych odczytanych przechowywanych w buforze SPI spi_read_blocking(<spi>, <bajt>, <dane>, <ilość>) - odczytuje dane z SPI jednocześnie przesyłając określoną ilość razy parametr "bajt" np. 0x0 (zgodnie z standardem SPI). Dane to wskaźnik do tablicy, gdzie dane mają zostać odczytane, a ilość to długość tej tablicy 🙂 spi_write_blocking(<spi>, <dane>, <ilość>) - przesyła dane na SPI - dane to wskaźnik do tablicy z danymi, ilość określa długość tej tablicy 🙂 spi_write_read_blocking(<spi>, <dane_zapis>, <dane_odczyt>, <ilość>) - połączenie read i write - transmisja duplex SPI. dane_zapis to wskaźnik do danych do wysłania na magistralę SPI, a dane_odczyt to wskaźnik do miejsca, gdzie dane mają zostać odczytane. Ilość określa ile danych ma być przesłane. PWM Pi Pico ma całkiem przyjazny w konfiguracji PWM (to jest półsarkazm). Jest on obsługiwany sprzętowo, więc potrzebujemy dodać bibliotekę do pliku CMakeLists.txt. Szukamy linijki "target_link_libraries(<NAZWA_PROJEKTU> pico_stdlib)" i dodajemy pod nią "target_link_libraries(<NAZWA_PROJEKTU> hardware_pwm)". Potem aktualizujemy plik CMake - w Clionie u góry pojawia się do tego ładny pasek z przyciskiem 😉 Klikamy reload changes i gotowe 😉 Teraz czas użyć PWM... #include <stdio.h> #include "pico/stdlib.h" #include "hardware/pwm.h" int main() { stdio_init_all(); gpio_init(0); // Init GPIO gpio_set_function(0, GPIO_FUNC_PWM); // Setup PWM // Pobierz dane PWM - slice i kanał uint slice_num = pwm_gpio_to_slice_num(0); uint channel = pwm_gpio_to_channel(0); // Ustaw długość trwania cyklu PWM na 65536 (granica licznika) pwm_set_wrap(slice_num, 65535); // Kanał PWM będzie miał stan wysoki tylko przez 32767 cykli licznika (ok 50%) pwm_set_chan_level(slice_num, channel, 32767); // Aktywuj PWM pwm_set_enabled(slice_num, true); return 0; } Efekt? Funkcje pwm_gpio_to_slice_num(<gpio>) - pobiera adres slice'a dla PWM pwm_gpio_to_channel(<gpio>) - pobiera adres kanału PWM pwm_set_wrap(<slice>, <cycles>) - ustala ilość cykli PWM dla danego kanału pwm_set_chan_level(<slice>, <channel>, <cycles>) - przez tyle cykli licznika PWM będzie miało stan wysoki pwm_set_enabled(<slice>, <stan>) - aktywuje PWM na danym slice 😉 ADC By odczytać ADC musimy załączyć odpowiednią bibliotekę - hardware_adc do pliku CMakeLists.txt. Szukamy linijki "target_link_libraries(<NAZWA_PROJEKTU> pico_stdlib)" i dodajemy pod nią "target_link_libraries(<NAZWA_PROJEKTU> hardware_adc)". Następnie musimy zainicjować adc, wybrać odpowiedni kanał oraz odczytać wartość. adc_init(); // zainicjuj ADC (wykonaj na początku main()) adc_gpio_init(gpio); // zainicjuj pin ADC adc_select_input(gpio - 26); // wybierz wejście ADC adc_read(); // Odczytaj wartość z ADC Piny ADC na Raspberry Pi Pico to: GP26, GP27, GP28 - odpowiednio wejście 0, 1, 2 Oprócz tego mamy również wejście 3 - wewnętrzny sensor temperatury, gdyby nasza płytka pracowała w mniej sprzyjających warunkach 😉 Funkcje adc_init() - inicjuje ADC, musi być wykonane przed odczytem adc_gpio_init(<gpio>) - inicjacja pinu GPIO adc_select_input(<id>) - wybiera wejście ADC (0 - GP26, 1 - GP27, 2 - GP28, 3 - sensor temperatury) adc_read() - odczytuje wartość z ADC (12-bit) Przerwania na GPIO GPIO obsługuje przerwania - podczas narastania, opadania zbocza oraz również przerwania "stanów" - wysokiego i niskiego. By dodać przerwanie na GPIO używamy funkcji gpio_set_irq_enabled_with_callback(<gpio>, <rodzaj przerwania>, true, <handler>); Oraz określamy handler przerwania np. void gpio_irq_handler(uint gpio, uint32_t events) { status = !status; gpio_put(15, status); } I wtedy nasza funkcja wygląda przykładowo: gpio_set_irq_enabled_with_callback(2, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, gpio_irq_handler); W tym przypadku przerwanie nastąpi podczas narastania lub opadania zbocza i wykona powyższą funkcję przerwania. Uwaga: na jeden rdzeń może być tylko jedno przerwanie SIO. Przykładowy program z przerwaniami: #include <stdio.h> #include "pico/stdlib.h" bool status = false; void gpio_irq_handler(uint gpio, uint32_t events) { status = !status; // Zmień status gpio_put(15, status); // Przełącz diodę } int main() { stdio_init_all(); // Zainicjuj pin z diodą gpio_init(15); gpio_set_dir(15, true); gpio_put(15, status); // Ustal przerwanie gpio_set_irq_enabled_with_callback(2, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, gpio_irq_handler); // Nieskończona pętla - WYMAGANA!!! while (1) { tight_loop_contents(); } } UWAGA: przerwania wymagają, by funkcja main wciąż się wykonywała - można to uzyskać w sposób podany powyżej! Alarm Alarm - jednorazowe przerwanie, Timer - powtarzające się wykonanie przerwania - tak można to najprościej opisać 😉 Timery omówimy w następnej sekcji, tymczasem zajmiemy się tematem tej... Przykładowe wywołanie alarmu: add_alarm_in_ms(1000, alarm, NULL, false); Alarm pozwala nam wykonać fragment kodu po określonym czasie - uwaga, dalszy kod wykonuje się pomimo ustawienia alarmu. W celu odczekania na wykonanie przerwania należy ustawić zmienną flagową i pętlę opartą na tej zmiennej. Przykładowy kod: #include <stdio.h> #include "pico/stdlib.h" // Alarm int64_t alarm(alarm_id_t id, void *user_data){ gpio_put(15, true); return 0; } int main() { stdio_init_all(); // Zainicjuj pin z diodą gpio_init(15); gpio_set_dir(15, true); gpio_put(15, false); // Dodaj alarm add_alarm_in_ms(1000, alarm, NULL, false); // Nieskończona pętla - WYMAGANA!!! while (1) { tight_loop_contents(); } } Jak widzimy w alarmie musimy podać czas (w tym przypadku ms, istnieje również wersja tej funkcji pracująca z mikrosekundami - add_alarm_in_us), callback naszego alarmu - funkcję, która będzie wykonana. Jej deklaracja musi być zgodna z tą powyżej (oprócz nazwy). Do tego możemy przesłać jakieś dane w formie wskaźnika do np. struktury. Ostatni parametr definiuje czy alarm ma się wykonać, kiedy jego czas już dawno minął. #include <stdio.h> #include "pico/stdlib.h" int64_t alarm(alarm_id_t id, void *user_data){ uint* pinId = (uint*) user_data; gpio_put(*pinId, true); return 0; } int main() { stdio_init_all(); // Zainicjuj pin z diodą gpio_init(15); gpio_set_dir(15, true); gpio_put(15, false); // Zmienna tymczasowa uint pinId = 15; add_alarm_in_ms(1000, alarm, &pinId, false); // Nieskończona pętla - WYMAGANA!!! while (1) { tight_loop_contents(); } } Jak widać na tym kodzie - możemy bez problemu przesłać np. numer gpio, które chcemy zmodyfikować w naszym alarmie 😉 Możemy również pobrać ID alarmu i w dowolnym momencie przerwać nasz alarm 😉 alarm_id_t id = add_alarm_in_ms(1000, alarm, &pinId, false); cancel_alarm(id); // Przerwij alarm o określonym ID Funkcje add_alarm_in_ms(<opóźnienie_ms>, <callback>, <user_data>, <aktywuj_przeszły>) - dodaje alarm opóźniony o podany czas wyrażony w milisekundach, ostatni parametr opisuje, że jeżeli dany moment nastąpi zanim ta metoda się wykona to callback również zostanie wykonany add_alarm_in_us(<opóźnienie_us>, <callback>, <user_data>, <aktywuj_przeszły>) - to samo co powyżej tylko zamiast ms są us 😉 cancel_alarm(<alarm_id>) - anuluje alarm o określonym ID pozyskanym z funkcji dodawania alarmu Oprócz alarmów są też timery... Teraz czas na timery - alarmy powtarzające się co pewien czas 😉 Timer inicjujemy podobnie do alarmu 🙂 struct repeating_timer timer; add_repeating_timer_ms(100, tCallback, &pin, &timer); Z tym, że na początku musimy stworzyć strukturę timera, potem możemy go dodać podając czas, przerwanie, dane użytkownika oraz adres naszej struktury. bool tCallback(repeating_timer_t * timer){ status = !status; uint* pinId = (uint*) timer->user_data; gpio_put(*pinId, status); return true; } Funkcja przerwania też jest ciut inna - ma inny typ zwracany i tylko jeden parametr - wskaźnik do timera. Jak widzimy w powyższym przykładzie dane są przechowywane wewnątrz naszego timera, więc by je uzyskać musimy odwołać się do pola zawartego wewnątrz struktury. Przykładowy kod: #include <stdio.h> #include "pico/stdlib.h" bool status = false; bool tCallback(repeating_timer_t * timer){ status = !status; uint* pinId = (uint*) timer->user_data; gpio_put(*pinId, status); return true; } int main() { stdio_init_all(); // Zainicjuj pin z diodą gpio_init(15); gpio_set_dir(15, true); gpio_put(15, false); uint pin = 15; struct repeating_timer timer; add_repeating_timer_ms(100, tCallback, &pin, &timer); // Nieskończona pętla - WYMAGANA!!! while (1) { tight_loop_contents(); } } Funkcje add_repeating_timer_ms(<czas_ms>, <callback>, <dane_usera>, <timer>) - uruchamia powtarzający się co określony w ms czas timer. Wcześniej należy stworzyć strukturę timera, patrz przykład. add_repeating_timer_us(<czas_us>, <callback>, <dane_usera>, <timer>) - to samo co powyżej tylko dla us 😉 cancel_repeating_timer(<timer>) - anuluje wykonywanie danego timera 😉 Wielowątkowość Miało nie być... jednak jest... Do obsługi wielowątkowości musimy dołączyć bibliotekę - pico_multicore do pliku CMakeLists.txt. Szukamy linijki "target_link_libraries(<NAZWA_PROJEKTU> pico_stdlib)" i dodajemy pod nią "target_link_libraries(<NAZWA_PROJEKTU> pico_multicore)". Następnie możemy stworzyć prosty program, który uruchomi kod na drugim rdzeniu - przykład? #include <stdio.h> #include "pico/stdlib.h" #include "pico/multicore.h" void core1_entry() { printf("To jest rdzeń 2!\n"); while (1) tight_loop_contents(); } int main() { stdio_init_all(); printf("Witaj wiele rdzeni!\n"); printf("To jest rdzeń 1!\n"); multicore_launch_core1(core1_entry); } Do uruchomienia funkcji na innym rdzeniu używamy multicore_launch_core1. W środku umieszczamy nazwę funkcji, którą chcemy uruchomić. Jej deklaracja musi się pokrywać z tą zaprezentowaną powyżej - musi to być funkcja nie zwracająca żadnej wartości i bez parametrów. Ale jak przekazać informacje między rdzeniami? Do tego należy wykorzystać funkcje multicore_fifo_push_blocking(<value>) oraz multicore_fifo_pop_blocking(). Pierwszą umieszczamy w kodzie wątku źródłowego, a drugą w docelowym. Pierwsza z nich ustawia wartość semafora, druga odczytuje. Dopóki semafor nie zostanie odczytany z wątku docelowego, wtedy kod na wątku źródłowym oczekuje (nie wykonuje innych operacji). Oprócz tego istnieje funkcja, która ustawia semafor z timeoutem, aczkolwiek tutaj odeślę do oficjalnej dokumentacji Pi Pico 😉 Przykład kodu z semaforami? #include <stdio.h> #include "pico/stdlib.h" #include "pico/multicore.h" #define FLAG_VALUE 123 void core1_entry() { multicore_fifo_push_blocking(FLAG_VALUE); uint32_t g = multicore_fifo_pop_blocking(); if (g != FLAG_VALUE) // Porównanie 1 printf("Semafor (0) nie jest OK :(\n"); else printf("Semafor (0) jest OK ;)\n"); while (1) tight_loop_contents(); } int main() { stdio_init_all(); printf("Witajcie rdzenie!\n"); multicore_launch_core1(core1_entry); sleep_ms(100); uint32_t g = multicore_fifo_pop_blocking(); if (g == FLAG_VALUE){ // Porównanie 2 multicore_fifo_push_blocking(FLAG_VALUE); printf("Semafor (1) jest OK ;)\n"); } else { printf("Semafor (1) nie jest OK :(\n"); } } Kod wykona się całkowicie poprawnie. Jego efekt na konsoli UART powinien wyglądać następująco: Działanie kodu w uproszczony sposób można opisać następująco: Rdzeń 1 Wyślij semafor o wartości FLAG_VALUE i oczekuj aż zostanie odebrany Oczekuj na semafor Jeżeli otrzymany semafor ma wartość FLAG_VALUE poinformuj, że semafor 0 JEST OK, w innym przypadku poinformuj, że semafor NIE JEST OK Zapętl się w nieskończoność Rdzeń 0 Wyświetl powitanie Uruchom funkcję core1_entry() na rdzeniu 1 Odczekaj 100ms Oczekuj na semafor Jeżeli semafor ma wartość FLAG_VALUE Poinformuj, że semafor 1 JEST OK Wyślij semafor o wartości FLAG_VALUE i oczekuj na odbiór Jeżeli punkt 5 nie został spełniony Poinformuj, że semafor 1 NIE JEST OK Jak można wywnioskować z powyższego opisu, gdy rdzeń 1 wyśle niepoprawny semafor do rdzenia 0, to kod z rdzenia drugiego się nie wykona. Możesz sam to sprawdzić podmieniając wartość FLAG_VALUE w funkcji multicore_fifo_push_blocking w core1_entry 😉 Wtedy rezultat w konsoli będzie wyglądał następująco: Funkcje multicore_launch_core1(<funkcja>) - uruchom funkcję na drugim rdzeniu (wyłącznie void bez parametrów) multicore_fifo_push_blocking(<wartość>) - wyślij semafor do drugiego rdzenia i oczekuj na jego pobranie multicore_fifo_pop_blocking() - pobierz wartość semafora (wykonywane z drugiego rdzenia) Zaawansowane Przerwania? Inne tematy? Temat zaawansowanych przerwań opisuje biblioteka hardware_irq. Tutaj odsyłam do oficjalnej dokumentacji Pi Pico 😉 Nie mogę przecież wszystkiego podawać na dłoni 😉 Ten poradnik miał omówić najbardziej podstawowe zagadnienia z pisania na Pi Pico. Powyższych do nich nie zaliczam... Dodatkowo warto też zapoznać się z wbudowanym RTC 🙂 Dodatkowe funkcje sleep_ms(<czas>) - odczekaj określony w ms czas sleep_us(<czas>) - odczekaj n mikrosekund Visual Studio - alternatywna metoda kompilacji (zalecana) Alternatywnie projekt można skompilować poprzez konsolę developerską (Developer Command Prompt). Wchodzimy do folderu z plikami, potem do podfolderu "build" np. cd C:\Users\nov11\Documents\Pico\pico-project-generator\Blinky\build Potem wpisujemy komendę generującą pliki cmake -G "NMake Makefiles" .. Potem możemy skompilować program nmake I gotowe 😉 Pytania? Po to jest sekcja komentarzy 😉
  10. Często zdarza się że potrzebne jest załączenie urządzenia na określony czas np. oświetlenia zewnętrznego. Do tego celu stosuje się programatory czasowe nazywane często wyłącznikami czasowymi. Są to urządzenia, dzięki którym możemy w skuteczny sposób zarządzać działaniem różnych urządzeń elektrycznych i zaoszczędzić zużycie energii. 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 » W tym artykule dowiesz się jak zbudować własny programator czasowy z wykorzystaniem Arduino oraz modułu DS1302. Dodatkowo zapoznasz się z obsługą modułu czasowego oraz wykorzystaniem pamięci EEPROM w praktyce. Potrzebne elementy: płytka Arduino płytka stykowa przewody do płytki stykowej moduł RTC DS1302 wyświetlacz LCD Nokia 5110 przekaźnik 5V rezystory 2 x 220 Ω, 5 x 10 kΩ dioda LED 5 przycisków chwilowych Wykonanie układu Zaczynamy od wyprowadzenia zasilania z Arduino na płytkę stykową Następnie umieszczamy wyświetlacz, przyciski oraz diodę LED Podłączamy moduł DS1302 (VCC do +5V, GND do masy, CLK do pinu A1, DAT do pinu A2, RST do pinu A3) Następnie podłączamy wyświetlacz (RST do pinu 3, CE do pinu 4, DC do pinu 5, DIR do pinu 6, CLK do pinu 7, VCC do +5V, GND do masy). Pin BL podłączamy do +5V poprzez rezystor 220 Ω Łączymy diodę LED z pinem 13 poprzez rezystor 220 Ω oraz do każdego z przełączników podłączamy +5V oraz podłączamy je do pinów 8, 9, 10, 11, 12 Łączymy piny 8, 9, 10, 11, 12 z masą poprzez rezystory 10 kΩ Podłączamy moduł przekaźnika 5V (VCC do +5V, GND do masy, IN do pinu A0) Gotowy układ prezentuje się następująco: Tak wygląda schemat połączeń układu: Programowanie Arduino Na początku wywołujemy biblioteki do obsługi modułu DS1302, wyświetlacza oraz pamięci EEPROM #include <virtuabotixRTC.h> #include <Adafruit_GFX.h> #include <Adafruit_PCD8544.h> #include <EEPROM.h> Następnie podajemy piny pod które podłączony jest wyświetlacz i moduł czasowy Adafruit_PCD8544 display = Adafruit_PCD8544(7, 6, 5, 4, 3); virtuabotixRTC myRTC(A1, A2, A3); Dodajemy niezbędne zmienne oraz określamy piny pod które podłączone są przyciski do nastawy czasu, dioda LED oraz przekaźnik int godzina1; int godzina2; int minuta1; int minuta2; int stang1; int stang2; int stanm1; int stanm2; int stanzm; int zmiana; int zmienna; int odczytg1; int odczytg2; int odczytm1; int odczytm2; #define przyciskg1 8 #define przyciskg2 9 #define przyciskm1 10 #define przyciskm2 11 #define przyciskzm 12 #define dioda 13 #define przekaznik A0 W funkcji startowej inicjalizujemy wyświetlacz oraz definiujemy wejścia/wyjścia void setup() { display.begin(); display.setContrast(50); display.clearDisplay(); pinMode(przyciskg1, INPUT); pinMode(przyciskg2, INPUT); pinMode(przyciskm1, INPUT); pinMode(przyciskm2, INPUT); pinMode(przyciskzm, INPUT); pinMode(dioda, OUTPUT); pinMode(przekaznik, OUTPUT); digitalWrite(przekaznik, HIGH); } Warto też dodać fragment umożliwiający wpisanie nastaw czasowych do pamięci przy pierwszym programowaniu wykorzystując funkcję EEPROM.write() oraz ustawienie godziny modułu czasu rzeczywistego (należy pamiętać, aby po ustawieniu tych wartości wgrać kod jeszcze raz ze skomentowanym tym fragmentem) //EEPROM.write(0, 16); //ustawianie domyślengo czasu //EEPROM.write(1, 16); //EEPROM.write(2, 18); //EEPROM.write(3, 20); //myRTC.setDS1302Time(00, 23, 41, 5, 12, 3, 2021); //ustawianie czasu sekundy, minuty, godziny, dzień tygodnia, dzień miesiąca, miesiąc, rok W funkcji loop na początku dodajemy funkcje if która będzie służyć do odczytu danych z pamięci EEPROM oraz porównywania ich z czasem rzeczywistym (domyślna praca) if(zmiana==0) { //odczyt danych z pamięci EEPROM oraz porównywanie ich z czasem rzeczywistym (domyślna praca) display.clearDisplay(); display.setTextColor(BLACK); display.setTextSize(1); odczytg1 = EEPROM.read(0); odczytg2 = EEPROM.read(1); odczytm1 = EEPROM.read(2); odczytm2 = EEPROM.read(3); display.setCursor(0,0); display.print("Zalacz o:"); display.setCursor(0,10); display.print(odczytg1); display.print(":"); display.print(odczytm1); display.setCursor(0,20); display.print("Wylacz o:"); display.setCursor(0,30); display.print(odczytg2); display.print(":"); display.print(odczytm2); display.setCursor(0,40); display.print("zmiana:"); display.print("OFF"); display.display(); myRTC.updateTime(); delay(1000); if(myRTC.hours>=odczytg1 && myRTC.minutes>=odczytm1 && myRTC.minutes<odczytm2) { digitalWrite(dioda, HIGH); digitalWrite(przekaznik, LOW); } else if(myRTC.hours>=odczytg1 && myRTC.minutes>=odczytm1 && myRTC.hours<odczytg2) { digitalWrite(dioda, HIGH); digitalWrite(przekaznik, LOW); } if(myRTC.hours>=odczytg2 && myRTC.minutes>=odczytm2) { digitalWrite(dioda, LOW); digitalWrite(przekaznik, HIGH); } } Następnie dodajemy funkcje if która będzie sprawdzać czy przycisk umożliwiający zmianę nastaw czasowych został wciśnięty oraz będzie dodawać jedynkę do zmiennej „zmiana”. Zmienną tą wykorzystamy do możliwości zmiany czasu stanzm = digitalRead(przyciskzm); if(stanzm==HIGH){ zmiana++; delay(500); } Kolejna funkcja if będzie miała za zadanie wpisanie nowych nastaw czasowych do pamięci EEPROM wykorzystując funkcję EEPROM.update() oraz wyzerowanie wybranych zmiennych if(zmiana==2) { EEPROM.update(0, godzina1); //zapis nowych nastaw czasowych EEPROM.update(1, godzina2); EEPROM.update(2, minuta1); EEPROM.update(3, minuta2); delay(200); zmiana=0; zmienna=0; godzina1=0; godzina2=0; minuta1=0; minuta2=0; } Tworzymy funkcję o nazwie „zmianaczasu” która będzie służyć do zmiany nastaw czasowych. W tej funkcji zastosujemy między innymi funkcję EEPROM.read() służącą do odczytu bajtu danych z pamięci EEPROM void zmianaczasu() { if(zmiana==1) { //ustawianie nowych nastaw czasowych stang1 = digitalRead(przyciskg1); stang2 = digitalRead(przyciskg2); stanm1 = digitalRead(przyciskm1); stanm2 = digitalRead(przyciskm2); odczytg1 = EEPROM.read(0); odczytg2 = EEPROM.read(1); odczytm1 = EEPROM.read(2); odczytm2 = EEPROM.read(3); while(zmienna < 1) { //dodawanie do zmiennych globalnych odczytu z pamięci EEPROM godzina1=godzina1+odczytg1; godzina2=godzina2+odczytg2; minuta1=minuta1+odczytm1; minuta2=minuta2+odczytm2; zmienna++; } display.clearDisplay(); display.setTextColor(BLACK); display.setTextSize(1); display.setCursor(0,0); display.print("Zalacz o:"); display.setCursor(0,10); display.print(godzina1); display.print(":"); display.print(minuta1); display.setCursor(0,20); display.print("Wylacz o:"); display.setCursor(0,30); display.print(godzina2); display.print(":"); display.print(minuta2); display.setCursor(0,40); display.print("zmiana:"); display.print("ON"); display.display(); if(stang1==HIGH) { godzina1++; delay(500); if(godzina1==24) { godzina1=0; } } if(stang2==HIGH) { godzina2++; delay(500); if(godzina2==24) { godzina2=0; } } if(stanm1==HIGH) { minuta1++; delay(500); if(minuta1==60) { minuta1=0; } } if(stanm2==HIGH) { minuta2++; delay(500); if(minuta2==60) { minuta2=0; } } } } Tak wygląda gotowy program: #include <virtuabotixRTC.h> #include <Adafruit_GFX.h> #include <Adafruit_PCD8544.h> #include <EEPROM.h> Adafruit_PCD8544 display = Adafruit_PCD8544(7, 6, 5, 4, 3); virtuabotixRTC myRTC(A1, A2, A3); int godzina1; int godzina2; int minuta1; int minuta2; int stang1; int stang2; int stanm1; int stanm2; int stanzm; int zmiana; int zmienna; int odczytg1; int odczytg2; int odczytm1; int odczytm2; #define przyciskg1 8 #define przyciskg2 9 #define przyciskm1 10 #define przyciskm2 11 #define przyciskzm 12 #define dioda 13 #define przekaznik A0 void setup() { display.begin(); display.setContrast(50); display.clearDisplay(); pinMode(przyciskg1, INPUT); pinMode(przyciskg2, INPUT); pinMode(przyciskm1, INPUT); pinMode(przyciskm2, INPUT); pinMode(przyciskzm, INPUT); pinMode(dioda, OUTPUT); pinMode(przekaznik, OUTPUT); digitalWrite(przekaznik, HIGH); //EEPROM.write(0, 16); //ustawianie domyślengo czasu //EEPROM.write(1, 16); //EEPROM.write(2, 18); //EEPROM.write(3, 20); //myRTC.setDS1302Time(00, 23, 41, 5, 12, 3, 2021); //ustawianie czasu sekundy, minuty, godziny, dzień tygodnia, dzień miesiąca, miesiąc, rok } void loop() { if(zmiana==0) { //odczyt danych z pamięci EEPROM oraz porównywanie ich z czasem rzeczywistym (domyślna praca) display.clearDisplay(); display.setTextColor(BLACK); display.setTextSize(1); odczytg1 = EEPROM.read(0); odczytg2 = EEPROM.read(1); odczytm1 = EEPROM.read(2); odczytm2 = EEPROM.read(3); display.setCursor(0,0); display.print("Zalacz o:"); display.setCursor(0,10); display.print(odczytg1); display.print(":"); display.print(odczytm1); display.setCursor(0,20); display.print("Wylacz o:"); display.setCursor(0,30); display.print(odczytg2); display.print(":"); display.print(odczytm2); display.setCursor(0,40); display.print("zmiana:"); display.print("OFF"); display.display(); myRTC.updateTime(); delay(1000); if(myRTC.hours>=odczytg1 && myRTC.minutes>=odczytm1 && myRTC.minutes<odczytm2) { digitalWrite(dioda, HIGH); digitalWrite(przekaznik, LOW); } else if(myRTC.hours>=odczytg1 && myRTC.minutes>=odczytm1 && myRTC.hours<odczytg2) { digitalWrite(dioda, HIGH); digitalWrite(przekaznik, LOW); } if(myRTC.hours>=odczytg2 && myRTC.minutes>=odczytm2) { digitalWrite(dioda, LOW); digitalWrite(przekaznik, HIGH); } } stanzm = digitalRead(przyciskzm); if(stanzm==HIGH){ zmiana++; delay(500); } if(zmiana==2) { EEPROM.update(0, godzina1); //procedura zapisu gdy przycisk zmiany zostanie wciśnięty 2 raz EEPROM.update(1, godzina2); EEPROM.update(2, minuta1); EEPROM.update(3, minuta2); delay(200); zmiana=0; zmienna=0; godzina1=0; godzina2=0; minuta1=0; minuta2=0; } zmianaczasu(); } void zmianaczasu() { if(zmiana==1) { //ustawianie nowych nastaw czasowych stang1 = digitalRead(przyciskg1); stang2 = digitalRead(przyciskg2); stanm1 = digitalRead(przyciskm1); stanm2 = digitalRead(przyciskm2); odczytg1 = EEPROM.read(0); odczytg2 = EEPROM.read(1); odczytm1 = EEPROM.read(2); odczytm2 = EEPROM.read(3); while(zmienna < 1) { //dodawanie do zmiennych globalnych odczytu z pamięci EEPROM godzina1=godzina1+odczytg1; godzina2=godzina2+odczytg2; minuta1=minuta1+odczytm1; minuta2=minuta2+odczytm2; zmienna++; } display.clearDisplay(); display.setTextColor(BLACK); display.setTextSize(1); display.setCursor(0,0); display.print("Zalacz o:"); display.setCursor(0,10); display.print(godzina1); display.print(":"); display.print(minuta1); display.setCursor(0,20); display.print("Wylacz o:"); display.setCursor(0,30); display.print(godzina2); display.print(":"); display.print(minuta2); display.setCursor(0,40); display.print("zmiana:"); display.print("ON"); display.display(); if(stang1==HIGH) { godzina1++; delay(500); if(godzina1==24) { godzina1=0; } } if(stang2==HIGH) { godzina2++; delay(500); if(godzina2==24) { godzina2=0; } } if(stanm1==HIGH) { minuta1++; delay(500); if(minuta1==60) { minuta1=0; } } if(stanm2==HIGH) { minuta2++; delay(500); if(minuta2==60) { minuta2=0; } } } } Krótki opis zasady działania układu Po włączeniu układu na wyświetlaczu ukazują się aktualne nastawy czasowe (czas załączenia oraz czas wyłączenia) oraz informacja o włączeniu lub wyłączeniu możliwości zmiany tych nastaw. Układ załącza oraz wyłącza przekaźnik o określonej godzinie i minucie. Aby zmienić czas załączenia oraz wyłączenia należy przytrzymać przycisk zmiany. Na wyświetlaczu ukaże się informacja o możliwości zmiany czasów „zmiana: ON”. Przyciskami możemy ustawiać godzinę oraz minutę o której ma się załączyć oraz wyłączyć przekaźnik. Aby zapisać nowe ustawienia naciskamy przycisk zmiany. Na wyświetlaczu ukaże się informacja o wyłączeniu możliwości zmiany czasów „zmiana: OFF”. Odłączenie zasilania lub jego awaria nie spowoduje zaburzenia działania układu oraz utraty zapisu aktualnie zapisanych nastaw czasowych. Materiały potrzebne do odtworzenia opisywanego projektu programator czasowy.rar Podsumowanie W tym artykule postarałem się przedstawić jedno z wielu zastosowań Arduino w automatyce domowej. Prezentowany układ można wykorzystać do sterowania dowolnym urządzeniem elektrycznym, ogranicza nas tylko wyobraźnia. Zachęcam do samodzielnego eksperymentowania i szukania nowych założeń i rozwiązań omówionego układu. Powodzenia!
  11. Istotnym elementem zapewnienia ciągłości działania naszej infrastruktury jest monitoring. Możemy do tematu podejść na wiele sposobów, bo nic nie stoi na przeszkodzie, aby codziennie sprawdzać parametry pracy naszego serwera za pomocą dostępnych w systemie narzędzi diagnostycznych (top, df, fdisk, itp.) oraz przeglądać zapisane logi. Nie jest to jednak idealne rozwiązanie, bo czasami ilość informacji może być zbyt duża. Dlatego warto używać oprogramowania, które zrobi to za nas, a w dodatku wszystko będzie czytelnie przedstawione na wykresach. Narzędzi do monitoringu pracy jest sporo. Niektóre, jak Nagios czy Zabbix są sprawdzone i używane przez wielu, natomiast ich wdrożenie dla osób z małym doświadczeniem pracy z systemem Linux czy sieciami komputerowymi może wydawać się kłopotliwe. Istnieją wygodne w użyciu rozwiązania, jak CloudRadar, ale są płatne. Dlatego dobrą alternatywą wydaje się być połączenie Grafany i Promotheus, bezpłatnych narzędzi (płatne jest jedynie hostowanie rozwiązania na serwerach producenta) stosunkowo prostych w konfiguracji. 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 » Grafana oferuje wsparcie dla Windows, macOS i Linux (architektury 64-bit i ARM64). Większość naszych projektów hostujemy najczęściej na systemach z rodziny Linux, więc instalację zaprezentuję na systemie Ubuntu 20.04. Domena nie jest konieczna, jednak wtedy panel Grafany będzie dostępny pod adresem [adres_IP]:3000. W domowych warunkach lokalnej sieci komputerowej możemy także ustawić odpowiednie wartości w pliku hosts, dzięki czemu uzyskamy działającą lokalnie domenę, co dokładniej opisałem w dalszej części. W wymaganiach jest napisane, że nasz serwer musi być wyposażony w minimum 255 MB pamięci operacyjnej, czyli bardzo niewiele. Grafanę zainstaluję na lokalnym serwerze z Ubuntu 20.04, na którym obecnie działa tylko Domoticz (domyślny port 8080), a do połączenia będę używał SSH. SSH Większość osób korzysta z SSH, ale jeśli na naszym lokalnym serwerze nie mamy zainstalowanej tej usługi, to dokonamy tego w prosty sposób: sudo apt update sudo apt install openssh-server Adres IP niezbędny do połączenia uzyskamy natomiast wydając polecenie ip a lub ifconfig . OpenSSH jest już w Windows 10, więc w zależności od systemu otwieramy Terminal lub Wiersz polecenia i wydajemy polecenie ssh [nazwa_użytkownika]@[adres_IP] Mój lokalny serwer ma przypisany adres IP 192.168.1.129, a użytkownik nazywa się michal, więc połączę się poprzez ssh michal@192.168.1.129 Do zainstalowania Grafany możemy użyć Dockera. To rozwiązanie, gdzie w odseparowanych od systemu kontenerach mamy możliwość uruchomienia wiele aplikacji. Uruchomienia Grafany wymaga jednego polecenia: docker run -d --name=grafana -p 3000:3000 grafana/grafana:7.4.0-ubuntu Osobiście uważam, że Docker służący do uruchomienia wyłącznie Grafany jest kiepskim pomysłem. Zajmuje stosunkowo dużo zasobów, które można przeznaczyć na rozwój naszych projektów. Jeśli jednak jesteśmy do Dockera przekonani, to jak najbardziej można z niego skorzystać. Powyższe polecenie uruchomi kontener z Grafaną „w tle” (opcja -d) na porcie 3000 (opcja -p), który jest domyślny. Grafana — instalacja Narzędzie pobierzemy z oficjalnej strony wydawcy, gdzie znajdują się również instrukcje instalacji dla pozostałych systemów operacyjnych. Jak widać, Grafana może być zainstalowana na kilka sposobów. Chcemy ją zainstalować na Ubuntu, więc najszybsza metoda to skorzystanie z pliku DEB, czyli wykonamy polecenia z pierwszego szarego tła. Na początek poleca się zainstalować pakiet adduser oraz bibliotekę libfontconfig1. adduser służy do dodawania do systemu nowego użytkownika i powinien już być zainstalowany. Z kolei libfontconfig1 stosuje się do konfiguracji czcionek. Wykonajmy zatem pierwsze polecenie. Nieco je zmodyfikowałem, ponieważ apt-get jest równoznaczny z apt, a parametr -y powoduje, że libfontconfig1 zostanie zainstalowany bez pytania użytkownika (Do you want to continue?). Teraz za pomocą wget pobierzemy paczkę DEB z Grafaną. Wydajemy drugie polecenie. Rozmiar pliku wynosi 49 MB. Standardowo instalujemy komendą sudo dpkg -i. Istotną informację zaznaczyłem w czerwonej ramce. Należy wydać dwa dodatkowe polecenia, aby usługa wystartowała. Wykonujemy więc sudo systemctl daemon-reload oraz sudo systemctl enable grafana-server Po wydaniu tych poleceń Grafana jest uruchomiona. Wynik sudo systemctl status grafana-server stanowi potwierdzenie. Zobaczmy za pomocą netstat-a, jakie porty są w tej chwili zajęte. Grafana — panel użytkownika Z powodzeniem zainstalowaliśmy i uruchomiliśmy Grafanę na porcie 3000. Teraz jest dobry moment, aby zapoznać się z panelem. Uruchamiamy przeglądarkę i w pasku adresu wpisujemy [adres_IP]:3000, w moim przypadku 192.168.1.129:3000 (w dalszej części ustawimy proxy, aby wystarczyło wpisać tylko domenę). Login i hasło to admin. Nie jest to najlepsze podejście z punktu widzenia bezpieczeństwa, ale już w drugim kroku możemy ustawić nowe hasło. Następnie jesteśmy przenoszeni do głównego panelu. Prometheus i Node Exporter — instalacja oraz konfiguracja Grafana nie jest oprogramowaniem do monitoringu systemu, pełni jedynie funkcję wyświetlania i agregowania zgromadzonych statystyk. Do zbierania metryk wykorzystamy narzędzie Prometheus z Node Exporter, które jest często stosowane razem z Grafaną, dzięki czemu połączenie jest bardzo proste. Najpierw dodamy dwóch użytkowników do systemu. sudo useradd --no-create-home --shell /usr/sbin/nologin prometheus sudo useradd --no-create-home --shell /bin/false node_exporter Ich nazwy to prometheus oraz node_exporter, nie utworzyliśmy im katalogu domowego, prometheus będzie miał blokowane próby logowania, a node_exporter nie ma żadnego dostępu do powłoki. Teraz utwórzmy katalogi na pliki konfiguracyjne i zebrane dane. sudo mkdir /etc/prometheus sudo mkdir /var/lib/prometheus Zmieniamy właściciela tych katalogów na nowego użytkownika prometheus. sudo chown prometheus:prometheus /etc/prometheus sudo chown prometheus:prometheus /var/lib/prometheus Przechodzimy do strony https://github.com/prometheus/node_exporter/releases i pobieramy na lokalny dysk najnowszą wersję Node Explorer odpowiednią do naszej platformy. Na dzień 04.04.2021 jest to node_exporter-1.0.1.linux-amd64.tar.gz. Wysyłamy pobrany plik poprzez WinSCP na serwer, a następnie rozpakowujemy archiwum. Z nowego katalogu przenosimy plik node_exporter do katalogu /usr/local/bin i zmieniamy właściciela na użytkownika node_exporter. Bardzo dobrym pomysłem jest ustawienie, aby Node Exporter uruchamiał się automatycznie po restarcie serwera. W tym celu w /etc/systemd/system tworzymy plik node_exporter.service: sudo nano /etc/systemd/system/node_exporter.service Dodajemy do niego poniższą treść: [Unit] Description=Node Exporter Wants=network-online.target After=network-online.target [Service] User=node_exporter Group=node_exporter Type=simple ExecStart=/usr/local/bin/node_exporter [Install] WantedBy=multi-user.target Tak jak zwykle, po tej operacji wykonujemy trzy polecenia. sudo systemctl daemon-reload sudo systemctl start node_exporter sudo systemctl enable node_exporter Czas na instalację Prometheusa. Pobieramy najnowszą wersję zgodną z naszym środowiskiem (na dzień 04.02.2021 jest to prometheus-2.24.1.linux-amd64.tar.gz). Przesyłamy plik poprzez WinSCP na nasz serwer i wypakowujemy dane z archiwum. Z utworzonego w wyniku działania tar katalogu przenosimy pliki prometheus i promtool do /usr/local/bin oraz katalogi consoles i console_libraries do /etc/prometheus. Ponownie ustawiamy odpowiedniego właściciela. Ostatnią czynnością przed uruchomieniem jest dodanie odpowiednich wpisów do pliku /etc/prometheus/prometheus.yml. To plik YAML, więc wcięcia mają znaczenie. Najlepiej skopiować i wkleić poniższą zawartość: global: scrape_interval: 15s evaluation_interval: 15s rule_files: # - "first.rules" # - "second.rules" scrape_configs: - job_name: 'prometheus' scrape_interval: 5s static_configs: - targets: ['localhost:9090'] - job_name: 'node_exporter' scrape_interval: 5s static_configs: - targets: ['localhost:9100'] Na koniec ustawiamy jeszcze właściciela pliku. sudo chown prometheus:prometheus /etc/prometheus/prometheus.yml Usługę uruchamiamy wykonując polecenie sudo -u prometheus /usr/local/bin/prometheus --config.file /etc/prometheus/prometheus.yml --storage.tsdb.path /var/lib/prometheus/ --web.console.templates=/etc/prometheus/consoles --web.console.libraries=/etc/prometheus/console_libraries Powinniśmy otrzymać komunikat jak na poniższym zrzucie ekranu. W przeglądarce wchodzimy pod [adres_IP]:9090, a w kliencie SSH wykonujemy Ctrl+C. W tym panelu nic nie musimy ustawiać. Utwórzmy jeszcze usługę systemową, aby Prometheus uruchamiał się automatycznie. sudo nano /etc/systemd/system/prometheus.service Do pliku wklejamy: [Unit] Description=Prometheus Monitoring Wants=network-online.target After=network-online.target [Service] User=prometheus Group=prometheus Type=simple ExecStart=/usr/local/bin/prometheus \ --config.file /etc/prometheus/prometheus.yml \ --storage.tsdb.path /var/lib/prometheus/ \ --web.console.templates=/etc/prometheus/consoles \ --web.console.libraries=/etc/prometheus/console_libraries ExecReload=/bin/kill -HUP $MAINPID [Install] WantedBy=multi-user.target Tradycyjne polecenia: sudo systemctl daemon-reload sudo systemctl enable prometheus sudo systemctl start prometheus Połączenie Prometheusa z Grafaną W Grafanie najeżdżamy kursorem na ikonę koła zębatego, po czym wybieramy Data Sources, a następnie Add data source. Pierwsza opcja (Prometheus) jest odpowiednia. Uzupełniamy jedynie pole URL, wpisując http://localhost:9090. Po naciśnięciu Save & Test powinniśmy zobaczyć poniższy komunikat. Najeżdżamy kursorem na ikonę plusa i wybieramy Import. W pierwszym polu wpisujemy 1860 i wybieramy Source (dostępny jedynie Prometheus). Nazwę możemy ustawić dowolną, Node Exporter Full jest domyślna. Po zaakceptowaniu od razu zobaczymy czytelne wykresy. Wybieramy logo Grafany z menu po lewej stronie. Pojawi się komunikat o niezapisanych zmianach. Oczywiście zapisujemy nasz dashboard. Domena Ustawmy reverse proxy, aby uniknąć w przyszłości wpisywania adresu IP i portu w pasku adresu. Jeśli dysponujemy publicznym serwerem, wystarczy podpiąć do niego naszą domenę. W przypadku lokalnych serwerów najprościej jest ustawić w lokalnym pliku hosts rozwiązywanie adresu IP serwera na dowolnie wybraną domenę. W systemach z rodziny Linux plik hosts znajduje się w katalogu /etc, a w systemie Windows w folderze C:\Windows\Systems32\etc\drivers. Do jego edycji konieczne są uprawnienia administratora. W Menu Start wyszukujemy Notatnik, naciskamy na jego ikonie prawym przyciskiem myszy i wybieramy Uruchom jako administrator. Otwieramy plik hosts z wyżej wymienionej lokalizacji. W dowolnej linii dopisujemy: [adres_IP] [domena] np. 192.168.1.129 grafana.local Reverse proxy Od teraz wystarczy wpisać domenę i port, aby uzyskać dostęp do panelu Grafany. Chcielibyśmy jednak uniknąć podawania portu. Do tego musimy użyć reverse proxy. Najprościej zainstalować serwer WWW NGINX, który często służy właśnie jako proxy. sudo apt install nginx NGINX działa ze swoją domyślną konfiguracją, dlatego musimy ją wyłączyć. sudo unlink /etc/nginx/sites-enabled/default Tworzymy w /etc/nginx/sites-enabled dowolny plik, aczkolwiek najlepiej, aby w nazwie była nasza domena. Rozszerzenie conf jest obowiązkowe. Moja konfiguracja nazywa się grafana.local.conf. Dodajemy następującą zawartość (pod [adres_IP] wstawiamy adres serwera): server { listen 80; location / { proxy_pass http://[adres_IP]:3000 } } I restartujemy NGINX poleceniem sudo systemctl restart nginx Od tej pory mamy dostęp do Grafany wpisując jedynie nazwę domeny. Dowiedzieliśmy się, w jaki sposób monitorować nasz serwer w wygodny i łatwy do wdrożenia sposób. Grafana ma wiele opcji i nie sposób przedstawić ich w jednym artykule. Zachęcam do samodzielnego odkrywania możliwości tego narzędzia.
  12. 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)
  13. Słowem wstępu Niniejszy poradnik jest przeznaczony dla osób, które nie miały dotąd styczności z programowaniem aplikacji wizualno-obiektowych przy użyciu języka C# oraz platformy .NET (czytane jako dotnet). Skupimy się tu na podstawach języka oraz na tym, jak krok po kroku napisać wybrane aplikacje. Na początek trochę teorii Język C# jest odpowiedzą firmy Microsoft na obiektowy język Java. Struktura obu języków jest podobna, a więc osoba programująca do tej pory w Javie szybko będzie mogła stosować w C# znane już sobie mechanizmy. Platforma .NET pozwala na pisanie programów nie tylko w C#, ale także w F#, C++, Visual Basic'u, Pythonie, HTML-u, Javascript’cie czy Node.js - .NET zakłada, że wszystkie języki programowania są równe, jednak to, że Microsoft stworzył język C# oraz platformę .NET sprawiło, że programowanie w tym środowisku przy użyciu języka C# jest najbardziej wydajne. C# oraz platforma .NET pozwala także na pisanie aplikacji sieciowych, jednak ten temat nie zostanie tutaj poruszony. 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 » Podstawowa wiedza Zakładam, że część osób czytająca ten poradnik miała już w swoim życiu styczność z jakimś językiem programowania wysokiego poziomu, mimo to zachęcam do zapoznania się z tą częścią poradnika, a także wracania do niej w razie potrzeb. Informacje na temat środowiska oraz zapoznania się z nim będą w oddzielnej części, przeznaczonej tworzeniu aplikacji wizualno-obiektowej. Osobom, które nie wiedzą jednak czym jest język wysokiego poziomu spróbuję wyjaśnić za pomocą skrótu myślowego i odpowiedniego rysunku: W dużym skrócie: istnieją języki wysokiego i niskiego poziomu. Język wysokiego poziomu to ten zrozumiały dla człowieka, mający postać zmiennych, ich nazw, pętli, instrukcji warunkowych itd. Natomiast język niskiego poziomu to ten najbliższy komputerowi, mający postać zer i jedynek, operacji na pamięci itd. Dawno temu ludzie programowali maszyny za pomocą kart perforowanych z wyciętymi dziurami. Było to swoistym językiem niskiego poziomu, dochodziło do błędów i było nieprzystępne dla ludzi. To właśnie było powodem powstania języków wysokiego poziomu oraz kompilatorów/interpreterów tłumaczących je na kod maszynowy. Język C# pozwala nam pracować z poniższymi typami danych byte – przyjmuje wartości od 0 do 255 char – znaki w Unicode bool – prawda / fałsz sbyte – wartości od -128 do 127 short – wartości od -32,765 do 32,767 ushort – wartości od 0 65,535 int – wartości od -2,147,483,648 do 2,147,483,647 uint – wartości od 0 do 4,294,967,295 long – wartości od 18,446,744,073,709,551,615 ulong – wartości od 0 do 18,446,744,073,709,551,615 float – wartości od -3.402823e38 do 3.402823e38 double – wartości od -1.79769313486232e308 do 1.79769313486232e308 decimal – wartości +/- od 1.0 x 10e-28 do 7.9 x 10e28 string – sekwencja znaków Unicode DateTime – format daty i czasu (0:00:00am 1/1/01 do 11:59:59pm 12/31/9999) Oprócz tych typów danych są także typy skalarne enum oraz struct, jednak ich temat nie będzie teraz rozwijany. W wymienionych powyżej typach danych, niektóre posiadają przedrostek u- i oznacza on, że typ jest unsigned czyli nieprzypisany. To z kolei oznacza, że wartości ujemne zostały przeniesione na plus w celu zwiększenia pojemności typu. Środowisko programistyczne W tej części zajmiemy się pobieraniem środowiska oraz jego wstępną konfiguracją. Pobieranie środowiska Środowisko, w którym będziemy programować to Visual Studio Community 2019 i można je pobrać na stronie https://visualstudio.microsoft.com/pl/vs/community/, to środowisko jest darmowe. Wejdź pod podany przed chwilą adres, pobierz instalator i uruchom go na swoim komputerze. Instalacja środowiska jest prosta, więc poruszymy tylko niektóre aspekty z nią związane. Ekran instalatora Visual Studio Installer W tym poradniku potrzebne będzie nam pobranie dodatku Programowanie aplikacji klasycznych dla platformy .NET, znajduje się on w części Komputery i urządzenia przenośne. Instalator powinien sam zaznaczyć wymagane pakiety opcjonalne, jednak dla pewności sprawdź, czy w prawym okienku Szczegóły instalacji są zaznaczone opcje jak na powyższym zdjęciu. W przyszłości oczywiście możesz doinstalować kolejne pakiety przy użyciu programu, który instaluje się razem z Visual Studio (jest to Visual Studio Installer), jednak teraz potrzebne nam będą tylko te domyślnie zaznaczone. Po zaznaczeniu wciskamy przycisk Instaluj znajdujący się w prawym dolnym rogu. Po instalacji może być wymagane zalogowania się do konta Microsoft. Tworzenie projektu Po uruchomieniu Środowiska wybieramy opcję Utwórz nowy projekt. Ekran użytkownika po uruchomieniu środowiska Najpierw skupimy się na aplikacji konsolowej. Znajdź szablon "Aplikacja Konsolowa", upewnij się że wybrana opcja ma tag C# Nadajmy projektowi nazwę np. Hello World. W informacjach dodatkowych na razie nic nie zmieniamy. Wybieramy Utwórz i naszym oczom powinno pokazać się środowisko, w którym będziemy pracować. Domyślnie wygenerowana aplikacja konsolowa będzie miała już napisany kod programu Hello World, skupimy się więc na jego analizie oraz dodatkowych informacjach, które mogą się przydać. using System; namespace Hello_World { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } } Na początek zaczniemy od funkcji Main. Znajdująca się w niej metoda WriteLine może odstraszać początkującego programistę, jednak spokojnie. Pracujemy z aplikacją konsolową, a co za tym idzie, jeśli chcemy wypisać coś w konsoli, musimy odwołać się do metody wypisz linie (WriteLine). Metoda ta należy do klasy konsola (Console). Metody oraz klasy można zrozumieć skrótem myślowym, w tym wypadku będzie to Konsola wypisz linie „Hello World!” class Program (klasa Program) jest klasą wygenerowaną, więc możemy napisać dla niej metodę, do której później będziemy mogli się odwołać, podobnie jak przy użyciu Console.WriteLine(); namespace Hello_World może wyglądać znajomo osobom, które pisały już w C++, gdzie dla ułatwienia stosowały using namespace std; w C# namespace deklaruje zakres (proste wytłumaczenie, czym jest zakres znajdzie się na końcu poradnika) jednak ten temat rozwiniemy za chwilę przy użyciu przykładu. W celu zademonstrowania i ułatwienia zrozumienia wiedzy zdobytej do tej pory, napisałem poniższy kod: using System; namespace Hello_World { class Klasa_HelloWorld { public static void Hello() { Console.WriteLine("Hello World!"); } } } namespace wypisywanie { class Klasa_Wypisywanie { static void Main(string[] args) { Hello_World.Klasa_HelloWorld.Hello(); } } } Aby uruchomić program klikamy przycisk oznaczony zielonym trójkątem Działanie kodu wygląda następująco: Wyjaśnię więc, co dzieje się po uruchomieniu programu. Funkcja Main znajdująca się w klasie Wypisywanie z namespace wypisywanie ma w sobie odwołanie do Metody Hello() klasy o nazwie Klasa_HelloWorld zakresu Hello_World. Sama klasa metoda jest publiczna (public static void). Z kolei metoda Hello() nie należy to klasy, w której jest wywoływana, jednak modyfikator dostępu public pozwala na jej wykorzystanie. Instrukcje wejścia/wyjścia Zanim przejdziemy dalej, trzeba wspomnieć o tym jak wyglądają instrukcje wejścia/wyjścia w C#. namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { String input = Console.ReadLine(); Console.WriteLine(input); } } } Dwa podstawowe polecenia wejścia i wyjścia to Console.ReadLine() oraz znane nam już Console.WriteLine(), w tym wypadku utworzyłem zmienną input, która jest ciągiem znaków (String) i przekazałem programowi, że zawiera w sobie to, co użytkownik wpisze w konsoli. Wykonywanie działań w języku C# Jak pewnie się domyślasz, w języku C# można wykonywać działania arytmetyczne, operatory arytmetyczne. Wyniki ich działań pokazuje poniższy kod. namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { double a = 10; double b = 4; Console.WriteLine(a + b); //dodawanie wynikiem będzie 14 Console.WriteLine(a - b); //odejmowanie wynikiem będzie 6 Console.WriteLine(a * b); //mnożenie wynikiem będzie 40 Console.WriteLine(a / b); //dzielenie wynikiem będzie 2,5 Console.WriteLine(a % b); //reszta z dzielenie wynikiem będzie 2 } } } Dla ułatwienia przekazu posłużyłem się zdefiniowanymi wartościami a oraz b, by w komentarzu (czym jest komentarz, wyjaśnię na końcu poradnika) pokazać przykładowe wyniki działań. W swoim kodzie możesz oczywiście wpisywać te zmienne przy użyciu polecenia Console.ReadLine(). Instrukcje warunkowe Pierwszą instrukcją warunkową, której działaniu się przyjrzymy jest if. Instrukcja warunkowa if przyjmuje jeden argument i wykonuje odpowiedni kod, jeśli argument jest spełniony. Warto także wspomnieć o słowie kluczowym else, które wykonuje inny kod, jeśli warunek nie został spełniony. Z połączenia obu tych poleceń wychodzi else if, który pozwala na doprecyzowanie kolejnego warunku. namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { String input = Console.ReadLine(); if(input == "hello") { Console.WriteLine("Hello World!"); } else if (input == "poradnik") { Console.WriteLine("Witamy w poradniku programowania w C#"); } else { Console.WriteLine("podano błędną wartość"); } } } } Powyższy kod sprawdza, czy użytkownik wpisał w konsoli słowo hello lub poradnik, i wykonuje odpowiednie polecenia. Jeśli zaś użytkownik wpisał coś innego, kod wyświetli komunikat o podaniu błędnej wartości. Drugą instrukcją warunkową, którą poznamy jest switch. Słowa kluczowe switch oraz case działają na podobnej (lecz nie identycznej) zasadzie co if i else. Różnica polega na wygodzie pisania oraz czasie wykonywania instrukcji (nie będzie tu poruszana szczegółowa analiza różnic w działaniu obu instrukcji). namespace Hello_World { class Program { static void Main(string[] args) { String input = Console.WriteLine(); switch (input) { case "hello": Console.WriteLine("Hello World!"); break; case "poradnik": Console.WriteLine("Poradnik programowania w C#"); break; default: Console.WriteLine("podano błędną wartość"); break; } } } } Powyższy kod działa identycznie jak ten z wykorzystaniem if oraz else. Słowo kluczowe break przerywa działanie funkcji oraz pętli. Pętle w języku C# Teraz spróbujmy wypisać w konsoli napis „Hello World!” 10 razy, można oczywiście w tym celu skopiować fragment kodu odpowiedzialny za to 10 razy. Jednak co w wypadku, gdy stwierdzimy, że 10 razy to za mało? Wtedy kopiowanie kodu nie ma sensu. Do uproszczenia tego procesu posłużą nam pętle. Osobom, które są już zapoznane z pętlami, ten fragment nie wniesie nic nowego, można więc go pominąć. Osobom, które chcą sobie powtórzyć lub nie wiedzą nic o pętlach warto powiedzieć że istnieją cztery różne pętle: for foreach while do while Pętla for namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { for(int i = 0; i<10; i++) { Console.WriteLine( i + " Hello World!"); } } } } Pętla for przyjmuję trzy argumenty, na obrazku są to kolejno: int i = 0 - ten argument wykonuje się tylko raz na początku wykonywania pętli; jest to zmienna, z którą pętla rozpoczyna swoje działanie; i<10 - to warunek pętli, w tym wypadku pętla będzie się wykonywała tak długo, aż wartość zdefiniowanej przed chwilą zmiennej będzie mniejsza od 10; i++ - to inkrementacja zmiennej czyli podniesienie jej wartości o 1 z każdym wykonaniem pętli. Pętla foreach namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { int[] tablica = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; foreach(int i in tablica) { Console.WriteLine( i + " Hello World!"); } } } } Pętla foreach wykonuje się dla każdego elementu tablicy. W tym wypadku zdefiniowałem tablicę liczb całkowitych int[], jako tablicę zawierającą 10 elementów. Pętla foreach przyjmuje dwa argumenty w tym wypadku int i inny, będący dowolnym elementem tablicy oraz in tablica, będący wskazaniem tablicy. While i do while namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { int i = 0; while (i < 10) { Console.Write(i + " Hello World!"); i++; } } } } Pętla while przyjmuje jeden warunek i będzie się wykonywać do momentu, kiedy warunek zostanie spełniony. namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { int i = 0; do { Console.Write(i + " Hello World!"); i++; }while (i < 10); } } } Pętla do while jest podobna do pętlu while, z tym że wykona się przynajmniej raz, niezależnie od prawdziwości warunku. namespace wypisywanie { class Wypisywanie { static void Main(string[] args) { int i = 11; do { Console.Write(i + " Hello World!"); i++; }while (i < 10); } } Na powyższym obrazku widać, że do while wykonało się raz, mimo iż warunek nie jest spełniony. Ćwiczenie Przy użyciu zdobytej wiedzy spróbuj napisać prosty kalkulator obsługujący działania na dwóch liczbach. Wskazówka do Ćwiczenia W razie problemów z pisaniem postaraj się wyobrazić sobie albo narysować schemat działania twojego programu. Czy ma najpierw wypisać komunikat i oczekiwać na wybranie danego działania? Ma poprosić o podanie liczb na których ma operować, czy w inny sposób ma poprowadzić użytkownika do wykonania działania? Pierwsza aplikacja wizualno-obiektowa W tej części poradnika zajmiemy się pisaniem twojej pierwszej aplikacji z interfejsem użytkownika. Na początku skoncentrujemy się na potrzebnych nam elementach środowiska i podstawowych kontrolkach, które będziemy wykorzystywać, potem zajmiemy się pisaniem prostego kalkulatora. Tworzenie nowego projektu W tej części poradnika dalej będziemy pracować ze środowiskiem Visual Studio Community. W poprzedniej części zainstalowaliśmy wszystkie potrzebne nam elementy tego środowiska, które będą nam potrzebne podczas tworzenia projektu. Po uruchomieniu Visual Studio wybieramy opcję „Utwórz nowy projekt”. Następnie z listy wybieramy „Aplikacja Windows Forms”, jeśli masz problem ze znalezieniem odpowiedniej opcji, możesz wpisać ją w wyszukiwarce szablonów. Klikamy „Dalej” i nadajemy naszemu projektowi odpowiednią nazwę. Po nadaniu nazwy i kliknięciu „Dalej” pojawi się okienko proszące nas o wybranie wersji .NET, z którą chcemy pracować. W tym poradniku pracujemy z .NET Core 3.1. Po upewnieniu się, że wybraliśmy odpowiednią opcję, klikamy „Utwórz”. Po utworzeniu projektu pokaże nam się takie okno. Znajduje się na nim puste okienko aplikacji, którą teraz tworzymy. Możemy uruchomić projekt w taki sam sposób jak robiliśmy to z aplikacją konsolową. Klikamy nazwę naszego projektu, która jest obok zielonej strzałki i po chwili pojawi nam się takie same okienko jak w środowisku. Możemy to okienko zamknąć w taki sam sposób jak inne aplikacje komputerowe - krzyżykiem znajdującym się w prawym górnym rogu. Jednak lepiej zamykać projekt z poziomu środowiska, ponieważ przy bardziej rozbudowanych projektach zamknięcie okienka nie musi oznaczać końca działania aplikacji. Znajdź czerwony kwadrat w w górnej części środowiska, będzie w tym samym miejscu, co przycisk odpowiedzialny za uruchomienie aplikacji. Czerwony kwadrat zatrzyma działanie aplikacji, opcję po lewej i prawej stronie czerwonego kwadratu odpowiadają kolejno za wstrzymanie działania programu oraz za ponowne uruchomienie. Potrzebne informacje o środowisku Teraz skupimy się na elementach środowiska, których będziemy potrzebować. Po lewej stronie będzie zwinięty pasek o nazwie Przybornik. Na górze listy Przybornika znajduje się wyszukiwarka, pozwala w szybki sposób znaleźć potrzebne nam elementy interfejsu. Skupimy się teraz na kontrolce „Button” oraz „TextBox”. Na powyższych zdjęciach znajdują się potrzebne nam kontrolki znalezione przy użyciu wyszukiwarki. Po znalezieniu odpowiednich kontrolek przeciągamy je na obszar roboczy. W związku z tym, że w poradniku zajmujemy się pisaniem aplikacji „Kalkulator”, potrzebne nam będzie 16 przycisków (10 numerycznych i 6 funkcyjnych) oraz jeden TextBox do wyświetlania wyniku. Dla ułatwienia sobie pracy, po przeciągnięciu pierwszego przycisku ustaw jego wymiary na takie, które chcesz mieć w projekcie końcowym. Późniejsza zmiana rozmiarów wielu elementów może być czasochłonna i nieprzyjemna. Aby zmienić rozmiar przycisku kliknij go i rozciągnij używając charakterystycznych punktów, które się pojawią. Gdy już nadasz kontrolce odpowiednie wymiary, możesz ją skopiować skrótem klawiszowym ctrl + c i wkleić kolejne przyciski o tych samych wymiarach. Wymiary kontrolki TextBox zmienia się w identyczny sposób, jednak jeśli chcesz ustawić jej wysokość, musisz wybrać opcję MultiLine klikając kolejno trójkącik nad wybranym TextBox’em, a następnie opcję MultiLine. Po dodaniu wszystkich potrzebnych kontrolek, moja propozycja wyglądu aplikacji prezentuje się następująco: Po napisaniu kodu aplikacja byłaby funkcjonalna, jednak jej wygląd oraz podpisy nie są przyjazne użytkownikowi, dlatego aby to zmienić, możemy dostosować wszystkie elementy interfejsu. Po wybraniu dowolnego elementu w prawym dolnym rogu środowiska pojawią nam się właściwości, które dla tego elementu możemy zmienić. Zachęcam do samodzielnego zapoznania się ze wszystkimi elementami, dlatego teraz skupmy się tylko na podstawowych, które będą nam potrzebne. Na początek zmienimy nazwę przycisku oraz tekst, jaki jest w nim wyświetlany. By zmienić nazwę w segmencie Design zmienimy właściwość (Name) na liczba_7 oraz w segmencie Wygląd właściwość text na 7. Możemy tez dostosować wygląd, zmieniając właściwości z segmentu Widok, takie jak BackColor (kolor tła), BorderSize z listy FlatApperance (grubość ramki wokół przycisku) oraz FlatStyle (ogólny wygląd przycisku. Zachęcam do samodzielnej zabawy tymi ustawieniami i zobaczenia jak wpływają na wygląd kontrolki. W ten sam sposób możesz również zmienić ustawienia okienka klikając na nie i zmieniając np. kolor tła albo rodzaj ramki. Teraz zmienimy ustawienia TextBox’a. Zmień właściwość Text na przykładowy ciąg liczb, TextAlign na Right oraz rozmiar czcionki właściwością Font, dostosuj czcionkę do rozmiaru kontrolki (pamiętaj, żeby usunąć wybrany przez siebie ciąg liczb po dobraniu satysfakcjonujących ustawień czcionki) . Zaproponowany przeze mnie wygląd aplikacji jest teraz taki: Teraz dodajmy naszemu kalkulatorowi kod, by zapewnić mu pewną funkcjonalność. Po podwójnym kliknięciu na kontrolkę, środowisko wygeneruje nam fragment kodu odpowiedzialny za wykonanie poleceń mających miejsce po kliknięciu przycisku w programie. Kliknij dwukrotnie na wszystkie przyciski. using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WinFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void liczba_1_Click(object sender, EventArgs e) { } } } Na powyższym obrazku widać wygenerowany kod. Zauważ, że funkcja void przyjmuje nazwy takie jak nazwy kontrolek, które przed chwilą stworzyłeś. W programie potrzebne będą nam trzy zmienne globalne: jedna przechowująca znak działania, jedna przechowująca wynik i jedna zmienna kontrolna. Double wynik = 0; //wynik działania String operacja = ""; //zmienna przechowująca znak działania bool czyPrzeprowadzaOperacje = false; //zmienna kontrolna do sprawdzania czy można przeprowadzić operacje Żeby móc wykonywać działania, potrzebne będą nam liczby. W tym celu dla każdej kontrolki w kodzie musimy przypisać wartość liczbową. private void liczba_7_Click(object sender, EventArgs e) { czyPrzeprowadzaOperacje = false; Button przycisk = (Button)sender; textBox1.Text = textBox1.Text + przycisk.Text; } Powyższy kod zadziała dla każdej kontrolki, można by go użyć nawet bez wklejania do każdej funkcji _Click, jednak dla ułatwienia skupimy się na prostszej wersji. Zanim wkleisz kod do wszystkich funkcji, wyjaśnię ci, w jaki sposób on działa. Na samym początku zmienna czyPrzeprowadzaOperacje jest ustawiona na fałsz. Użyjemy tej zmiennej, żeby upewnić się, że kod będzie działał dokładnie tak jak tego chcemy. Kolejna linijka “Button button = (Button)sender” na niewtajemniczonych robi wrażenie, jednak jest to bardzo proste. Ta linijka tworzy nowy obiekt typu Button o nazwie przycisk, zaś (Button)sender w uproszczeniu tworzy nadawcę. Cała funkcja należy do przycisku 7, który jest nadawcą, więc ta linijka sprawia, że nasz przycisk może „odebrać” informacje sam od siebie. Ostatnia linijka jest łatwa do zrozumienia. Chcemy przypisać tekst dla pola tekstowego i, aby to zrobić, wywołujemy Text z textBox1, a następnie przypisujemy mu tekst zawarty w przycisku (w tym wypadku jest to 7). Teraz możesz skopiować ten kod dla innych liczb. Nie martw się, nic złego się nie stanie 😊. Jak już skopiujesz ten kod w odpowiednie miejsca, uruchom program i upewnij się, że wszystkie liczby wyświetlają się poprawnie. Następnie potrzebny nam będzie kod sprawdzający, jaka operacja została wybrana. private void dzielenie_Click(object sender, EventArgs e) { Button button = (Button)sender; if (wynik != 0) { rownosc.PerformClick(); operacja = button.Text; czyPrzeprowadzaOperacje = true; } else { operacja = button.Text; wynik = Double.Parse(textBox1.Text); textBox1.Text = " "; czyPrzeprowadzaOperacje = true; } } Pozwoli na to powyższy kod. Podobnie jak poprzednio, ten sam kod możesz zastosować dla każdego przycisku osobno. Zanim jednak go powklejasz tam gdzie trzeba - trochę wyjaśnień. Zajmę się rzeczami, które wykorzystujemy po raz pierwszy. Linijka rownosc.PerformClick() ta linijka odwołuje się do kontrolni o nazwie rownosc, a PerformClick() to metoda, która wykona kod w przycisku równości bez jego kliknięcia. Stanie się tak, tylko jeśli wynik będzie różny od zera. Linijka wynik = Double.Parse(textBox1.Text) przypisze zmiennej wynik wartość z textbox1 nie możemy tego jednak zrobić bezpośrednio, w tym celu użyjemy metody Parse by zmienić string zawierający ciąg liczbowy na typ Double. Niezależnie od spełnienia instrukcji warunkowej if, zmieniamy zmienną czyPrzeprowadzaOperację naprawdę. Teraz potrzebna nam funkcja, która wykonuje obliczenia. private void rownosc_Click(object sender, EventArgs e) { switch (operacja) { case "+": textBox1.Text = (wynik + Double.Parse(textBox1.Text)).ToString(); break; case "-": textBox1.Text = (wynik - Double.Parse(textBox1.Text)).ToString(); break; case "*": textBox1.Text = (wynik * Double.Parse(textBox1.Text)).ToString(); break; case "/": textBox1.Text = (wynik / Double.Parse(textBox1.Text)).ToString(); break; default: break; } wynik = Double.Parse(textBox1.Text); } Powyższy kod wykorzystuje instrukcję switch case w celu sprawdzenia jaką operację chcemy wykonać. Jeśli spełnimy jeden z warunków, wtedy wykona się odpowiednie działanie. Skupimy się teraz na tym, co nowe. W każdej instrukcji case możesz zauważyć metodę ToString(), w tym wypadku używamy jej, żeby wyświetlić wynik obliczeń w textBox’ie. Zauważ, że wykonujemy obliczenia na typach liczbowych, których nie możemy bezpośrednio wyświetlić, właśnie dlatego używamy metody ToString(). Na koniec zostało nam tylko zajęcie się przyciskiem CE. Dla zapewnienia jego funkcjonalności wykorzystamy poniższy kod. private void CE_Click(object sender, EventArgs e) { textBox1.Text = " "; wynik = 0; } Dodatkowe objaśnienia pojęć, które wystąpiły w poradniku Zakres – dla ułatwienia przekazu posłużymy się przykładem static void Main(string[] args) { //to jest zmienna x z tego zakresu int x = 3; } void InnaFunkcja() { //to jest zmienna x z tego zakresu String x = "Hello"; } W powyższym fragmencie kodu znajdują się dwie funkcje, każda z tych funkcji jest ograniczona przez „agrafki” (otwarcia „{„ oraz zamknięcia „}”) te agrafki są zakresem tej funkcji, co sprawia, że wszystko co się w nim znajduje, jest widziane tylko w tym zakresie i nie jest dostępne poza nim. Komentarz – w języki C# możemy „zakomentować” fragmenty kodu, oznacza to, że kompilator nie będzie brał tego fragmentu pod uwagę przy kompilowaniu naszego kodu. Istnieją dwa rodzaje komentarza: jednoliniowy oraz wieloliniowy, pokazane na poniższym obrazku: static void Main(string[] args) { //to jest komentarz jednoliniowy /** to jest komentarz wieloliniowy */ } Praca domowa Chciałbym, abyś po ukończeniu tego poradnika samodzielnie zapoznał się z właściwościami kontrolek i zmieniał ich wygląd. Postaraj się także dodać dodatkową funkcjonalność w postaci obsługi liczb dziesiętnych, potęg oraz pierwiastków. Powodzenia 😊.
  14. Ten artykuł jest częścią serii "Arduino dla początkujących: instrukcje sterujące w C/C++" #1 - if, bloki #2 - while i coś jeszcze #3 - for, break, switch W poprzednich częściach artykułu poznaliśmy dwie podstawowe instrukcje sterujące: if i while. Teraz kolej na coś bardziej rozbudowanego. Przyjrzyjmy się jak wygląda Pętla z efektami, czyli instrukcja "for" 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 » Zapis tej instrukcji jest następujący: for (START; WARUNEK; KROK) INSTRUKCJA; Wykonuje się ona w sposób następujący: Najpierw wykonywana jest instrukcja określona jako START. Następnie sprawdzany jest WARUNEK. Jeśli nie jest spełniony następuje wyjście z pętli. Jeśli jest - wykonywane są kolejno INSTRUKCJA i KROK, a następnie następuje powrót do sprawdzenia warunku. Wszystkie trzy wyrażenia (START, WARUNEK i KROK) mogą być pominięte, przy czym pominięcie WARUNEK jest równoznaczne z "prawda". Spróbujmy rozpisać to za pomocą goto: START; etykieta: if (WARUNEK) { INSTRUKCJA; KROK; goto etykieta; } Możemy to zobrazować również za pomocą diagramu: Możemy sobie napisać prosty program, pokazujący działanie instrukcji for: #define BUTTON_PIN 2 void setup() { Serial.begin(9600); pinMode(BUTTON_PIN, INPUT_PULLUP); // ustawiamy pin jako wejście z podciągnięciem do Vcc } void loop() { if (digitalRead(BUTTON_PIN) == LOW) { int i; for ( i=0; i<10; i += 1) { Serial.println(i); } } } Jako ćwiczenie możemy przewidzieć, jak będzie wyglądać działanie programu a potem, po uruchomieniu sprawdzić, czy zachowuje się zgodnie z naszymi oczekiwaniami. Obie instrukcje (while i for) mają pewną "zdegenerowaną" formę, czyli w skrócie "kręć się w kółko". Będą to odpowiednio: while(1) INSTRUKCJA; // nieskończona pętla while for(;;) INSTRUKCJA; // nieskończona pętla for Jak jednak wyjść z takiej nieskończonej pętli? Oczywiście, zawsze się znajdzie Wyjście awaryjne, czyli instrukcja "break" Wykonanie instrukcji break powoduje natychmiastowe przerwanie pętli i wznowienie wykonania od następnej instrukcji za pętlą. Czyli jeśli w programie mamy konstrukcję: while (WARUNEK) { INSTRUKCJE; if (przerywamy) break; INSTRUKCJE; } to możemy ją sobie wyobrazić jako: etykieta: if (WARUNEK) { INSTRUKCJE; if (przerywamy) goto etykieta2; INSTRUKCJE; goto etykieta; } } etykieta2: Za pomocą instrukcji break możemy oczywiście przerwać nie tylko nieskończone pętle ale również takie, które mają swój warunek kończący. Instrukcja ta może oczywiście zawierać się w zagnieżdżonym bloku - jej wykonanie spowoduje wyjście z bloku i zniszczenie należących do niego zmiennych, tak jakby było to normalne dojście programu do końca bloku. Tak więc pętle mamy już opanowane - pozostała nam jeszcze jedna instrukcja, a mianowicie if na sterydach, czyli instrukcja "switch" Za pomocą tej instrukcji możemy wybrać jedną z kilku możliwości, nie jesteśmy ograniczeni do dwóch. Przykładowy zapis będzie wyglądać w ten sposób: switch(WYRAŻENIE) { case STAŁA1: INSTRUKCJE1; case STAŁA2: INSTRUKCJE2; case STAŁA3: INSTRUKCJE3; default: INSTRUKCJE4; } Jak widać, nie jesteśmy tu ograniczeni do pojedynczej instrukcji jak poprzednio; INSTRUKCJE to po prostu ciąg dowolnej ilości instrukcji, z zerem włącznie, np. tak: switch(x) { case 1: case 2: // robimy coś case 3: // robimy coś innego default: // robimy coś jeszcze innego } Oczywiście i tu mamy pewne ograniczenia: WYRAŻENIE powinno zwracać wartość typu int, a argumentem case mpożę być wyłącznie stała tego typu. Tak więc konstrukcje typu: int n,x; n= cośtam(); x=cośtam(); switch(x) { case n: albo (widywana w próbach rozwiązań zadań z kursu): String s; s=cośtam(); switch(s) { case "zielona": będą nieprawidłowe; pierwsza ponieważ argumentem case nie jest stała, drugie ponieważ zarówno wyrażenie, jak i stała napisowa użyta w case nie jest typu int. I tu bardzo ważna uwaga. Dojście do kolejnej etykiety case nie przerywa toku programu; zostają wykonane następne instrukcje. Najlepiej wyjaśni to rozpisanie za pomocą goto: R=WYRAŻENIE; // gdzie R to wewnętrzna, niejawna zmienna instrukcji switch if (R == STAŁA1) goto etykieta1; // case STAŁA1 if (R == STAŁA2) goto etykieta2; // case STAŁA2 if (R == STAŁA3) goto etykieta3; // case STAŁA3 goto etykieta4; //default etykieta1: INSTRUKCJE1; etykieta2: INSTRUKCJE2; etykieta3: INSTRUKCJE3; etykieta4: INSTRUKCJE4; lub za pomocą diagramu: Klauzula default powinna zawsze wystąpić w instrukcji switch, jednak jej brak nie jest błędem. W takiej sytuacji kompilator uzna, że domyślnym zachowaniem w przypadku, gdy wartość wyrażenia nie jest równa żadnej z wartości w klauzulach case jest "nic nie rób" - tak jakby przed zamykającym nawiasem dopisywał sobie dodatkową, brakującą etykietę default. Zostało nam jedno: dojście do kolejnego case nie powoduje przerwania instrukcji switch, ale w większości przypadków jest nam to potrzebne. Tak jak w instrukcji if mamy dwie niezależne gałęzie, tak tu chcielibyśmy mieć ich więcej, równie niezależnych. Do tego celu służy nam znana już instrukcja break. Jej wykonanie powoduje natychmiastowe zakończenie wykonania instrukcji switch. Przykładowy kod może wyglądać tak: switch(WYRAŻENIE) { case STAŁA1: INSTRUKCJE1; break; case STAŁA2: INSTRUKCJE2; break; case STAŁA3: INSTRUKCJE3; break; default: INSTRUKCJE4; } W ten sposób zależnie od wartości wyrażenia zostanie wykonany jeden, konkretny ciąg instrukcji. Co więcej, kompilator (jeśli włączymy wszystkie ostrzeżenia) będzie spodziewał się, że instrukcja break jest w danym miejscu (czyli przed case/default) potrzebna i jej brak uzna za nasze przeoczenie. Czyli fragment typu: case 1: x = x+1; case 2: y = y-1; uzna za co najmniej podejrzany. Jak mu wyjaśnić, że o to nam chodziło i ma się nie wtrącać? Do tego służy specjalna postać komentarza. Jeśli przekształcimy nasz kod w sposób następujący: case 1: x = x+1; // pass thru case 2: y = y+1; kompilator uzna, że komentarz "pass thru" oznacza, że tak miało być i tu nie trzeba żadnego break. I to już wszystko - możemy zabrać się do pisania programów. I na koniec przypomnienie: co prawda instrukcja goto występuje w językach C i C++, ale umówmy się: nie będziemy jej używać w rzeczywistych programach. Ale... jeśli ktoś zetknął się z sytuacją, której nie można rozwiązać tylko za pomocą omawianych instrukcji (bez goto) albo przynajmniej jest to utrudnione - niech da znać w komentarzach!
  15. SZTUCZNA INTELIGENCJA W SŁUŻBIE MOTORYZACJI Cz.1. (Trochę przydługi) Wstęp. Sztuczna inteligencja to ostatnio temat na topie. Wydaje się, że na dobre zostawiliśmy za sobą tzw. „wieki ciemne” , kiedy to, zaraz po wynalezieniu i udowodnieniu działania sieci neuronowych w końcówce lat ‘50 ubiegłego wieku, cała społeczność naukowców zniechęcona „fałszywymi obietnicami” uznała, że sieci, owszem działają, ale nie można ich nauczyć niczego pożytecznego i zgodnie zapomnieli o nich na paręnaście dobrych lat[1]. Dzisiaj nikogo nie trzeba przekonywać, że sieci działają, do tego w wielu przypadkach działają skuteczniej niż człowiek oraz inne „konwencjonalne” metody. Każdy z nas, chcąc nie chcąc, odczuwa działanie sztucznej inteligencji jeśli jest zalogowany na jakimkolwiek większym portalu społecznościowym. Sam niejednokrotnie byłem pełen podziwu dla algorytmów polecających w serwisach Youtube, czy Facebook. Akcjonariusze serwisów społecznościowych i instagramowe modelki pewnie się ze mną nie zgodzą, ale zawsze uważałem, że potencjał sztucznej inteligencji jest marnowany na algorytmy polecające albo upiększające zdjęcia. Jako inżynier transportu naturalnie widzę miejsce dla sieci neuronowych w transporcie motoryzacyjnym. Rynek pokazuje, że na pewno nie jestem w tym, ani jedyny, ani nawet pierwszy. Amerykański producent samochodów elektrycznych Tesla zdaje się być o krok od wypuszczenia na rynek całkowicie autonomicznego samochodu (Full Self-Driving) opartego w znacznej mierze na sieciach neuronowych, jednak również pozostali producenci aut z powodzeniem stosują rozwiązania sztucznej inteligencji w celu poprawienia bezpieczeństwa i komfortu jazdy (system monitorowania koncentracji kierowcy, system rozpoznawania znaków drogowych)[2]. 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 » Samochody autonomiczne to temat szalenie interesujący zarówno z punktu widzenia technicznego (duh!) jak i prawnego (w razie wypadku z winy pojazdu autonomicznego, kto powinien być pociągnięty do odpowiedzialności? Kierowca czy producent pojazdu?). Rozważania nad tymi zagadnieniami zachęciły mnie w końcu do podjęcia próby stworzenia własnego samochodu autonomicznego w małej skali (na razie 😉 ). Pojazdy autonomiczne do tego, aby odnaleźć siebie i innych uczestników ruchu drogowego w przestrzeni, wykorzystują różne technologie, a różnice między producentami są widoczne gołym okiem (sieć autonomicznych taksówek Waymo działających na terenie San Francisco wykorzystuje do nawigacji charakterystyczne, umiejscowione na dachach pojazdów LIDARy oraz bardzo dokładną mapę 3D terenu, tymczasem Tesla woli korzystać ze zwykłych kamer 2D i wspierać się sieciami neuronowymi). Cz.2. Założenia projektu, czyli co ja robię?! W czasie kiedy zaczynałem pracę nad tym projektem byłem już dobrze zaznajomiony z technikami analizy obrazu, biblioteką OpenCV i miałem jako-takie (podparte praktyką) pojęcie o sieciach neuronowych. Ponieważ byłem w tym czasie w trakcie lektury „Superinteligencji” Nicka Bostroma i moje myśli nieustannie krążyły w ogół sztucznej inteligencji, zacząłem się zastanawiać, czy to możliwe, żeby mój pojazd potrafił się poruszać po drodze wykorzystując TYLKO I WYŁĄCZNIE sieci neuronowe. Postanowiłem, że tworząc pojazd będę próbował używać sieci neuronowych do realizacji wszystkich założeń projektu, posiłkując się klasyczną analizą obrazu tylko w ostateczności, w sytuacji kiedy moje sieci polegną. Poniżej przedstawiam założenia projektu: pojazd do poruszania się ma wykorzystywać tylko kamery i sieci neuronowe, żadne inne sensory nie są dozwolone, pojazd ma poruszać się po narysowanej na ziemi 2-pasmowej drodze, pojazd na respektować zasadę ruchu prawostronnego, zjazd na przeciwległy pas uznawany jest jako błąd, pojazd ma stosować się do znaków drogowych i sygnalizacji świetlnej, pojazd ma mieć możliwość rozpoznawania pieszych, I na koniec zadanie z gwiazdką: pojazd ma posiadać możliwość przejazdu przez skrzyżowanie w dowolnym kierunku. W tym miejscu chciałbym dodać również, że moim zamiarem było osiągnięcie tego, nie nadwyrężając znacznie mojego budżetu. Do listy założeń wypada zatem dodać: osiągnąć powyższe minimalizując wydatki. Cz.3. Najmniej znaczy ZERO? Od początku wiedziałem, że bazą do projektu będzie robot, którego używałem zaczynając swoją przygodę z programowaniem Arduino, znany zapewne użytkownikom robot z kursu budowy robotów Forbot’a. Pozostał wybór kamer i komputera. Wahałem się między Jetsonem Nano, a Raspberry Pi, jednak urządzenia te nie należą do najtańszych, a trzeba też brać pod uwagę koszt kamery. Do głowy przyszła mi następująca myśli: "najmniej PLN to chyba będzie zero, prawda?". A gdyby tak wykorzystać stacjonarny komputer jako jednostkę obliczeniową i w jakiś sposób przesyłać obraz z kamery na pojeździe do komputera, a następnie odsyłać informacje o sterowaniu do jednostki wykonawczej, czyt. Arduino? A gdyby tak wykorzystać do tego smartfona? Pomysł spodobał mi się ze względu na to, że w razie porażki, nie wydałbym na projekt ani złotówki. Postanowiłem spróbować, mając z tyłu głowy, że w każdej chwili mogę zmienić podejście i dokonać zakupu mini komputera pokładowego. Pojazd użyty do projektu. Cz.4. Kontekst. Celem niniejszego artykułu (czy to nie powinno być w pierwszym akapicie?) jest ukazanie potęgi sieci neuronowych. Nie ma tu miejsca na szczegółowe opisywanie całego projektu. Niemniej jednak czytelnikom należy się wyjaśnienie, w jaki sposób doprowadziłem do tego, że mój pojazd jeździ i w jaki sposób komunikuje się z komputerem. Pozwolę sobie dokonać lakonicznego przedstawienia mojego systemu. Z chęcią dokładniej opiszę poszczególne elementy, jeśli artykuł będzie cieszył się dużym zainteresowaniem 🙂. Robot, którego używałem posiada 2 silniki napędzające niezależnie 2 przednie koła oraz jedno koło nienapędzane (typu kółko od wózka w super-markecie). Na pokładzie znajduje się telefon połączony kablem OTG-USB z Arduino. Pojazd nie ma osi skrętnej, mamy tu do czynienia ze sterowaniem typu "czołg" - jeśli chcemy wykonać skręt to jedno koło musi się kręcić wolniej od drugiego. Zainteresowanych budową robota odsyłam do kursu, jest bezpłatny. Teraz pora na tor sygnałowy. Na telefonie zainstalowana jest aplikacja do przesyłania obrazu typu ipwebcam, nazywa się DroidCam. Obraz jest przesyłany prosto z aplikacji do komputera za pośrednictwem sieci Wi-Fi (lokalnie). Na komputerze skrypt Pythona dokonuje analizy obrazu (każda klatka zostaje przepuszczona przez sieci, o czym opowiem w następnym rozdziale). Wyniki sieci są interpretowane i skrypt odsyła informację o sterowaniu na telefon, tym razem do specjalnej aplikacji w Javie (tą niestety musiałem napisać już sam) i dalej kablem USB do Arduino. Mikrokontroler podaje informacje na koła i jazda! Schemat toru sygnałowego. Cz.5. Sieć sieci nie równa. W końcu dochodzimy do sedna. Kiedy już miałem potrzebne narzędzia przyszła pora na tworzenie sieci i trening. Jest to etap nad którym spędziłem najwięcej czasu, eksperymentując z różnymi modelami sieci, optymizerami, rozważając sens zastosowania wstecznej propagacji błędów i tak dalej. W następnych akapitach chciałbym przedstawić moje osobiste odkrycie roku jakim jest biblioteka FastAi zbudowana na Pytorchu. Dla niezorientowanych spieszę z wyjaśnieniami. W świecie sieci neuronowych 90% użytkowników jako języka programowania używa Python’a. Dlaczego? Czy to ze względu na czytelność kodu, łatwość w programowaniu, intuicyjność? Czy może dlatego, że (całkiem przypadkiem) dwie największe biblioteki do uczenia maszynowego(TensorFlow i Pytorch) zostały napisane dla Pythona? W tym momencie ciężko powiedzieć, co było pierwsze, jajko czy kura? Jedno wiadomo na pewno, niska prędkość kompilacji i wykonywania kodu nie ma większego znaczenia w procesie trenowania sieci. Gorzej oczywiście z pracą w czasie rzeczywistym, ale należy pamiętać, że gotowe modele można konwertować i zaprzęgać do pracy w wielu innych językach. Rozpoczynając przygodę z sieciami neuronowymi zacząłem pracę z biblioteką TensorFlow (w tamtym czasie właśnie dla tej biblioteki w internecie było najwięcej informacji i tutoriali) i myślę, że wyrażę opinię wielu mówiąc, że było to doświadczenie nieprzyjemne. Jako, że o nieprzyjemnych rzeczach najlepiej nie mówić, wraz z nowym projektem zdecydowałem się na zmianę biblioteki na Pytorch (do zmiany nakłonił mnie również film na Youtubie w, którym szef ekipy od jazdy autonomicznej Tesli chwali się, że właśnie tej biblioteki używają). Odnośnie TensorFlow powiem tylko, że problemy jakie napotkałem próbując zainstalować tą bibliotekę na mojej maszynie powracają w koszmarach po dziś dzień. Ale co z tym FastAi? FastAi jest biblioteką zbudowaną na Pytorchu. Ułatwia korzystanie z Pytorch, dzięki niemu tworzenie i trenowanie sieci neuronowych staje się dziecinną igraszką. Każdemu, kto chce zacząć przygodę z sieciami neuronowymi, ale nie ma śmiałości polecam FastAi i serię tutoriali na Youtube współtwórcy tej biblioteki - Jeremy’ego Howarda. Dodatkową zaletą FastAi jest jej poziomowość. Można trenować naprawdę dobre modele, odpowiednio jedynie przygotowując dane i korzystając z gotowych ustawień, jednak wszystkie funkcje Pytorch są stale pod ręką. Jeszcze jedna pozytywna rzecz wynikła dla mnie z tego projektu. FastAi dosłownie wymusza pracowanie w Jupyter Notebooks, który zawsze z jakiegoś powodu mnie odstraszał. Teraz widzę, że jego używanie bywa uzasadnione, każdemu kto nie miał do czynienia z jupyterem polecam spróbowanie. Jest to świetne narzędzie dla eksperymentatorów (chwała wykonywaniu pojedynczych komórek kodu!). Minusy? FastAi działa tylko na Linuxie, ale kto jest zainteresowany sieciami z pewnością od dawna już korzysta z tego systemu operacyjnego. Cz.6. Sieć czy może sieci? Niemal od początku pracy miałem w głowie koncepcję na to w jaki sposób wykorzystać sieci, żeby sterować robotem. W realnej sytuacji (prawdziwy pojazd na prawdziwej drodze jako prawdziwy uczestnik prawdziwego ruchu drogowego) obraz z kamery/wielu kamer jest zapisywany razem z korespondującymi danymi o stanie pojazdu w tym o WIELKIEJ TRÓJCY do której należą: kąt skrętu kierownicą, stopień wciśnięcia pedału gazu, stopień wciśnięcia pedału hamulca. Potem wystarczy stworzyć model, który jako daną wejściową dostaje obraz z kamery, a wypluwa te trzy wartości. Proste prawda? Szkopuł w tym, że aby porządnie wytrenować sieć neuronową potrzeba dużej ilości danych. Sieci dosłownie żywią się danymi. Bez nich dają marne predykcje, choć są sposoby na poprawienie wyników sieci bez zwiększania ilości danych, ale o tym później. W realnej sytuacji (prawdziwy pojazd na prawdziwej drodze…) w celu zebrania danych do trenowania sieci wystarczy zapisywać normalne zachowania kierowców, jednak dla mnie oznaczało by to długie i mozolne jeżdżenie moim robotem po przygotowanym torze, a ciężko jest określić na początku, ile danych tak naprawdę potrzebuje sieć (chociaż z dużą pewnością można stwierdzić, że czym więcej tym lepiej 🙂 ). W dodatku wyniki sieci zależą oczywiście od jakości wprowadzonych danych, zatem musiałbym jeździć po torze bardzo precyzyjnie. Jeśli ktoś kiedykolwiek bawił się samochodem zdalnie sterowanym to wie, że precyzyjna jazda nie jest wcale taka łatwa. Musiał zatem istnieć jakiś sposób na zautomatyzowanie procesu zbierania danych. Ah, gdyby istniał taki samochód autonomiczny, który by zbierał te informacje… W końcu wpadłem na pomysł. Wykorzystałem odrobinę wiedzy z zakresu cyfrowej analizy obrazu, bibliotekę OpenCV i jedną dodatkową kamerę, którą umieściłem nad torem jazdy, tuż przy suficie. Na robocie przymocowałem zielony znacznik, który później wyodrębniłem na komputerze z tła. W Paint'cie narysowałem drogę po której ma się poruszać pojazd, i połączyłem te dwa obrazy. Miałem teraz współrzędne zielonego znacznika i niebieski tor. Z pomocą funkcji pointPolygonTest z OpenCV mogłem wyznaczyć odległość znacznika od najbliższego punktu na torze. Jeden warunek: tor musi stanowić zamknięty kształt. Mi to nawet było na rękę, dzięki temu pojazd mógł jeździć w kółko i zbierać dane. Teraz tylko prosty regulator PID i gotowe. Dla ułatwienia postanowiłem, że robot będzie jeździł ze stałą prędkością, więc zbierane dane miały postać: obraz z kamery w danym momencie oraz odchylenie znacznika od toru. Prosta sieć klasyfikująca. Wpada obraz z kamery (na GIF-ie poniżej dolny lewy róg), a wypada „odchylenie” od toru jazdy. Stworzyłem kilka wirtualnych "tras" na torze i zacząłem zbieranie danych. W międzyczasie rozregulowałem nieco PID, aby robot łapał trochę więcej skrajnych pozycji. Po godzinie jazdy uznałem, że mam wystarczająco danych do rozpoczęcia trenowania sieci (ostatecznie jeździłem nieco dłużej, w celu pobrania informacji o miejscach, w których robot mylił się najczęściej). Zbieranie danych. Cz.7. Można oszukiwać. Jak wspomniałem są sposoby na poprawienie wyników sieci bez zwiększania ilości danych. Jednym z nich jest tzw. „transfer learning” polegający na użyciu gotowego modelu sieci (w moim przypadku ResNet50), wytrenowanego na milionach zdjęć, którego skuteczność została udowodniona na „ogólnych” obiektach (odróżnia jabłko od gruszki, psa od kota) i dotrenowanie zaledwie kilku ostatnich warstw tak, żeby model lepiej odpowiadał na nasze dane. Przyspiesza to trenowanie i zwiększa dokładność predykcji. W trakcie pracy eksperymentowałem z różnymi gotowymi modelami jak i wieloma wartościami, głównie z wielkością zdjęcia (znacząco wpływa to na czas trenowania) jak i z ilością epok, typem optymizerów. Mimo, że zdarzało mi się zostawiać komputer włączony całą noc, a treningi trwały nieraz ponad 14 godzin, ostateczny model został wytrenowany w około 2 godziny (na karcie graficznej GTX 1660). Trenowanie modelu. Niektóre próby trwały ponad 14 godzin. Jak ze skutecznością predykcji? Zaledwie 60%. A jednak robot jeździ po torze prawie bezbłędnie. Jak to możliwe skoro 4 na 10 razy się myli? Kluczem nie jest w tym przypadku skuteczność, tylko skala błędu. W większości przypadków komputer popełniał bardzo niewielkie pomyłki, które nie miały wpływu na jazdę. Analizując zdjęcia jestem pod wrażeniem działania tego modelu, sam dla zabawy próbowałem odgadnąć jakie odchyły przypadają do jakich zdjęć. Poniżej przedstawiam kilka, spróbujcie odgadnąć sami (wartości od -15 do 15): Od lewej: 6,-11, 15 (pikseli odchyłu od idealnego toru jazdy). Kiedy przekonałem się, że robot jeździ po prostej drodze, nadszedł czas na coś bardziej skomplikowanego niż zwykła klasyfikacja obrazu(np.: na zdjęciu jest pies!) mianowicie na detekcję (np.: na zdjęciu jest pies i znajduje się on tutaj!). Wykorzystałem do tego system detekcji YOLO (You only look once) stworzony przez Josepha Redmona. YOLO znane jest w internecie z bardzo niskiego czasu detekcji (detekcja trwa dłużej niż klasyfikacja). Redmon udostępnia swój algorytm za darmo wraz z instrukcją jak trenować sieć pod detekcję własnych obiektów. Na początku próbowałem wykorzystać bazę zdjęć i etykiet Open Images Dataset, ale szybko przekonałem się, że lepiej zastosować własne zdjęcia. Nie pozostało mi nic innego jak odpalić program do etykiet (LabelImg) i ręcznie oznaczyć ok. 20 tyś zdjęć przedstawiających znaki drogowe, sygnalizację świetlną i pieszych. Zajęło to parę dni, ale było warto, wskaźnik detekcji poprawił się znacznie, głównie ze względu na to, że niektóre zdjęcia które zdecydowałem się oznaczać były bardzo niewyraźne, albo słońce świeciło bezpośrednio w kamerę, co odzwierciedlało prawdziwe sytuacje. Etykietowanie danych było mozolnym procesem, ale efekt był więcej niż zadowalający. Cz.8. Mądrzej, szybciej, lepiej. Ostatecznie projekt spełnił moje oczekiwania, muszę przyznać, że na początku nie spodziewałem się tak dobrych rezultatów. Finalnym potwierdzeniem działania robota powinien być przejazd po nieznanym torze. Dla utrudnienia tor testowy postanowiłem zrobić w innym kolorze (nie posiadał on również przerywanych linii), na dodatek w testowym przejeździe postarałem się, aby słońce świeciło w kamerę. Wyniki przejazdu można zobaczyć na załączonym GIF-ie 🙂 . Pozwoliłem sobie na zachowanie tajemnicy w kwestii przedstawionego na początku "zadania z gwiazdką" czyli jazdy przez skrzyżowanie. Dla dociekliwych na końcu artykułu umieściłem link do GitHuba, kto będzie chciał, dojdzie w jaki sposób realizuję to zadanie 🙂 . Jazda po nowym torze. Jest wiele sposobów na usprawnienie robota. Warto zaznaczyć, że czas przesyłu danych po Wi-Fi zajmuje niemal tyle samo czasu co detekcja i klasyfikacja. Umieszczenie algorytmów na komputerze pokładowym umożliwiłoby przetwarzanie większej ilości klatek na sekundę tym samym zwiększając dokładność i być może maksymalną prędkość pojazdu. Należy również zaznaczyć, że projekt był eksperymentem mającym na celu sprawdzenie możliwości sieci neuronowych i mam nadzieję, że nikt nie pomyśli, że neguję sens stosowania klasycznej analizy obrazu. Poniżej załączam film przedstawiający działanie robota. Aha, nie wspomniałem chyba, że można nim sterować za pomocą kierownicy komputerowej, która w trybie autonomicznym odzwierciedla ruchy pojazdu? Wystarczy nacisnąć przycisk na kierownicy i jesteśmy w trybie jazdy autonomicznej. Cały kod znajduje się na GitHubie. W razie niejasności służę pomocą, w przypadku dużego zainteresowania tematem chętnie stworzę bardziej szczegółowe wpisy. Pozdrawiam i zachęcam do dyskusji w komentarzach. Rafał [1] Bostrom, Nick. Superinteligencja. Wydawnictwo Helion 2016. ISBN 978-83-283-1934-9. [2] Zdaję sobie sprawę, że pojęcia "sieci neuronowe" i "sztuczna inteligencja" nie są tożsame, używam uproszczenia jako, że sieci stanowią jak na razie główny kierunek rozwoju SI.
  16. Giętarka do tworzyw sztucznych to urządzenie służące do kształtowania tworzyw termoplastycznych, które w handlu występują najczęściej w postaci płaskich arkuszy (dotyczy głównie PMMA, czyli popularnej pleksi/plexy). Giętarkę do tworzyw sztucznych postanowiłem zbudować na własny użytek, aby mieć możliwość zaginania wyciętych na laserze elementów, z których następnie będę budować prototypy swoich robotów. Udział w tegorocznym konkursie Forbota potraktowałem jako motywację do ukończenia projektu w ściśle określonym terminie. 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 » Tworząc ten artykuł miałem w planach udostępnienie pełnej dokumentacji technicznej urządzenia, aby każdy czytelnik był w stanie je samodzielnie zbudować w swoim warsztacie. W trakcie budowy i podczas pierwszych testów okazało się jednak, że prototyp jest jeszcze mocno niedoskonały i wymaga licznych modyfikacji. Postanowiłem zmienić konwencję – zamiast instrukcji krok-po-kroku zaprezentuję poszczególne rozwiązania wraz z uwagami, które uwzględnię w drugiej wersji urządzenia. W trakcie projektowania Zapraszam do pytań i dyskusji – może wykluje się z tego pełnoprawny projekt open hardware 🙂 Giętarka do tworzyw sztucznych – zasada działania Aby zagiąć płaski arkusz tworzywa pod zadanym kątem należy podgrzać go na linii zgięcia do odpowiedniej temperatury, a następnie po prostu uformować i utrzymać w zadanej pozycji, aż materiał znów się utwardzi. Materiałem, który będzie kształtowany na tym urządzeniu jest polimetakrylan metylu (PMMA, czyli potocznie plexiglas, pleksi lub pleksa) o grubości 3 mm. Internetowe źródła podają, że pleksa staje się plastyczna w temperaturze 115 °C, jednak testy pokazały, że arkusz można giąć już po krótkim podgrzewaniu w temperaturze ok. 70 °C. Elementem grzewczym w giętarkach jest najczęściej drut oporowy, spirala lub grzałka podobna do tych stosowanych w piekarnikach lub kuchenkach elektrycznych. W swoim urządzeniu zastosowałem drut oporowy Kanthal A1 ze względu na łatwość jego obróbki i stosunkowo niską cenę. Fotografia 1: Giętarka do tworzyw sztucznych gotowa do pracy | Fotografia 2: Zagięty element z pleksy Konstrukcja urządzenia Elementy obudowy urządzenia wykonałem z płyty HDF o grubości 3 mm. Jest materiał względnie tani i łatwo poddający się obróbce na popularnych laserach CO2. Ramę wykonałem z drewnianych kantówek – głównie z tego względu, że drewno nie przewodzi prądu i mam pewne doświadczenie w jego obróbce. Aby zapewnić sobie dostęp do wnętrza urządzenia po jego zmontowaniu, obudowa nie jest przymocowana do ramy na sztywno za pomocą wkrętów – zastosowałem tzw. mufy, czyli odpowiednik nitonakrętek do drewna, w które można wkręcać zwykłe śruby metryczne. Mimo, że drewno rzadko kiedy jest dobrym towarzystwem dla wysokich temperatur, w tym przypadku nie ma ryzyka pożaru. Temperatura zapłonu surowego drewna to 210–350 °C, a HDFu jeszcze więcej. Maksymalna przewidywana temperatura pracy urządzenia to 120 °C, więc istnieje jeszcze duży margines bezpieczeństwa. Fotografia 3: Gotowa rama urządzenia Rzeczy do poprawy O ile elementy obudowy można zaprojektować i wyciąć na laserze z dużą dokładnością, o tyle przy samej ramie należy przyjąć dosyć duże tolerancje wymiarowe. Drobne przesunięcia podczas klejenia kantówek spowodowały, że elementy obudowy nie zbiegały się ze sobą idealnie w miejscach łączenia. Rama kolejnej wersji urządzenia powinna zostać zbudowana z materiału o bardziej przewidywalnych wymiarach pierwotnych. Na myśl przychodzą mi profile aluminiowe, ale trzeba będzie zadbać o dobrym odizolowaniu ich od elementów przewodzących. Zasilanie giętarki Nie czułem się na siłach, aby zasilać giętarkę bezpośrednio z gniazdka 220 V, więc postanowiłem zastosować zasilacz obniżający napięcie do wartości bezpiecznej. Wybrałem tani zasilacz laboratoryjny z zakresem pracy 30V/10A, co dało mi możliwość regulowania temperatury drutu oporowego bez dodatkowych elementów typu regulator mocy w samym urządzeniu. Zasilanie doprowadziłem do giętarki za pomocą przewodów bananowych, a następnie rozprowadziłem: (A) Bezpośrednio do obu końców drutu oporowego (jeden biegun do jednego, drugi do drugiego), (B) Do przetwornicy step-up/step-down, stabilizującej napięcie na poziomie 12 V. Komponent ten ma za zadanie utrzymywać napięcie zasilające mikrokontroler na stałym poziomie, niezależnym od bieżących parametrów zewnętrznego zasilacza. Napięcie z przetwornicy doprowadziłem następnie do zespołu 3 połączonych szeregowo stabilizatorów napięcia (C) obniżających 12 V do kolejno: 9V, 5 V i 3,3 V. Dwa ostatnie rozprowadziłem do osobnych szyn na płytce, aby w prosty sposób można było zasilać pozostałe podzespoły. Stabilizator 12 V służy jedynie bardziej równomiernemu rozprowadzaniu ciepła. Fotografia 4: Wnętrze urządzenia Rzeczy do poprawy Dużo tańszym rozwiązaniem byłoby zastąpienie zasilacza laboratoryjnego prostym regulatorem mocy oraz wyeliminowanie przetwornicy, która – ze względu na duży zakres obsługiwanych napięć – okazała się jednym z najdroższych komponentów całego urządzenia. Raspberry Pi Pico i peryferia Sercem całej elektroniki jest Raspberry Pi Pico. Za wyborem tego mikrokontrolera stały dwie przesłanki: Cena – dużo niższa niż Arduino, co miało mieć przełożenie na finalny koszt urządzenia. Założenie to okazało się jednak fałszywe – o ile sam mikrokontroler faktycznie jest tańszy, o tyle kompatybilne z nim peryferia znacząco podwyższyły budżet. Mowa tu o ekspanderze wyprowadzeń (którego Arduino nie potrzebuje) oraz wyświetlacz 3,3 V. Co prawda teoretycznie można zastosować „zwykły” wyświetlacz 5 V, ale konieczny jest wówczas dodatkowy konwerter stanów logicznych. Język programowania – Pico można programować w Pythonie, w którym czuję się pewniej niż w C++. Oszczędność czasu jednak znów okazała się pozorna. Wydaje się, że firmware Pico jest jeszcze mocno niedopracowany, przez co niestabilny. Ilość problemów, które pojawiły się przy okazji tworzenia stosunkowo nieskomplikowanego programu jest ogromna w porównaniu do analogicznego rozwiązania, które napisałem na Arduino: Moduł Timer nie działa prawidłowo – „gubi” zadaną częstotliwość i potrafi zawiesić program. Nie ma działających gotowych bibliotek do obsługi wyświetlaczy LCD i termometru – komendy obsługujące te peryferia musiałem zaszyć ręcznie w kodzie głównym, po uprzednim przekopaniu się przez biblioteki dla innych platform. Komunikacja poprzez magistralę I2C szwankuje – interpreter co chwila sypie błędami o braku podłączonych urządzeń lub nieprawidłowo wybranych pinach. Pico regularnie się zawiesza i nie ma to związku z temperaturą pracy, która cały czas pozostaje w normie. Do Raspberry Pi Pico – oprócz przycisków sterujących – podłączyłem 3 urządzenia peryferyjne: Buzzer z generatorem (D) – wydający dźwięk w momencie zakończenia odliczania czasu. Wyświetlacz LCD (E) – należy pamiętać o konieczności wyboru modelu obsługiwanego przez napięcie 3,3 V lub zastosowanie dodatkowego konwertera stanów logicznych. Termoparę (F) wraz ze sterownikiem (G) – nie wiedziałem jaką temperaturę będę musiał wytworzyć, aby urządzenie działało poprawnie, więc wybrałem czujnik z dużym zakresem odczytu (wg dokumentacji: od -270 °C do 1372 °C). Fotografia 5: Wnętrze urządzenia | Fotografia 6: Termopara | Fotografia 7: Front urządzenia Rzeczy do poprawy Kwestią dyskusyjną jest sam wybór Pico, jako głównego mikrokontrolera. Tak długo, jak firmware będzie niestabilne oraz gama akcesoriów będzie stosunkowo niewielka to wybór Pico do zastosowań półprofesjonalnych będzie raczej bezzasadne. W przypadku tego projektu identyczną funkcjonalność można uzyskać dużo niższym kosztem za pomocą starego poczciwego Arduino. Przerostem formy nad treścią okazał się również zakup termopary. Z uwagi na to, że używane tworzywo sztuczne staje się plastyczne już w 70 °C, to w zupełności wystarczyłby dużo tańszy i prostszy w obsłudze termometr cyfrowy DS18B20. Kod źródłowy Oprogramowanie sterownika giętarki do tworzyw sztucznych pełni następujące funkcje: W regularnych odstępach czasu (co 1 sekundę) odczytuje temperaturę z czujnika i jeśli nastąpiła zmiana to wyświetla ją na wyświetlaczu. Aby odczyt i wyświetlanie temperatury było niezależne od głównej pętli, ten fragment kodu wykonywany jest na drugim rdzeniu Pico. Po naciśnięciu przycisku „Start” program zaczyna odliczać w dół od ustawionej liczby sekund. Dzięki temu użytkownik ma możliwość dokładnego odmierzania czasu potrzebnego do uplastycznienia obrabianego materiału. Po osiągnięciu „0” uruchamia się buzzer sygnalizujący koniec odliczania. Aktualna pozycja timera wyświetlana jest na wyświetlaczu. Przycisk „Reset” umożliwia przerwanie odliczania w dowolnym momencie. Przyciski „Plus” i „Minus” umożliwiają zmianę zaprogramowanego czasu odliczania i wyświetlenie aktualnej wartości na wyświetlaczu. import machine import utime import _thread # Deklaracja zmiennych globalnych global time time = 60 global prev_time prev_time = 0 global temp temp = 0 global prev_temp prev_temp = 0 global reset reset = False global counting counting = False # Deklaracja pinow led = machine.Pin(25, machine.Pin.OUT) start_btn = machine.Pin(2, machine.Pin.IN, machine.Pin.PULL_DOWN) reset_btn = machine.Pin(3, machine.Pin.IN, machine.Pin.PULL_DOWN) plus_btn = machine.Pin(4, machine.Pin.IN, machine.Pin.PULL_DOWN) minus_btn = machine.Pin(5, machine.Pin.IN, machine.Pin.PULL_DOWN) buzzer = machine.Pin(15, machine.Pin.OUT) # Funkcje def PrintData(): global LCD_i2c global TEM_i2c global time global prev_time global temp global prev_temp if (prev_time != time) or (prev_temp != temp): #jezeli aktualny czas lub temperatura rozni sie od poprzedniego odczytu to wyswietl aktualny. Warunek zapobiega ciaglemu wysylaniu danych na LCD temp_prt = str(temp) temp_prt = temp_prt[0:4] temp_str = "Temp: " + temp_prt + " " time_str = "Time: " + str(time) lcd_str = temp_str + time_str print(temp_str) print(time_str) LCD_i2c.writeto(114, '\x7C') LCD_i2c.writeto(114, '\x2D') LCD_i2c.writeto(114, lcd_str) prev_time = time prev_temp = temp def StopCounting(): global reset for i in range(6): buzzer.toggle(); utime.sleep(0.1) reset = False def ResetCounting(x): global reset reset = True reset_btn.irq(trigger=machine.Pin.IRQ_RISING, handler=ResetCounting) #przerwanie "nasluchujace", czy przycisk RESET zostal wcisniety def CheckTemp(): global temp TEM_i2c=machine.SoftI2C(sda=machine.Pin(12), scl=machine.Pin(13), freq=10000, timeout=1000) #nie wiem dlaczego musi byc SoftI2C - normalny nie dzialal utime.sleep(1) while True: a = TEM_i2c.readfrom_mem(16,0x00,1)[0] b = TEM_i2c.readfrom_mem(16,0x01,1)[0] d = TEM_i2c.readfrom_mem(16,0x03,1)[0] temp = (((a << 8) | b) >> 2)*0.25 #wzor zaczerpniety z biblioteki termopary - przeliczanie odczytow na stopnie utime.sleep(1) _thread.start_new_thread(CheckTemp, ()) #uruchomienie funkcji na drugim rdzeniu # Inicjalizacja LCD global TEM_i2c global LCD_i2c LCD_i2c=machine.SoftI2C(sda=machine.Pin(16), scl=machine.Pin(17), freq=10000, timeout=1000) #znow SoftI2C, bo zwykly nie dzialal utime.sleep(1) #Petla glowna while True: PrintData() #Funkcja wyswietli dane (czas, temperatura) tylko jesli ulegly zmianie if start_btn.value() == 1: #po nacisnieciu Start uruchom odliczanie... i = time while (i > 0) and (reset == False): #... ale tylko do momentu dojscia do zera lub wcisniecia resetu print("Counting...") print("Time: " + str(i)) lcd_str = "Counting... " + "Time: " + str(i) LCD_i2c.writeto(114, '\x7C') LCD_i2c.writeto(114, '\x2D') LCD_i2c.writeto(114, lcd_str) utime.sleep(1) i -= 1 prev_time = i StopCounting() if (plus_btn.value() == 1) and (time < 1000): #dodanie sekundy do czasu odliczania utime.sleep(0.25) time += 1 if (minus_btn.value() == 1) and (time > 1): #odjecie sekundy od czasu odliczania utime.sleep(0.25) time -= 1 Moduł grzewczy Moduł grzewczy to najważniejszy komponent giętarki do tworzyw sztucznych, ponieważ od jakości jego działania zależy, czy urządzenie w ogóle będzie spełniać swoją funkcję. Niestety w trakcie testów okazało się, że jest to najsłabszy punkt całego prototypu i jego opis to w zasadzie jedna długa lista koniecznych poprawek: Dobór drutu oporowego – mimo usilnych chęci nie udało mi się usystematyzować wiedzy dot. doboru odpowiedniego drutu oporowego. Największym problemem jest określenie mocy, za pomocą której będzie można osiągnąć zakładaną temperaturę samego drutu, a co za tym idzie jego otoczenia. Znając potrzebną moc oraz parametry źródła zasilania będzie można następnie dobrać średnicę oraz długość drutu. Na ten moment jednak nie podejmę się tego zadania – potraktujmy to jako „zawieszony” temat do kolejnego artykułu. Drut do prototypowej giętarki dobrałem eksperymentalnie. Najlepiej sprawdził się Kanthal A1 o średnicy 0,32 mm. Przy długości wynoszącej ok. 30 cm optymalną temperaturę do gięcia pleksy uzyskałem po zasileniu go mocą 100 W (ok. 25V i 4 A). Do zagięcia pleksy o grubości 3 mm należy podgrzewać ją przy zadanych parametrach przez ok. 60 sekund. Mocowanie drutu oporowego – jeden koniec drutu oporowego można zamocować „na sztywno”, ale drugi koniecznie musi kończyć się sprężyną ściągającą. Wraz ze wzrostem temperatury drut rozciąga się, a naciąg ma za zadanie to kompensować i zapobiegać jego przerwaniu po zetknięciu z obudową. Dosyć problematyczne okazało się zarobienie końcówek drutu. Wykorzystałem do tego celu zwykłe konektory zaciskowe, jednak utrudniają one dokładne wypoziomowanie – wraz ze wzrostem temperatury, gdy sprężyna zaczyna pracować, konektory wraz z mocowaniem zaczynają niekontrolowanie „poruszać się”, co zwiększa ryzyko pęknięcia. W drugiej wersji urządzenia konieczne będzie podpatrzenie profesjonalnych rozwiązań, które utrzymują drut w idealnie równej pozycji niezależnie od jego temperatury oraz zapewniają odpowiedni naciąg po jego rozciągnięciu. Zasilanie drutu oporowego – miejsce podłączenia końcówek zasilających będzie pochodną zastosowanego sposobu mocowania. W pierwszej wersji (niepokazanej na zdjęciach) jeden biegun podłączyłem bezpośrednio do sprężyny naciągowej. Jednak z powodu wzrostu temperatury sprężyna traciła swoje właściwości. Warto więc pamiętać o tym, aby pomiędzy dwoma biegunami zasilającymi nie było żadnego innego przewodnika oprócz samego drutu oporowego. Fotografia 8: Mocowanie drutu oporowego | Fotografia 9: Mocowanie drutu oporowego Dodatki usprawniające pracę Jednym z ciekawszych dodatków usprawniających pracę na giętarce, którego nie podpatrzyłem w urządzeniach profesjonalnych, jest ruchomy profil aluminiowy blokowany za pomocą nakrętek skrzydełkowych. Dzięki wygrawerowanej podziałce na górnej obudowie użytkownik ma możliwości dokładnego ustalenia pozycji giętego elementu na urządzeniu. Fotografia 10: Linijka Podsumowanie Patrząc na ostatnią eksplozję rakiety SpaceX jeden z jej twórców stwierdził podobno ze stoickim spokojem, że każda kolejna porażka tylko przybliża go do sukcesu. Mimo, że prototyp mojego pierwszego w życiu urządzenia DIY jest daleki od założonego ideału to i tak cieszę się, że udało się go ukończyć, a efektami i wnioskami jestem w stanie podzielić się na szerszym forum. O autorze Łukasz Jędrasiak - z wykształcenia politolog, z zawodu marketing manager, z pasji robotyk. Uważam, że najlepszą metodą przewidywania przyszłości jest jej tworzenie, więc chcę zbudować robota, który weźmie udział w misji na Marsa.
  17. 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)
  18. Chcielibyśmy stworzyć robota omijającego przeszkody lub znajdującego swoją drogę w labiryncie. Potrzebny jest nam wtedy jakiś sposób wykrycia oraz ustalenia odległości od naszej ściany lub przeszkody. Jednym z tańszych sposobów jest zastosowanie ultradźwiękowego czujnika odległości. Odczyt sygnału i jego interpretacja jest tylko jednym z kilku zagadnień użytkowania tego typu sensora. Zgłębmy je razem poniżej. 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 » Zacznijmy od początku W naszych rozważaniach weźmy pod uwagę jeden z najpopularniejszych tego typu czujników – HC-SR04. Powyżej widzimy schemat połączenia czujnika z płytką Arduino Uno. W teorii wystarczy, że określimy wejścia/wyjścia, nadamy odpowiedni impuls na pin Trg, odczytamy długość impulsu na pin Echo i podzielimy go przez podaną w dokumentacji liczbę 58. Otóż ta wartość nie zawsze jest poprawna. Zasada działania czujnika opiera się na tym, że nadany sygnał dźwiękowy odbija się od przeszkody i wraca do odbiornika. W czasie podróży fal dźwiękowych na pinie echo ustawiony jest stan wysoki, zmienia się on na stan niski wraz z odczytaniem przez odbiornik powrotu fali. My odczytujemy długość trwania tego stanu i dzielimy go przez prędkość dźwięku. Prędkość a temperatura Załóżmy, że chcemy by nasz robot pracował na świeżym powietrzu. Liczba 58 o której mówi dokumentacja daje nam po podzieleniu przez czas impulsu odległość mierzoną w centymetrach zakładając, że prędkość dźwięku nie zmienia się. Dla temperatury 20˚C prędkość dźwięku wynosi 343,5 m/s ale dla 10˚C jest to już 337,5 m/s. Jeżeli dla prędkości w 20˚C odczytamy na czujniku 1 metr to dla 10˚C przy pozostawieniu tego samego przelicznika dla tej samej odległości odczytamy 0,98 metra. Poniżej znajduje się wykres, gdzie pokazano o ile procent różnią się pomiary w zależności od temperatury otoczenia w stosunku do pomiaru, gdzie za prędkość dźwięku przyjęliśmy tą podaną w dokumentacji (340 m/s). Prędkość dźwięku w zależności od temperatury liczymy ze wzoru: v = 331,5 + 0,6*T [m/s] Zakładając, że mierzymy odległość 4 metrów to w temperaturze 0˚C nasz pomiar jest o 10 cm różny od rzeczywistego licząc odległość dla 340 m/s. Aby temu zaradzić możemy zastosować czujnik temperatury który umożliwiałby robotowi w odpowiedni sposób zmieniać mnożnik, lub uwzględnić ten błąd pomiarowy w projekcie. Położenie ma znaczenie Błędy pomiarowe opisane powyżej są dla większości rozwiązań akceptowalne i można nie zwracać na nie uwagi. Jednak co jeżeli obiekt jest niewidoczny dla czujnika? W dokumentacji technicznej sensora HC-SR04 możemy wyczytać informację o tym, że kąt pomiarowy wynosi 30˚. Jednak nasuwa się pytanie dla jakich obiektów mierzono, gdzie ustalono wierzchołek kąta mierzenia oraz jakie są kryteria uznania pomiaru za poprawny. Po wykonaniu wielu pomiarów znalezione zostały na nie odpowiedzi. Po to by nasz stożek pomiarowy był symetryczny należy umieścić jego wierzchołek w odpowiednim miejscu. Położeniem tym jest środek czujnika. To właśnie ten punkt uznajemy za miejsce, gdzie kąt pomiaru wynosi 0˚. Symetria stożka pomiarowego ułatwi nam późniejsze projektowanie całego systemu odpowiedzialnego za mierzenie odległości. W naszej analizie będziemy wizualizować wyniki za pomocą schematu poniżej. Nasze rozważania co do kąta zaczniemy od prostokątnego pudełka. W zależności od czujnika zakres ,,widzenia’’ wynosił -11˚÷ 11˚ a w jednym z czujników nawet -6˚÷6˚. Jak można zauważyć efektywne kąty pomiarowe różnią się w zależności od egzemplarza. Pomiar poprawny otrzymujemy już wtedy, kiedy przynajmniej krawędź obiektu znajduje się w obszarze zaznaczonym na zielono. Jako dobry pomiar uznajemy zmierzenie odległości rzędu 50 cm z błędem mniejszym niż 2 cm, uwzględniając korektę temperaturową. Kolejnym elementem będzie ,,ścianka” z kartonu. Z takim obiektem może spotkać się nasz robot wielokrotnie w labiryncie. W tym przypadku wierzchołek pomiarowy jest o wiele mniejszy niż w przypadku pudełka. Zakres dobrych pomiarów wynosi -4˚÷4˚ a dla najgorszego przypadku -1˚÷1˚. Z racji tak wąskiego stożka może zajść potrzeba zastosowania większej liczby czujników bądź dokładne ich rozmieszczenie z uwzględnieniem zakresu dobrych pomiarów wspomnianych powyżej. Dla elementów wklęsłych i wypukłych względem sensora pomiary dla całego stożka są niepoprawne. Podsumowanie Czujnik HC-SR04 nie jest bardzo wysokiej klasy sensorem ultradźwiękowym (świadczy o tym na przykład rozrzut wartości kąta stożka pomiarowego różnych egzemplarzy). Można jednak przy uwzględnieniu warunków atmosferycznych i rodzajów obiektów jakie mamy mierzyć otrzymać dobre wyniki nie narażając się na koszty związane z zakupem sprzętu o lepszych parametrach. Dla poszerzenia kąta pomiarowego w labiryncie z cienkimi ścianami można zastosować kilka czujników. Kwestia zmiany prędkości dźwięku w zależności od temperatury staje się problematyczna dopiero na dużych odległościach, więc w przypadku robotów manewrujących nie musimy jej brać pod uwagę.
  19. Witam, Chcę przedstawić wam moje podejście do tematu sterowników akwarystycznych. Sterownik ten zbudowałem dla swojego dziadka, który chciał załączać automatycznie pompkę, napowietrzacz i światło do oświetlenia akwarium. Zacząłem więc planować, stwierdziłem, że sterownik musi posiadać minimum 2 wyjścia 230V, jedno wyjście 12V z możliwością sterowania PWM, jakieś bajery (odczyt temperatury wody, automatyczne wyłącznie przekaźników, automatyczny karmnik dla ryb itp). Kilka lat temu zbudowałem swój pierwszy sterownik akwarystyczny (nazwałem go V.1), wykorzystałem do tego celu esp8266-12e, moduł podwójnego przekaźnika elektromagnetycznego oraz czujnik DS18B20. Nie mam niestety zdjęć pierwszego modelu. Działał on u mnie przez ponad rok, w tym czasie wynotowałem kilka wad mojego rozwiązania. Największe wady: -Podczas braku dostępu do internetu nie mogłem korzystać z sterownika, -Przekaźnik nie zawsze wyłączał się, -esp zawieszało się podczas załączania przekaźnika (podczas rozbiórki sterownika okazało się, że dobrałem złe wartości rezystorów do LM317 i zamiast 3,3V na esp podawałem 2,95V ) Sterownik V.2 (dla dziadka) Postanowienia: Urządzenie musiało być proste w użytkowaniu, nie mogło tracić swoich podstawowych funkcjonalności podczas braku dostępu do internetu, sterownik powinien pamiętać stany wyjść podczas braku zasilania sterownik powinien automatycznie sterować wyjściami i modułami do niego podłączonymi sterownik powinien sterować wyjściem 12V w sposób płynny, imitować zachód lub wschód słońca na pasku LED musiał posiadać funkcję zdalnego zarządzania swoimi funkcjami 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 » Działanie: Sterownik składa się z dwóch części, głównego sterownika oraz karty sieciowej. Sterownik główny odpowiada za włączanie/wyłączanie gniazd 230V, podawaniem sygnału PWM na wyjście, odczyt temperatury/wilgotności z czujników ds18b20 i DHT11, wyświetlanie godziny i innych danych na wyświetlaczu siedmio segmentowym, sprawdzaniu i wykonywaniu "alarmów", komunikowanie się z modułem karty sieciowej poprzez UART. Karta sieciowa - czyli moduł esp8266 (Wemos D1 mini) odpowiada za komunikację z aplikacją Blynk, zbieranie informacji (logów) z działania całego urządzenia i zapisywaniu ich na karcie SD, pobieraniu z internetu godziny do synchronizacji czasu, wysyłaniu wiadomości email z błędami w działaniu urządzenia lub plikami txt z logami, wyświetlaniu statusu połączenia z siecią i aplikacją za pomocą diod LED, komunikacją ze sterownikiem głównym, obsłudze komend wysyłanych przez terminal w aplikacji Blynk, itp. Budowa: W sterowniku głównym znajduje się: Arduino Nano Moduł podwójnego przekaźnika półprzewodnikowego SSR, zdecydowałem się na zmianę rodzaju przekaźnika z powodu tego, że taki przekaźnik jest bezgłośny, zawsze działa (nie zawiesza się tak jak przekaźniki elektromagnetyczne) Moduł zegara czasu rzeczywistego DS1307, odpowiada za dostarczanie dla wyświetlacza aktualnej godziny nawet po wyłączeniu zasilania oraz jako czas potrzebny do działania alarmów. Czujniki DS18B20 (wersja wodoodporna na metrowym przewodzie, metalowa końcówka czujnika znajduje się w wodzie) oraz DHT11 (wyprowadzony jest na zewnątrz obudowy sterownika) 4 przyciski z podświetleniem led w 4 kolorach(czerwony, zielony, niebieski, żółty). Odpowiadają one za włączanie i wyłączanie przekaźników, zmianę jasności paska LED, włączenie karmnika dla ryb oraz wyświetleniu na wyświetlaczu aktualnych temperatur Wyświetlacz siedmio segmentowy TM1637, wyświetla on aktualną godzinę, temperaturę, kody błędów Tranzystor IRF540 wraz z transoptorem PC817 i kilkoma rezystorami Przetwornica step-down (obniża napięcie 12V z zasilacza, na 5V potrzebnych do zasilenia Arduino i reszty modułów) Bezpieczniki 1A na gniazdka 230V oraz 1,2A na zasilaczu 12V Włącznik dźwigniowy (przerywa napięcie zasilania 12V) Zasilacz 12V 1,5A W karcie sieciowej znajduje się: Wemos D1 Mini, z dolutowaną anteną od starego routera tp-link (link do instrukcji takiej operacji instrukcja ) Adapter do karty microSD - SD 3 diody plus rezystory 220Ω micro switch, służy do resetowania płytki przełącznik 3 pozycyjny, służy do wyboru sieci WiFi, np. 1 - sieć1, 2 - sieć2, 3 - sieć serwisowa przełącznik 2 pozycyjny, służy do wyłączenia 3 diod sygnalizacyjnych LED, kiedy nie są potrzebne konwerter stanów logicznych, służy do konwersji napięć między Arduino a Wemosem na magistrali UART moduł pcf8574, steruje diodami oraz automatycznym karmnikiem, w przyszłości może także sterować kolejnymi przekaźnikami Działanie Sterownika głównego void setup: wczytanie ostatnich stanów wyjść z pamięci EEPROM ustawienie przerwania na 1ms (odpowiada za zmianę wartości na pinie PWM oraz dekrementuje zmienną potrzebną do wyświetlania informacji na wyświetlaczu ustawiam wejścia i wyjścia ustawiam stany pinów przy pomocy danych odczytanych z pamięci EEPROM (funkcja przywracania ostatnich stanów pinów) ustawiam jasność paska led (podczas włączenia sterownika, światło powoli rozjaśnia się, do ostatniego zapisanego stanu) Inicjuje zegar, czujniki temperatur, przyciski itp. void loop: W pętli loop sprawdzam: Czy jakiś przycisk został wciśnięty Ustawiam stany pinów, odświeżam informacje na wyświetlaczu Sprawdzam czy jakiś alarm może zostać wykonany Sprawdzam czy karta sieciowa wysłała jakieś dane Liczę ilość przejść pętli w czasie jednej sekundy, co sekundę wyświetlam tę ilość na serial monitorze Sprawdzam, czy minęło 5 sekund, po tym czasie raportuje do karty sieciowej o stanach wyjść ------------------------------------------------------------------------------------------------------------------------ 1.Sprawdzenie przycisków - Arduino obsługuje 4 przyciski, służą one do: Pojedyncze (krótkie wciśnięcie) - włączenie/wyłączenie przekaźnika 1 i 2, zmiany jasności paska LED podłączonego do pinu PWM, załączenie karmnika, Długie wciśnięcie - wyświetlenie na wyświetlaczu siedmio segmentowym temperatury wody/powietrza, szybkie wyłączenie paska LED przycisk niebieski - krótkie wciśnięcie (zmiana stanu przekaźnika 1) / długie wciśnięcie (wyświetlenie na wyświetlaczu temperatury wody) przycisk czerwony - krótkie wciśnięcie (zmiana stanu przekaźnika 2) / długie wciśnięcie (wyświetlenie na wyświetlaczu temperatury powietrza) przycisk zielony - krótkie wciśnięcie (zmiana stanu na pinie PWM ), każde wciśnięcie zwiększa wartość na pinie PWM [0,20,40,60,80,100%] / długie wciśnięcie szybko wygasza pasek LED przycisk żółty - krótkie wciśnięcie. Powoduje mruganie podświetleniem przycisku czerwonego i zielonego przez 10 sekund, wciśnięcie przycisku zielonego spowoduje podjęcie przez sterownik próby załączenia automatycznego karmnika \ wciśnięcie przycisku czerwonego anuluje ten proces, tak samo jak nie wciśnięcie żadnego z przycisków przez 10 sekund void checkbutton() { if (checkbutton_debuce == 1) { Serial.print(F("R_SW_State: ")); Serial.print(R_SW_state); Serial.print(F(" | B_SW_State: ")); Serial.print(B_SW_state); Serial.print(F(" | G_SW_State: ")); Serial.print(G_SW_state); Serial.print(F(" | Y_SW_State: ")); Serial.println(Y_SW_state); } G_SW.read(); B_SW.read(); Y_SW.read(); R_SW.read(); if (B_SW.changed()) { display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(1, false, 1, 1); last_millis2 = 0; B_SW_state = B_SW.toggleState(); if (B_SW_state == 1) { display.setSegments(display_symbol_off, 2, 2); send_command(s1on, 8); } else { display.setSegments(display_symbol_on, 2, 2); send_command(s1off, 8); } screen_timeout = 5000; } if (R_SW.changed()) { display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(2, false, 1, 1); last_millis2 = 0; R_SW_state = R_SW.toggleState(); if (R_SW_state == 1) { send_command(s2on, 8); display.setSegments(display_symbol_off, 2, 2); } else { send_command(s2off, 8); display.setSegments(display_symbol_on, 2, 2); } screen_timeout = 5000; } if (G_SW.pressedFor(LONG_PRESS)) { digitalWrite(GREEN_SW_LIGHT, 0); delay(50); digitalWrite(GREEN_SW_LIGHT, 1); delay(50); digitalWrite(GREEN_SW_LIGHT, 0); delay(50); digitalWrite(GREEN_SW_LIGHT, 1); delay(50); last_millis2 = 0; G_SW_button_cycle = 0; G_SW_state = 0; set_pwm_led_1(G_SW_state); led_step = 2; Serial.println("Green Button LONG PRESS DETECTED !"); display.clear(); display.showNumberDec(map(G_SW_state, 0, 255, 0, 100), false, 3, 1); display.setSegments(display_symbol_s, 1, 0); while (true) { G_SW.read(); if (G_SW.wasReleased()) { break; } } screen_timeout = 5000; return; } if (G_SW.changed()) { digitalWrite(GREEN_SW_LIGHT, 1); last_millis2 = 0; G_SW_button_cycle++; if (G_SW_button_cycle >= 6) { G_SW_button_cycle = 0; } switch (G_SW_button_cycle) { case 0: G_SW_state = 0; break; case 1: G_SW_state = 51; break; case 2: G_SW_state = 102; break; case 3: G_SW_state = 153; break; case 4: G_SW_state = 204; break; case 5: G_SW_state = 255; break; } display.clear(); display.showNumberDec(map(G_SW_state, 0, 255, 0, 100), false, 3, 1); display.setSegments(display_symbol_s, 1, 0); vpwm1[4] = (byte)G_SW_button_cycle * 2; send_command(vpwm1, 8); delay(40); if (G_SW_state != 255) { digitalWrite(GREEN_SW_LIGHT, 0); } led_step = led_step_button; screen_timeout = 5000; } if (Y_SW.pressedFor(LONG_PRESS)) { //locking feeding option Serial.println("Yellow Button LONG PRESS DETECTED !"); while (true) { Y_SW.read(); if (Y_SW.wasReleased()) { //send_command(auto_feed, 9); delay(25); break; } } return; } if (Y_SW.changed()) { bool wait2pressB = 0; bool w2p_ledstate = 1; unsigned long y_long_press_ms = millis(); unsigned long y_long_press_ms_led = millis(); while (wait2pressB == 0) { green_led_switch_on = 0; R_SW.read(); G_SW.read(); if (millis() - y_long_press_ms_led > 500) { digitalWrite(GREEN_SW_LIGHT, w2p_ledstate); digitalWrite(RED_SW_LIGHT, w2p_ledstate); w2p_ledstate = !w2p_ledstate; y_long_press_ms_led = millis(); } if (G_SW.changed()) { digitalWrite(GREEN_SW_LIGHT, G_SW_state); digitalWrite(RED_SW_LIGHT, R_SW_state); send_command(auto_feed, 9); delay(25); green_led_switch_on = 1; return; } else if (R_SW.changed()) { digitalWrite(GREEN_SW_LIGHT, G_SW_state); digitalWrite(RED_SW_LIGHT, R_SW_state); green_led_switch_on = 1; return; } else { } if (millis() - y_long_press_ms > 10000) { digitalWrite(GREEN_SW_LIGHT, G_SW_state); digitalWrite(RED_SW_LIGHT, R_SW_state); green_led_switch_on = 1; return; } } } } 2.Ustawienie odpowiednich stanów pinów: Ta funkcja odpowiada za zmianę stanów wyjść do których podłączony jest moduł przekaźników oraz za zmianę informacji na wyświetlaczu (godziny). void set_pin_states() { set_power_230V_1(B_SW_state); set_power_230V_2(R_SW_state); display_value_on_7s_display(Y_SW_state); } void set_power_230V_1(bool s) { digitalWrite(power_230V_1, !s); digitalWrite(BLUE_SW_LIGHT, s); } void set_power_230V_2(bool s) { digitalWrite(power_230V_2, !s); digitalWrite(RED_SW_LIGHT, s); } void display_value_on_7s_display(byte dis) { if (screen_timeout > 0) { return; } int current_t = 0; current_t = H * 100 + MIN; display.showNumberDecEx(current_t, 64, true); } 3."Alarmy": Poprzednia wersja sterownika posiadała ogromną wadę, chodzi o automatyczne załączanie przekaźników lub innych modułów. W poprzedniej wersji za załączanie automatyczne odpowiadała aplikacja Blynk z widżetem Timer, było to dość wygodne wyjście, ponieważ całą konfigurację (co ma się włączyć, kiedy, itp.) robiło się w przejrzystym menu aplikacji. To rozwiązanie miało jednak dużą wadę, podczas braku dostępu do internetu, sterownik nie wykonywał żadnych akcji. W drugiej wersji postanowiłem całkowicie oddzielić od siebie warstwę internetową (komunikacji z internetem) od warstwy podstawowych funkcjonalności, takich jak automatyczne włączanie/wyłączanie przekaźników, automatyczna zmiana jasności, włączenie karmnika itp. Co to jest "alarm"? - alarmem nazywam jedną funkcję, która wykona się o danej godzinie, minucie, dniu. Przechowywany jest w formie 5 jedno bajtowych komórek, ułożonych obok siebie w pamięci EEPROM. Alarm składa się z: A (Action- Pierwsza komórka, przechowuje numer funkcji, która ma zostać wykonana.) D (Day- Dzień tygodnia w który musi wykonać się alarm. 1 - poniedziałek / ... / 7 - niedziela / 8 - każdy dzień tygodnia) H (Hour - godzina. 0 - 23) M (Minute - minuta, 0 - 59) V (Value - parametr funkcji, np. funkcja 1 przyjmuje parametr 1 lub 0 [ włączenie / wyłączenie przekaźnika 1]) Ograniczenia - sterownik może przechować maksymalnie 80 alarmów, dwa lub więcej alarmów nie może zostać wykonanych w tej samej minucie Funkcja sprawdzająca, czy jakiś alarm może zostać wykonany: void check_alarm() { if (alarm_onoff == 1) { int p = 0; int n = 0; while (n <= ile_alarmow) { if (EEPROM.read(n * 5) != 127) { p = (byte)(n * 5); byte alarm_d, alarm_h, alarm_min, alarm_action, alarm_val; alarm_action = (byte)EEPROM.read(p); alarm_d = (byte)EEPROM.read(p + 1); alarm_h = (byte)EEPROM.read(p + 2); alarm_min = (byte)EEPROM.read(p + 3); alarm_val = (byte)EEPROM.read(p + 4); get_current_time(); if ((alarm_d == 8 || alarm_d == weekd) && alarm_onoff == 1) { if (alarm_h == H && alarm_min == MIN) { Serial.print(F("Wykonam akcje nr. ")); Serial.println(alarm_action); Serial.print(F("Alarm is trigered on ")); Serial.print(alarm_h); Serial.print(F(":")); Serial.print(alarm_min); Serial.print(F(":")); Serial.println(S); execute_alarm((byte)alarm_action, (byte)alarm_val); alarm_onoff = 0; alarm_timer.reset(); //1 minuta przerwy między alarmami } } else { //Serial.print(F("Nie wykonam akcji nr. ")); //Serial.println(i); } } n++; } } } void execute_alarm(byte execute_action_number, byte execute_value) { Serial.print(F("I'm executing function number ")); Serial.print(execute_action_number); Serial.print(F(" with value: ")); Serial.println(execute_value); if (execute_action_number == 1 || execute_action_number == '1') { bool action_flag; if (execute_value == 1) { action_flag = 1; } else if (execute_value == 0) { action_flag = 0; } else { action_flag = 0; } B_SW_state = action_flag; display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(1, false, 1, 1); if (B_SW_state == 1) { display.setSegments(display_symbol_off, 2, 2); } else { display.setSegments(display_symbol_on, 2, 2); } screen_timeout = 5000; //set_power_230V_1(action_flag); } else if (execute_action_number == 2 || execute_action_number == '2') { bool action_flag2; if (execute_value == 1) { action_flag2 = 1; } else if (execute_value == 0) { action_flag2 = 0; } else { action_flag2 = 0; } R_SW_state = action_flag2; display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(2, false, 1, 1); last_millis2 = 0; R_SW_state = R_SW.toggleState(); if (R_SW_state == 1) { send_command(s2on, 8); display.setSegments(display_symbol_off, 2, 2); } else { send_command(s2off, 8); display.setSegments(display_symbol_on, 2, 2); } screen_timeout = 5000; //set_power_230V_2(action_flag2); } else if (execute_action_number == 3 || execute_action_number == '3') { if (execute_value < 11 && execute_value > -1) { G_SW_state = map(execute_value, 0, 10, 0, 255); Serial.print(F("Setting led pwm value by alarm to ")); Serial.println(G_SW_state); if (execute_value == 0) { G_SW_button_cycle = 0; } else if (execute_value > 0 && execute_value <= 2) { G_SW_button_cycle = 1; } else if (execute_value > 2 && execute_value <= 4) { G_SW_button_cycle = 2; } else if (G_SW_state > 4 && G_SW_state <= 6) { G_SW_button_cycle = 3; } else if (G_SW_state > 6 && G_SW_state <= 8) { G_SW_button_cycle = 4; } else if (G_SW_state > 8 && G_SW_state <= 10) { G_SW_button_cycle = 5; } else { G_SW_button_cycle = 0; } led_step = led_step_alarm; //set_pwm_led_1(execute_value); display.clear(); display.showNumberDec(map(G_SW_state, 0, 255, 0, 100), false, 3, 1); display.setSegments(display_symbol_s, 1, 0); screen_timeout = 5000; } else { Serial.println(F("Wrong value !")); } } else if (execute_action_number == 4 || execute_action_number == '4') { Serial.println(F("Automatic feeding !!!")); //byte auto_feed[9] = {0x7E, 'A', 'F', ';', 'e', 'E', 'G', 0, 0x7F} send_command(auto_feed, 9); } else if (execute_action_number == 5 || execute_action_number == '5') { //re_send_time[9] = {0x7E, 'R', 'E', ':', 'S', '8', 'M', 0, 0x7F}; Serial.println(F("Alarm Automatic time syncing !!!")); send_command(re_send_time, 9); } else if (execute_action_number == 6 || execute_action_number == '6') { set_brightness(execute_value); } else if (execute_action_number == 7 || execute_action_number == '7') { if (EEPROM.read(1010) == 0) { //EEPROM.write(1010, 1); eeprom_optimalization(1010, 1); if (execute_value == 0) { //reset arduino Serial.println(F("Resseting arduino...")); resetFunc(); } else if (execute_value == 1) { //reset esp Serial.println(F("Resseting esp...")); send_command(reset_esp_command, 9); } else if (execute_value == 2) { //reset arduino and esp Serial.println(F("Resseting esp...")); send_command(reset_esp_command, 9); Serial.println(F("Resseting arduino...")); resetFunc(); } else { Serial.println(F("Wrong reset value!range:0-2")); //EEPROM.write(1010, 0); eeprom_optimalization(1010, 0); } } } else if (execute_action_number == 8 || execute_action_number == '8') { //send_all_mail } else if (execute_action_number == 9 || execute_action_number == '9') { //deleta_all_sd_card_files } else { Serial.println(F("Wrong alarm !!!")); } } 4.Sprawdzenie czy karta sieciowa wysłała jakieś dane: Esp8266 i Arduino Nano komunikują się ze sobą poprzez UART. Wszystkie informacje przesyłane są w postaci komend rozpoczynających się od znaku 0x7E, dalej reszta komendy wraz z parametrami, suma kontrolna, znak 0x7F, 3 znaki nowej lini '\n'. Przykładowa komenda wysyłana przez Arduino do Esp: 0x7E, 'n', '1', 'o', 'n', '+', (wyliczona suma kontrolna), 0x7F, '\n', '\n', '\n' Funkcja sprawdzająca odebrane dane w karcie sieciowej oraz Arduino jest prawie taka sama, polega na nasłuchiwaniu czy jest coś nadawane. Jeżeli tak to funkcja zapisuje do buffera bajt po bajcie aż do otrzymania trzech znaków nowej lini '\n' pod rząd. Jeżeli otrzymała te 3 znaki, rozpoczyna liczenie sumy kontrolnej od początku komendy do ostatniego bajtu danych w komendzie. Następnie porównuje wyliczoną sumę kontrolną z odebraną sumą, jeżeli sumy zgadzają się (są takie same), to Arduino lub Esp wykonuje daną komendę, np. komenda (0x7E, 'n', '1', 'o', 'n', '+', (wyliczona suma kontrolna), 0x7F, '\n', '\n', '\n') odpowiada za ustawienie w aplikacji Blynk stanu przycisku nr. 1 na włączony (podczas włączenia przekaźnika 1 przy pomocy przycisku na obudowie, Arduino musi wysłać tą informacje do karty sieciowej, aby użytkownik miał podgląd na aktualny stan przekaźnika). void recive_data() { bool control_val = 1; espSerial.flush(); if (espSerial.available() && control_val == 1) { espSerial.flush(); //digitalWrite(13, 1); delay(3); r_array_counter = 0; r_array[r_array_counter] = '\n'; timestart = millis(); //byte character_counter = 0; bool detect_end = 0; byte add_val = 2; while (espSerial.available() && r_array_counter < 25 && (millis() - timestart) < 400 && control_val == 1) { delay(1); byte character = espSerial.read(); r_array[r_array_counter] = character; r_array_counter++; r_array[r_array_counter] = '\n'; if (character != '\n' && control_val == 1) { if (serial_reciv_debug == 1) { Serial.print(F("{")); Serial.print(character); Serial.print(F("}")); } } if (character == '\n' && r_array[r_array_counter - 3] == '\n' && r_array[r_array_counter - 2] == '\n' && r_array[r_array_counter - 4] == 0x7F) { detect_end = 1; } if (detect_end == 1 && control_val == 1) { detect_end = 0; r_array_counter -= 1; if (serial_reciv_debug == 1) { Serial.print(F(" [r_array_counter]-> ")); Serial.println(r_array_counter); Serial.println(F(" - new line symbol detect!")); } byte chk = 0; uint8_t i = 0; for (i = 0; i < r_array_counter - (2 + add_val); i++) { chk ^= r_array[i]; } Serial.print("Control sum = "); Serial.println(chk); if (r_array[0] == 0x7E && r_array[r_array_counter - (1 + add_val)] == 0x7F && (byte)r_array[r_array_counter - (2 + add_val)] == (byte)chk && control_val == 1) { if (serial_reciv_debug == 1) { Serial.println(F("Good control sum !")); } last_millis2 = 0; if (r_array[1] == 's' && (r_array[3] == '$' || r_array[3] == '#')) { switch (r_array[2]) { case '1': if (r_array[4] == 'o' && r_array[5] == 'n') { B_SW_state = 1; set_power_230V_1(1); B_SW.m_toggleState = 1; display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(1, false, 1, 1); display.setSegments(display_symbol_off, 2, 2); screen_timeout = 5000; } else if (r_array[4] == 'o' && r_array[5] == 'f') { B_SW_state = 0; set_power_230V_1(0); B_SW.m_toggleState = 0; display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(1, false, 1, 1); display.setSegments(display_symbol_on, 2, 2); screen_timeout = 5000; } else { } break; case '2': if (r_array[4] == 'o' && r_array[5] == 'n') { R_SW_state = 1; set_power_230V_2(1); R_SW.m_toggleState = 1; display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(2, false, 1, 1); display.setSegments(display_symbol_off, 2, 2); screen_timeout = 5000; } else if (r_array[4] == 'o' && r_array[5] == 'f') { R_SW_state = 0; set_power_230V_2(0); R_SW.m_toggleState = 0; display.clear(); display.setSegments(display_symbol_G, 1, 0); display.showNumberDec(2, false, 1, 1); display.setSegments(display_symbol_on, 2, 2); screen_timeout = 5000; } else { } break; } control_val = 0; } ... } } } 5.Sprawdzanie wydajności programu: Podczas pisania programu dla tego sterownika, musiałem sprawdzać wydajność danego rozwiązania, np. ile czasu zajmuje włączenie/wyłączenie jednej funkcji. Napisałem więc prostą funkcję, która sprawdza ile raz wykonują się wszystkie funkcje w funkcji main. Co sekundę Arduino wysyła na serial monitor informację ile razy wykonała się funkcja main. void framerate() { fr++; if (S != fr_sec) { Serial.print("##> FRAMERATE = "); Serial.print(fr); Serial.println(" fps <##"); fr = 0; fr_sec = S; } } 6.Funkcja wysyłające dane do karty sieciowej: Sterownik co 5 sekund wysyła do karty sieciowej informację o aktualnych temperaturach wody i powietrza oraz wilgotność. Wysyła je w formie komendy: 0x7E, '%', (wilgotność powietrza), 'T', (temp. powietrza liczba całkowita), (temp. powietrza liczba po przecinku), '@', (temp. wody liczba całkowita), (temp. wody liczba po przecinku), (suma kontrolna), 0x7F, '\n', '\n', '\n' Co 10 sekund wysyła dane oraz zapisuje stan wyjść w pamięci EEPROM, jeżeli są one inne od ostatnio zapisanych ( jest to potrzebne do działania funkcji przywracania stanów podczas wyłączenia sterownika). Wysyłane dane składają się z informacji o stanach wyjść sterownika (stan przekaźnika 1 i 2, wartość na pinie PWM). Komenda wygląda następująco: 0x7E, (stan przekaźnika 1), '&', (stan przekaźnika 2), '^', (wartość na pinie PWM przeskalowana od 0 do 10), '%', (suma kontrolna) , 0x7F, '\n', '\n', '\n' void execute_millis_functions() { if ((millis() - last_millis1) > 5000) { send_sensors_data(); last_millis1 = millis(); } if ((millis() - last_millis2) > 10000) { sync_data(); Serial.print(F("****> power_loos_counter = ")); Serial.println(power_loos_counter); if (B_SW_state != EEPROM.read(1001)) { //EEPROM.write(1001, B_SW_state); eeprom_optimalization(1001, B_SW_state); power_loos_counter++; } if (R_SW_state != EEPROM.read(1002)) { //EEPROM.write(1002, R_SW_state); eeprom_optimalization(1002, R_SW_state); power_loos_counter++; } if (G_SW_state != EEPROM.read(1003)) { //EEPROM.write(1003, G_SW_state); eeprom_optimalization(1003, G_SW_state); power_loos_counter++; } last_millis2 = millis(); } if (alarm_onoff == 0 && alarm_timer.isReady()) { alarm_onoff = 1; } if (timer1.isReady()) { timer_clock(); } } void sync_data() { //sync[9] = {0x7E, 0, '&', 0, '^', 0, '%', 0, 0x7F}; if (B_SW_state == 1) { data_2_sync[1] = 1; } else if (B_SW_state == 0) { data_2_sync[1] = 0; } else { } if (R_SW_state == 1) { data_2_sync[3] = 1; } else { data_2_sync[3] = 0; } if (G_SW_button_cycle == 0) { data_2_sync[5] = 0; } else if (G_SW_button_cycle == 1) { data_2_sync[5] = 2; } else if (G_SW_button_cycle == 2) { data_2_sync[5] = 4; } else if (G_SW_button_cycle == 3) { data_2_sync[5] = 6; } else if (G_SW_button_cycle == 4) { data_2_sync[5] = 8; } else if (G_SW_button_cycle == 5) { data_2_sync[5] = 10; } else { data_2_sync[5] = 0; } send_command(data_2_sync, 9); } KOD Sterownika (Zalecam wykorzystanie bibliotek dołączonych do tego artykułu, ponieważ zmieniałem w nich kilka rzeczy): aquarium_driver_v2.zip Działanie Karty Sieciowej (esp8266) void setup(): void setup() { Serial.begin(9600); EEPROM.begin(1000); //0-405 - alarms eeprom //507-998 - epprom variable space //1000-4096 - log eeprom space delay(50); pcf8574.pinMode(Wifi_status_led, OUTPUT); pcf8574.pinMode(Blynk_status_led, OUTPUT); pcf8574.pinMode(Connection_status_led, OUTPUT); pcf8574.pinMode(feeding_pin, OUTPUT); pcf8574.pinMode(retraction_pin, OUTPUT); pcf8574.pinMode(wifi_network_sw_a, INPUT_PULLUP); pcf8574.pinMode(wifi_network_sw_b, INPUT_PULLUP); pcf8574.begin(); pcf8574.digitalWrite(Wifi_status_led, OFF); pcf8574.digitalWrite(Blynk_status_led, OFF); pcf8574.digitalWrite(Connection_status_led, OFF); pcf8574.digitalWrite(feeding_pin, OFF); pcf8574.digitalWrite(retraction_pin, OFF); for (int i = 0; i < 880; i++) { message_aray[i] = 127; } feeding_time = EEPROM.read(510); //510 retraction_time = EEPROM.read(511); //511 feeding_today = EEPROM.read(509); //509 delay(100); set_network(); byte datalog_d, datalog_m, datalog_y; datalog_d = EEPROM.read(523); datalog_m = EEPROM.read(524); datalog_y = EEPROM.read(525); String dd_str = ""; if (datalog_d < 10) { dd_str = "0"; } dd_str += String(datalog_d); String dm_str = ""; if (datalog_m < 10) { dm_str = "0"; } dm_str += String(datalog_m); String dy_str = ""; if (datalog_y < 10) { dy_str = "0"; } dy_str += String(datalog_y); current_log_filename = "L" + dd_str + dm_str + dy_str + ".txt"; cmd.print("Log filename is "); cmd.println(current_log_filename); cmd.flush(); if (EEPROM.read(522) == 1) { EEPROM.write(522, 0); EEPROM.commit(); send_mail(0); } else if (EEPROM.read(522) == 2) { EEPROM.write(522, 0); EEPROM.commit(); send_mail(1); } else { } if (WiFi.status() == 6) { pcf8574.digitalWrite(Wifi_status_led, OFF); } unsigned long startWifi = millis(); WiFi.mode(WIFI_STA); WiFi.begin(E_ssid.c_str(), E_pass.c_str()); bool wifi_flag = false; while (WiFi.status() != WL_CONNECTED) { delay(100); pcf8574.digitalWrite(Wifi_status_led, wifi_flag); wifi_flag = !wifi_flag; if (millis() > startWifi + myWifiTimeout) { Serial.println("Time out"); break; } } Blynk.config(auth, server, port); checkBlynk(); timer.setInterval(functionInterval, myfunction); // run some function at intervals per functionInterval timer.setInterval(blynkInterval, checkBlynk); // check connection to server per blynkInterval timer.setInterval(500L, period_function); timer.setInterval(5000L, sync_alarms); // sync alarm && check_logs(); timer.setInterval(15000L, check_day_change); cmd.clear(); blynk_led.off(); setSyncInterval(1000); dp_network(); if (debug_log_string.length() > 0) { cmd.println(debug_log_string); cmd.flush(); } add_log(48, datalog_d, datalog_m, datalog_y); add_log(30, 1, 999, 999); check_day_change(); } Zainicjowanie UARTA oraz pamięci "EEPROM" esp8266 Ustawienie pinów modułu ekspandera pcf8574 (wejścia dla przełącznika do wyboru sieci WiFi, wyjścia do sterowania karmnikiem oraz dla diod LED wyświetlających stan połączenia z siecią WiFi, połączenia z serwerem aplikacji Blynk oraz potwierdzenie o otrzymaniu komendy od Arduino na magistrali UART) Odczytanie parametrów z pamięci "EEPROM" (karta sieciowa odczytuje: informacje potrzebne do działania karmnika) Odczytanie z przełącznika wybranej sieci wifi oraz wczytanie nazwy Sieci (SSID) oraz hasła do sieci Ustawienie nazwy pliku z logami sterownika Próba połączenia się z siecią Konfiguracja ustawień aplikacji Blynk i sprawdzenie połączenia z serwerem aplikacji Ustawienie Timerów Dodanie do logów informacji o starcie karty sieciowej ------------------------------------------------------------------------------------------------------------------------ 4.Sterownik może obsłużyć maksymalnie 3 sieci WiFi, 2 sieci użytkownika oraz jedną sieć serwisową. Na karcie sieciowej znajduje się przełącznik 3 pozycyjny, pierwsze dwie pozycje to sieci użytkownika, ostatnia, to sieć serwisowa. Po sprawdzeniu przez kartę sieciową stanu przełącznika, odczytuje z pamięci "EEPROM" nazwę sieci (SSID) oraz hasło, jeżeli wybrano 3 pozycję przełącznika, SSID ustawiane jest na SIEC_SERWISOWA oraz hasło SERWISANT123. Ma to na celu zapobieganie blokadzie karty sieciowej, przez źle podane hasło. Hasło do sieci WiFi można zmienić przy pomocy telefonu, służy do tego komenda add_wifi=(numer pod którym ma zostać zapisana nowa sieć, 1 lub 2)-(SSID maks 20 znaków),(Hasło maks 20 znaków) 5.Podczas załączania się karty sieciowej tworzona jest nazwa pliku, do którego będą zapisywane dane. Nazwa zawiera aktualną datę, np. L010321.txt, urządzenie zapisuję do tego pliku oraz pliku DATALOG.txt informację o działaniu sterownika oraz karty sieciowej w postaci logów. void loop(): void loop() { recive_data(); check_feeding(); if (millis() - repair_data_timer >= 10000 && EEPROM.read(526) == 1) { repair_data_timer = millis(); byte repair_number = check_is_data_ok(); if (repair_number != 0) { repair_data(repair_number); } } if (millis() >= pwm_timeout_time) { pwm_timeout = 0; } if (Blynk.connected()) { Blynk.run(); } timer.run(); if (millis() - one_hour_timer >= 60000 && one_hour_timer != 0) { //rechecking values one_hour_timer = 0; E_value_eeprom_Succes_write_counter_esp = 0; E_value_eeprom_Fail_write_counter_esp = 0; E_value_eeprom_Succes_write_counter_arduino = 0; E_value_eeprom_Fail_write_counter_arduino = 0; E_arduino_alarm_syncing_counter = 0; } } 1.Sprawdzenie czy otrzymano jakieś dane 2.Włączenie/wyłączenie karmnika 3.Sprawdzenie poprawności danych 4.Callback Blynk'a 5.Resetowanie kodów błędu po upłynięciu 1h ------------------------------------------------------------------------------------------------------------------------ 1. Funkcja ta działa tak samo jak funkcja sprawdzająca czy otrzymano dane z sterownika 2. Karta sieciowa odpowiada za sterowanie modułem karmnika, sterowany jest dwoma pinami z modułu pcf8574 podłączonego do Wemosa. Stan wysoki na pierwszym pinie załącza karmienie, stan wysoki na drugim pinie zaczyna cofać pokarm do zbiornika. 3. Ten element programu służy do sprawdzania, czy wszystko z danymi (np. stany wyjść, zmienne zapisane w "EEPROMIE" itp.) jest ok. Np. komórka 510 w pamięci "EEPROM" przechowuje czas podawania pokarmu, zakres to 0-29 sekund. Cały sterownik umożliwia zmianę wartości w pamięci EEPROM zdalnie przy pomocy komend, np. write_esp_eeprom=(komórka pamięci "EEPROM" w zakresie 0-1000),(wartość od 0 do 255) lub write_ard_eeprom=(komórka pamięci EEPROM w zakresie 0-1023),(wartość od 0 do 255). Przez przypadek mogę ustawić wartość w komórce 510 na 255, oznacza to, że karmnik będzie podawał pokarm przez ponad 4 minuty ! Jest to dość niebezpieczne dla ryb. Funkcja ta sprawdza więc czy wszystkie wpisane wartości w odpowiednich komórkach są poprawne, jeżeli nie, to sterownik informuje o tym użytkownika oraz stara się naprawić dane, np. ustawia wartość w komórce 510 na 2 sekundy. Sterownik może wysłać maila jeżeli usterka jest bardziej złożona, np. sterownik oraz karta sieciowa korzystają z pamięci EEPROM, pozwala ona na zachowanie wartości nawet po zaniku zasilania, niestety korzystanie z tej pamięci jest ograniczone ilością zapisów (średnio około 100 000), po tej wartości z czasem mogą pojawiać się problemy z pamięcią, może ona ulec uszkodzeniu. Napisałem funkcję która ma na celu kontrolowanie zużycia tej pamięci, void eeprom_optimalization(int E_case, int E_val2w, bool add2counter, bool safe_mode) { if (E_val2w > 255 || E_val2w < 0) { cmd.println("Value to write is out of range!"); add_log(31, E_case, E_val2w, 0); return; } if (safe_mode == 1) { //int eeprom_case_not2write[7] = {507, 508, 509, 510, 511, 512, 513}; for (int i = 0; i < eeprom_case_not2write_length; i++) { if (E_case == eeprom_case_not2write[i]) { cmd.println("This eeprom case cannot be written!"); cmd.flush(); add_log(31, E_case, E_val2w, 1); return; } } } else { for (int i = 0; i < eeprom_case_not2write_length; i++) { if (E_case == eeprom_case_not2write[i]) { cmd.println("This eeprom case is dangerous to written!"); cmd.flush(); break; } } } byte val2write = (byte)E_val2w; byte E_val_read = EEPROM.read(E_case); if (E_val_read != E_val2w) { EEPROM.write(E_case, val2write); if (add2counter == 1) { int r_e = ((EEPROM.read(514) << 8) + EEPROM.read(515)); r_e += 1; byte r_e_b = highByte(r_e); EEPROM.write(514, r_e_b); r_e_b = lowByte(r_e); EEPROM.write(515, r_e_b); cmd.print("Writting eeprom succesful counter = "); cmd.println(r_e); } add_log(31, E_case, E_val2w, 2); } else { if (add2counter == 1) { int r_e = ((EEPROM.read(516) << 8) + EEPROM.read(517)); r_e += 1; byte r_e_b = highByte(r_e); EEPROM.write(516, r_e_b); r_e_b = lowByte(r_e); EEPROM.write(517, r_e_b); cmd.print("Writting eeprom fail counter = "); cmd.println(r_e); } add_log(31, E_case, E_val2w, 3); } EEPROM.commit(); } Ma ona kilka ważnych według mnie funkcji: sprawdzenie czy podana wartość mieści się w zakresie 0-255 sprawdzenie czy podany numer komórki nie jest zapisany na liście z niebezpiecznymi komórkami do zapisu, jeżeli tak to albo odmówi zapisu w danym miejscu albo poinformuje użytkownika, że trzeba uważać na tą komórką pamięci sprawdzenie czy aktualna wartość w podanej komórce jest inna od podanej, jeżeli tak to następuje zapis oraz zwiększenie się licznika udanych zapisów Sterownik sprawdza czy ilość zapisów nie przekracza wartości maksymalnej, np. 1500 zapisów, jeżeli przekracza, to informuje o tym użytkownika wysyłając mu wiadomość email. 5. Jeżeli coś z danymi jest nie tak, to użytkownik może dostać wiadomość, nie chcę jej jednak otrzymywać często, wystarczy przypomnienie np. co godzinę. Dlatego karta sieciowa ustawia jedno godzinny time out na wysyłanie wiadomości, jeżeli w czasie jednej godziny użytkownik nie naprawi usterki (nie zresetuje przy pomocy komendy licznika zapisów) to wyśle on kolejną wiadomość. Obsługa komend przez kartę sieciową: Podczas budowy i programowania tego urządzenia miałem na celu stworzenie czegoś w rodzaju zdalnego zarządzania całym urządzeniem, obecnie sterownik pracuje u dziadka dlatego muszę mieć możliwość kontrolowania jego działania zdalnie. Dla przykładu, jedyną opcją na dodanie alarmu jest wpisanie go w formie komendy na telefonie. Plusem tego rozwiązania jest to, że nie muszę jechać do dziadka aby zmienić np. godzinę włączenia światła itp., mam także podgląd na to czy wszystko działa. Komendy wprowadzane są w widżecie Terminal w aplikacji Blynk, kod obsługujący komendy: BLYNK_WRITE(V6) { String cmd_request = " "; cmd_request = param.asStr(); if (String("cls") == cmd_request) { cmd.clear(); add_log(51, 1, 999, 999); } else if (cmd_request.startsWith("show_alarms=") || cmd_request.startsWith("sha=")) { uint8_t index_val_1 = cmd_request.indexOf('='); uint8_t cmd_val = cmd_request.substring(index_val_1 + 1).toInt(); show_all_alarms[3] = cmd_val + '0'; send_command(show_all_alarms, 8); add_log(52, cmd_val, 999, 999); } else if (String("alarms_counter") == cmd_request || String("ac") == cmd_request) { int e_count = 0, n = 0; while (n <= ile_alarmow) { if (EEPROM.read((n * 5) + 1) != 127) { e_count++; } n++; } cmd.print("Esp alarms: "); cmd.println(e_count); send_command(show_alarms_counter, 8); add_log(53, e_count, 999, 999); } else if (cmd_request.startsWith("display_alarm=") || cmd_request.startsWith("da=")) { uint8_t index_val_1 = cmd_request.indexOf('='); uint8_t ter_val = cmd_request.substring(index_val_1 + 1).toInt(); if (ter_val <= ile_alarmow) { cmd.print("Displayed alarm "); cmd.print(ter_val); cmd.println(" :"); cmd.print("-Esp: "); cmd.flush(); int p = ter_val * 5; cmd.print("f(x): "); cmd.print(EEPROM.read(p + 4)); cmd.print("|D: "); cmd.print(EEPROM.read(p + 1)); cmd.print("|H: "); cmd.print(EEPROM.read(p + 2)); cmd.print("|MIN: "); cmd.print(EEPROM.read(p + 3)); cmd.print("|X: "); cmd.println(EEPROM.read(p + 5)); cmd.flush(); show_alarm[4] = ter_val + '0'; send_command(show_alarm, 8); } else { cmd.print("Wrong alarm case (0-"); cmd.print(ile_alarmow); cmd.println(")"); cmd.flush(); } add_log(54, ter_val, 999, 999); } else if (cmd_request.startsWith("set_alarm=") || cmd_request.startsWith("sa=")) { int eep_szuf = 1; int eep_buff = 1; uint8_t eep_a, eep_b, eep_c, eep_d, eep_f; uint8_t index_val_1, index_val_2, index_val_3, index_val_4, index_val_5, index_val_6; int cmd_val_1, cmd_val_2, cmd_val_3, cmd_val_4, cmd_val_5, cmd_val_6; index_val_1 = cmd_request.indexOf('='); index_val_2 = cmd_request.indexOf(','); index_val_3 = cmd_request.indexOf('-'); index_val_4 = cmd_request.indexOf(':'); index_val_5 = cmd_request.indexOf('&'); index_val_6 = cmd_request.indexOf('*'); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); cmd_val_2 = cmd_request.substring(index_val_2 + 1).toInt(); cmd_val_3 = cmd_request.substring(index_val_3 + 1).toInt(); cmd_val_4 = cmd_request.substring(index_val_4 + 1).toInt(); cmd_val_5 = cmd_request.substring(index_val_5 + 1).toInt(); cmd_val_6 = cmd_request.substring(index_val_6 + 1).toInt(); if (cmd_request[index_val_1] == '=' && cmd_request[index_val_2] == ',' && cmd_request[index_val_3] == '-' && cmd_request[index_val_4] == ':' && cmd_request[index_val_5] == '&' && cmd_request[index_val_6] == '*') { if ((cmd_val_2 >= 0 && cmd_val_2 < 9) && (cmd_val_3 > -1 && cmd_val_3 < 24) && (cmd_val_4 > -1 && cmd_val_4 <= 60)) { if (cmd_val_1 == 127) { cmd.print("[auto-case]-"); for (int w = 0; w <= ile_alarmow; w++) { if (EEPROM.read((w * 5) + 1) == 127) { eeprom_optimalization(0, w, 1, 0); byte x = EEPROM.read(0); cmd.print("Setting auto alarm value to "); cmd.println(x); cmd.flush(); //EEPROM.commit(); eep_szuf = x * 5; eep_buff = eep_szuf / 5; break; } } } else { if (cmd_val_1 >= 0 && cmd_val_1 <= ile_alarmow) { cmd.print("Setting manual alarm value to "); cmd.println(cmd_val_1); eep_szuf = cmd_val_1 * 5; eep_buff = eep_szuf / 5; } else { cmd.print("Wrong manual case value!"); return; } } cmd.println(); cmd.flush(); cmd.print("Setting alarm "); cmd.print(eep_buff); cmd.print(" on the day "); cmd.print(cmd_val_2); cmd.print(" ("); cmd.print(cmd_val_3); cmd.print(":"); cmd.print(cmd_val_4); cmd.print(") - function: "); cmd.print(cmd_val_5); cmd.print(" with value: "); cmd.println(cmd_val_6); cmd.flush(); eeprom_optimalization(eep_szuf + 1, cmd_val_2, 1, 0); eeprom_optimalization(eep_szuf + 2, cmd_val_3, 1, 0); eeprom_optimalization(eep_szuf + 3, cmd_val_4, 1, 0); eeprom_optimalization(eep_szuf + 4, cmd_val_5, 1, 0); eeprom_optimalization(eep_szuf + 5, cmd_val_6, 1, 0); //EEPROM.commit(); set_alarm[2] = cmd_val_2 + '0'; set_alarm[3] = cmd_val_3 + '0'; set_alarm[4] = cmd_val_4 + '0'; set_alarm[6] = cmd_val_5 + '0'; set_alarm[7] = cmd_val_6 + '0'; set_alarm[9] = eep_buff + '0'; send_command(set_alarm, 13); } else { cmd.println("Wrong time !"); cmd.flush(); } } else { cmd.println("Wrong command structure!"); cmd.flush(); } add_log(26, eep_buff, cmd_val_5, cmd_val_6); } else if (String("alarm_help") == cmd_request || String("ah") == cmd_request) { cmd.clear(); cmd.println("write: set_alarm=y,D-H:M&F*X$A"); String y_nr_str = "y - nr. szufladki (127 = auto, 0-" + String(ile_alarmow) + " counter)"; cmd.println(y_nr_str); cmd.println("D - dzien (1-7) 8=all"); cmd.println("H - godzina (0-23)"); cmd.println("M - minuta (0-59)"); cmd.println("F - funkcja (1-6)[alarm_function=help]"); cmd.println("x - wartosc funkcji"); cmd.println(""); cmd.flush(); cmd.println("______________"); cmd.println("F: 1 - gniazdo 1 [1 or 0] (pompa)"); cmd.println(" 2 - gniazdo 2 [1 or 0]"); cmd.println(" 3 - pasek led [0 - 100]"); cmd.println(" 4 - karmnik [none]"); cmd.flush(); cmd.println(" 5 - auto time syncing [none]"); cmd.println(" 6 - display brightness [0 - 3]"); cmd.println(" 7 - reset device [0-2:arduino/esp/all]"); cmd.flush(); add_log(55, 1, 999, 999); } else if (cmd_request.startsWith("rm_alarm=") || cmd_request.startsWith("rma=")) { uint8_t index_val_1; int cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); if (cmd_val_1 < 0 || cmd_val_1 >= ile_alarmow) { cmd.print("Wrong alarm number - range(0-") ; cmd.print(ile_alarmow); cmd.println(")"); add_log(56, cmd_val_1, 0, 999); } else { cmd.print("Removing alarm nr. "); cmd.println(cmd_val_1); rm_single_alarm[3] = cmd_val_1 + '0'; int n = cmd_val_1; n = n * 5; eeprom_optimalization(n + 1, 127, 1, 0); eeprom_optimalization(n + 2, 127, 1, 0); eeprom_optimalization(n + 3, 127, 1, 0); eeprom_optimalization(n + 4, 127, 1, 0); eeprom_optimalization(n + 5, 127, 1, 0); //EEPROM.commit(); send_command(rm_single_alarm, 8); add_log(56, cmd_val_1, 1, 999); } } else if (String("rm_all_alarm") == cmd_request) { send_command(rm_all_alarms, 8); for (int n = 1; n <= ((ile_alarmow + 1) * 5) - 1; n++) { eeprom_optimalization(n, 127, 0, 0); //EEPROM.commit(); if (n % 10 == 0) { cmd.print("Removed case from "); cmd.print(n - 10); cmd.print(" to "); cmd.println(n); } } eeprom_optimalization(0, 0, 1, 0); //EEPROM.commit(); add_log(57, 1, 999, 999); } else if (String("millis_usage") == cmd_request || String("mu") == cmd_request) { float x, z; cmd.println(); cmd.print("ESP8266: "); x = millis() / (1000 * 60); int millis_esp = millis() / (1000 * 60); cmd.print(millis() / (1000 * 60)); cmd.print(" min. / 71,580 min. - "); z = x / 71580; cmd.print(z, 3); cmd.println(" %"); send_command(disp_millis, 6); add_log(58, millis_esp, 999, 999); } else if (cmd_request.startsWith("read_ard_eeprom") || cmd_request.startsWith("rae")) { //eeprom_action[11] = {0x7E, 'E', 0, 'P', '.', 0, 0, 'm', 0, 'A', 0, 0x7F}; uint8_t index_val_1; int cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); if (cmd_val_1 > 1023 || cmd_val_1 < 0) { cmd.println("Wrong value (0-1023)"); add_log(59, cmd_val_1, 0, 999); } else { byte Bdata_a = 0, Bdata_b = 0; Bdata_a = highByte(cmd_val_1); Bdata_b = lowByte(cmd_val_1); cmd.print("highByte = "); cmd.print(Bdata_a); cmd.print(" - lowByte = "); cmd.println(Bdata_b); //eeprom_action[12] = {0x7E, 'E', 0, 'P', '.', 0, 0, 'm', 0, 'A', 0, 0x7F}; eeprom_action[2] = 1 + '0'; eeprom_action[5] = Bdata_a; eeprom_action[6] = Bdata_b; eeprom_action[8] = 0 + '0'; send_command(eeprom_action, 12); add_log(59, cmd_val_1, 1, 999); } } else if (cmd_request.startsWith("write_ard_eeprom=") || cmd_request.startsWith("wae=")) { uint8_t index_val_1, index_val_2; int cmd_val_1, cmd_val_2; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); index_val_2 = cmd_request.indexOf(','); cmd_val_2 = cmd_request.substring(index_val_2 + 1).toInt(); if (cmd_val_1 > 1023 || cmd_val_1 < 0 || cmd_val_2 > 255 || cmd_val_2 < 0) { cmd.println("Wrong value (val1: 0-1023 val2: 0-255)"); add_log(60, cmd_val_1, cmd_val_2, 0); } else { byte Bdata_a = 0, Bdata_b = 0; Bdata_a = highByte(cmd_val_1); Bdata_b = lowByte(cmd_val_1); cmd.print("highByte = "); cmd.print(Bdata_a); cmd.print(" - lowByte = "); cmd.println(Bdata_b); //eeprom_action[12] = {0x7E, 'E', 0, 'P', '.', 0, 0, 'm', 0, 'A', 0, 0x7F}; cmd.print("Writening ard eeprom case "); cmd.print(cmd_val_1); cmd.print(" with value of "); cmd.println(cmd_val_2); eeprom_action[2] = 2 + '0'; eeprom_action[5] = Bdata_a; eeprom_action[6] = Bdata_b; eeprom_action[8] = cmd_val_2; send_command(eeprom_action, 12); add_log(60, cmd_val_1, cmd_val_2, 1); } } else if (cmd_request.startsWith("sync_time=")) { //set_time[12] = {0x7E, 'T', 0, 0, 0, '/', 0, 0, 0, 's' 0, 0x7F}; //H:Min-D/M.Y uint8_t i_val_1, i_val_2, i_val_3, i_val_4, i_val_5; int val_1, val_2, val_3, val_4, val_5; i_val_1 = cmd_request.indexOf('='); val_1 = cmd_request.substring(i_val_1 + 1).toInt(); i_val_2 = cmd_request.indexOf(':'); val_2 = cmd_request.substring(i_val_2 + 1).toInt(); i_val_3 = cmd_request.indexOf('-'); val_3 = cmd_request.substring(i_val_3 + 1).toInt(); i_val_4 = cmd_request.indexOf('/'); val_4 = cmd_request.substring(i_val_4 + 1).toInt(); i_val_5 = cmd_request.indexOf('.'); val_5 = cmd_request.substring(i_val_5 + 1).toInt(); String currentTime = String(val_1) + ":" + String(val_2) + ":" + String(second()); String currentDate = String(val_3) + " " + String(val_4) + " " + String(val_5 + 2000); cmd.print(currentTime); cmd.print(" - "); cmd.println(currentDate); cmd.flush(); set_time[2] = val_1 + '0'; set_time[3] = val_2 + '0'; set_time[4] = second() + '0'; set_time[6] = val_3 + '0'; set_time[7] = val_4 + '0'; set_time[8] = val_5 + '0'; send_command(set_time, 12); add_log(61, val_1, val_2, val_3); } else if (String("auto_sync_time") == cmd_request || String("ast") == cmd_request) { String currentTime = String(hour()) + ":" + minute() + ":" + second(); String currentDate = String(day()) + " " + month() + " " + year(); cmd.print(currentTime); cmd.print(" - "); cmd.println(currentDate); set_time[2] = hour() + '0'; set_time[3] = minute() + '0'; set_time[4] = second() + '0'; set_time[6] = day() + '0'; set_time[7] = month() + '0'; set_time[8] = year() - 2000 + '0'; send_command(set_time, 12); add_log(62, hour(), minute(), day()); } else if (cmd_request.startsWith("Arduino_info=") || cmd_request.startsWith("ai=")) { uint8_t index_val_1; int cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); ard_info[3] = (byte)cmd_val_1 + '0'; send_command(ard_info, 8); add_log(63, cmd_val_1, 999, 999); } else if (String("arduino_info_help") == cmd_request || String("aih") == cmd_request) { cmd.println("1 - show arduino alarm syncing counter"); cmd.println("2 - print arduino eeprom in serial monitor"); cmd.println("3 - reset alarm syncing counter"); cmd.println("4 - show arduino eeprom succesful writing counter"); cmd.flush(); cmd.println("5 - show arduino eeprom fail writing counter"); cmd.println("6 - reset arduino eeprom succesful writing counter"); cmd.println("7 - reset arduino eeprom fail writing counter"); cmd.flush(); cmd.println("8 - reset arduino eeprom succesful and fail writing counter"); cmd.flush(); add_log(64, 1, 999, 999); } else if (cmd_request.startsWith("read_esp_alarm") || cmd_request.startsWith("rea")) { uint8_t index_val_1; int cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); int n = 0; int p = 0; byte ac_2_log = 0; byte log_score = (byte)cmd_val_1; if (cmd_val_1 == 0 || cmd_val_1 == 1) { }else{ cmd.println("Wrong argument (0 or 1)"); cmd.flush(); add_log(65, log_score, ac_2_log, 999); return; } while (n <= ile_alarmow) { if (cmd_val_1 == 0) { if (EEPROM.read((n * 5) + 1) != 127 && n <= ile_alarmow) { p = n * 5; cmd.print("Szuf."); cmd.print(n); cmd.print(" ->f(x): "); cmd.print(EEPROM.read(p + 4)); //1 cmd.print(" |D: "); cmd.print(EEPROM.read(p + 1)); //2 cmd.print("|H: "); cmd.print(EEPROM.read(p + 2)); //3 cmd.print("|MIN: "); cmd.print(EEPROM.read(p + 3)); //4 cmd.print("|X: "); cmd.println(EEPROM.read(p + 5)); //5 cmd.flush(); ac_2_log += 1; } } else { p = n * 5; cmd.print("Szuf."); cmd.print(n); cmd.print(" ->f(x): "); cmd.print(EEPROM.read(p + 1)); cmd.print(" |D: "); cmd.print(EEPROM.read(p + 2)); cmd.print("|H: "); cmd.print(EEPROM.read(p + 3)); cmd.print("|MIN: "); cmd.print(EEPROM.read(p + 4)); cmd.print("|X: "); cmd.println(EEPROM.read(p + 5)); cmd.flush(); ac_2_log += 1; } n++; } add_log(65, log_score, ac_2_log, 999); } else if (String("rm_esp_all_alarm") == cmd_request) { int n = 0; for (n = 1; n <= ((ile_alarmow + 1) * 5) - 1; n++) { eeprom_optimalization(n, 127, 0, 0); //EEPROM.commit(); if (n % 10 == 0) { cmd.print("Removed case from "); cmd.print(n - 10); cmd.print(" to "); cmd.println(n); } } //EEPROM.commit(); add_log(66, 1, 999, 999); } else if (cmd_request.startsWith("write_esp_eeprom=") || cmd_request.startsWith("wee=")) { uint8_t index_val_1; int cmd_val_1; uint8_t index_val_2; int cmd_val_2; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); index_val_2 = cmd_request.indexOf(','); cmd_val_2 = cmd_request.substring(index_val_2 + 1).toInt(); if (cmd_val_1 > -1 && cmd_val_1 < 1000 && cmd_val_2 > -1 && cmd_val_2 < 256) { eeprom_optimalization(cmd_val_1, cmd_val_2, 0, 0); //EEPROM.commit(); } else { cmd.print("Error value! Val1 = Range (0-1000) / Val2 = Range(0-255)"); cmd.flush(); add_log(67, cmd_val_1, cmd_val_2, 0); return; } cmd.print("Writening to Esp eeprom case "); cmd.print(cmd_val_1); cmd.print(" value "); cmd.println(cmd_val_2); cmd.flush(); add_log(67, cmd_val_1, cmd_val_2, 1); } else if (cmd_request.startsWith("read_esp_eeprom=") || cmd_request.startsWith("ree=")) { uint8_t index_val_1; int cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); if (cmd_val_1 > -1 && cmd_val_1 < 1000) { cmd.print("Reading Esp eeprom case "); cmd.print(cmd_val_1); cmd.print(" = "); cmd.println(EEPROM.read(cmd_val_1)); //EEPROM.commit(); cmd.flush(); add_log(68, cmd_val_1, EEPROM.read(cmd_val_1), 1); } else { cmd.println("Wrong value range(0-1000)!"); cmd.flush(); add_log(68, cmd_val_1, 0, 0); } } else if (String("clear_arduino_eeprom") == cmd_request) { cmd.println("Clearing Arduino eeprom with value 127"); cmd.flush(); send_command(clear_ard_eeprom, 8); add_log(69, 1, 999, 999); } else if (String("clear_esp_eeprom") == cmd_request) { for (int n = 0; n < 4096; n++) { eeprom_optimalization(n, 127, 0, 1); } //EEPROM.commit(); add_log(70, 1, 999, 999); } else if (String("what's_time") == cmd_request || String("wt") == cmd_request) { //what_is_time[9] = {0x7E, 'W', 'H', 'a', '|', 'S', 0, 0x7F}; send_command(what_is_time, 8); add_log(71, 1, 999, 999); } else if (String("debug_help") == cmd_request || String("dh") == cmd_request) { cmd.clear(); cmd.flush(); cmd.println("Debuging option: "); cmd.println("write: debug_mode=x"); cmd.println(); cmd.println("______________"); cmd.println(" [0]-disable all debuging info"); cmd.flush(); cmd.println(" [1]-enable recive data debug"); cmd.println(" [2]-enable check control sum debug"); cmd.println(" [3]-enable send command debug"); cmd.println(" [4]-enable sync alarms debug"); cmd.println(" [5]-enable thingspeak data debug"); cmd.flush(); cmd.println(" [6]-enable feeding info debug"); cmd.println(" [7]-enable data about syncing pwm value"); cmd.println(" [8]-enable show free ram space"); cmd.println(" [9]-enable show add log result"); cmd.flush(); cmd.println(" [10]-enable check data is ok debug"); cmd.println(" [11]-enable send check is new day command debug"); cmd.flush(); /* bool D_recive_data_single_char = 0; bool D_check_control_sum = 0; bool D_sync_alarms = 0; bool D_send_command = 0; */ add_log(72, 1, 999, 999); } else if (cmd_request.startsWith("debug_mode") || cmd_request.startsWith("dm")) { uint8_t index_val_1; int cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); byte D_ok = 1; if (cmd_val_1 == 0) { D_recive_data_single_char = 0; D_check_control_sum = 0; D_sync_alarms = 0; D_send_command = 0; D_thingspeak_info = 0; D_feeding_info = 0; D_pwm_syncing_value = 0; D_show_ram = 0; D_add_log = 0; D_check_is_data_ok = 0; D_check_is_data_error = 0; D_checkday_change = 0; } else if (cmd_val_1 == 1) { D_recive_data_single_char = 1; } else if (cmd_val_1 == 2) { D_check_control_sum = 1; } else if (cmd_val_1 == 3) { D_send_command = 1; } else if (cmd_val_1 == 4) { D_sync_alarms = 1; } else if (cmd_val_1 == 5) { D_thingspeak_info = 1; } else if (cmd_val_1 == 6) { D_feeding_info = 1; } else if (cmd_val_1 == 7) { D_pwm_syncing_value = 1; } else if (cmd_val_1 == 8) { D_show_ram = 1; } else if (cmd_val_1 == 9) { D_add_log = 1; } else if (cmd_val_1 == 10) { D_check_is_data_ok = 1; D_check_is_data_error = 1; } else if(cmd_val_1 == 11) { D_checkday_change = 1; } else { cmd.println("Wrong argument!"); cmd.flush(); D_ok = 0; } add_log(73, cmd_val_1, D_ok, 999); } else if (String("feed_info") == cmd_request || String("fi") == cmd_request) { feeding_time = EEPROM.read(510); //510 retraction_time = EEPROM.read(511); //511 feeding_today = EEPROM.read(509); //509 F_pump_pin = EEPROM.read(508); cmd.print("1. Today feeding "); cmd.print(feeding_today); cmd.println(" times"); cmd.println(); cmd.flush(); cmd.println("2. Feeding settings: "); cmd.print(" a) Feeding time = "); cmd.print(feeding_time); cmd.println(" s"); cmd.print(" b) Retraction time = "); cmd.print(retraction_time); cmd.println(" s"); cmd.flush(); cmd.print(" c) Max fedding per day = "); cmd.println(max_daily_feeding_counter); cmd.flush(); cmd.print(" d) OFF this socket, when feeding (0-none/1-blue/2-red/3-all)= "); cmd.println(F_pump_pin); cmd.flush(); cmd.print(" e) time to turn on socket (min)= "); cmd.println(time_to_return); cmd.flush(); cmd.print(" f) feeding status: "); if (EEPROM.read(513) == 0) { cmd.println("Ok"); } else if (EEPROM.read(513) == 1) { cmd.println("No feeding today"); } else if (EEPROM.read(513) == 2) { cmd.println("Feeding is permanently Off"); } else { cmd.println("Error with value! out of range(0-2)"); add_log(74, 3, 999, 999); return; } cmd.flush(); add_log(74, EEPROM.read(513), 999, 999); } else if (cmd_request.startsWith("feed_sett=") || cmd_request.startsWith("fs=")) { uint8_t index_val_1; uint8_t cmd_val_1; uint8_t index_val_2; uint8_t cmd_val_2; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); index_val_2 = cmd_request.indexOf(','); cmd_val_2 = cmd_request.substring(index_val_2 + 1).toInt(); if (cmd_val_1 == 1) { if (cmd_val_2 > 0 && cmd_val_2 < 30) { eeprom_optimalization(510, cmd_val_2, 1, 0); //EEPROM.commit(); feeding_time = EEPROM.read(510); cmd.print("Setting feeding time to "); cmd.print(feeding_time); cmd.println(" s"); cmd.flush(); add_log(75, 1, cmd_val_2, 1); } else { cmd.print("Wrong argument 1s - 29s"); cmd.flush(); add_log(75, 1, cmd_val_2, 0); } } else if (cmd_val_1 == 2) { if (cmd_val_2 > 0 && cmd_val_2 < 30) { eeprom_optimalization(511, cmd_val_2, 1, 0); //EEPROM.commit(); retraction_time = EEPROM.read(511); cmd.print("Setting retraction time to "); cmd.print(retraction_time); cmd.println(" s"); cmd.flush(); add_log(75, 2, cmd_val_2, 1); } else { cmd.print("Wrong argument 1s - 29s"); cmd.flush(); add_log(75, 2, cmd_val_2, 0); } } else if (cmd_val_1 == 3) { bool add_log75 = 1; if (cmd_val_2 == 1) { cmd.println("Setting socket number to blue, number 1"); eeprom_optimalization(508, 1, 1, 0); //EEPROM.commit(); F_pump_pin = EEPROM.read(508); } else if (cmd_val_2 == 2) { cmd.println("Setting socket number to red, number 2"); eeprom_optimalization(508, 2, 1, 0); //EEPROM.commit(); F_pump_pin = EEPROM.read(508); } else if (cmd_val_2 == 3) { cmd.println("Setting socket number to red and blue, number 3"); eeprom_optimalization(508, 3, 1, 0); //EEPROM.commit(); F_pump_pin = EEPROM.read(508); } else if (cmd_val_2 == 0) { cmd.println("Setting socket number to none, number 0"); eeprom_optimalization(508, 0, 1, 0); //EEPROM.commit(); F_pump_pin = EEPROM.read(508); } else { cmd.println("Error value! range(1-blue / 2-red / 3-red and blue / 0-none)"); add_log75 = 0; } add_log(75, 3, cmd_val_2, add_log75); } else if (cmd_val_1 == 4) { bool add_log75 = 1; if (cmd_val_2 > 0 && cmd_val_2 <= 60) { eeprom_optimalization(507, cmd_val_2, 1, 0); //EEPROM.commit(); time_to_return = EEPROM.read(507); cmd.print("Setting time_to_return value to "); cmd.print(time_to_return); cmd.println(" min"); } else { cmd.println("Wrong value. range(1-60)"); add_log75 = 0; } cmd.flush(); add_log(75, 4, cmd_val_2, add_log75); } else { cmd.println("Wrong function number: from 1 to 4"); cmd.flush(); add_log(75, 5, 999, 999); } } else if (String("feed_sett_info") == cmd_request || String("fsi") == cmd_request) { cmd.clear(); cmd.println(" feed_sett=x,y"); cmd.println(""); cmd.println("---------------------------------"); cmd.flush(); cmd.println(""); cmd.println("--> 1 - setting feeding time in seconds (y = 1-60)"); cmd.println(""); cmd.println("--> 2 - setting retraction time in seconds (y = 1-60)"); cmd.println(""); cmd.println("--> 3 - setting socket color to off when feeding (y: 0= none | 1= blue | 2= red | 3= all)"); cmd.flush(); add_log(76, 1, 999, 999); } else if (cmd_request.startsWith("set_display_brightnes=") || cmd_request.startsWith("sdb=")) { uint8_t index_val_1; uint8_t cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); if (cmd_val_1 > -1 && cmd_val_1 <= 3) { cmd.print("Setting display brightness to "); cmd.println(cmd_val_1); cmd.flush(); //set_disp_brightnes[8] = {0x7E, 'S', 'd', 0, 'B', '?', 0, 0x7F}; set_disp_brightnes[3] = cmd_val_1 + '0'; send_command(set_disp_brightnes, 8); add_log(77, cmd_val_1, 1, 999); } else { cmd.println("Wrong value - range(0-3)"); cmd.flush(); add_log(77, 4, 0, 999); } } else if (cmd_request.startsWith("reset_device=")) { uint8_t index_val_1; uint8_t cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); switch (cmd_val_1) { case 0: cmd.println("Resetting arduino..."); send_command(reset_ard, 9); add_log(78, cmd_val_1, 1, 999); break; case 1: cmd.println("Resetting esp..."); save_data(1); //save_data(0); delay(100); add_log(78, cmd_val_1, 1, 999); ESP.restart(); break; case 2: cmd.println("Resetting arduino and esp..."); send_command(reset_ard, 9); save_data(1); //save_data(0); delay(100); add_log(78, cmd_val_1, 1, 999); ESP.restart(); break; default: cmd.println("Wrong value! Range(0-2)(arduino/esp/all)"); add_log(78, 3, 0, 999); return; } } else if (cmd_request.startsWith("add_wifi=")) { uint8_t index_val_1 = cmd_request.indexOf('='); uint8_t cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); uint8_t index_val_2 = cmd_request.indexOf('-'); uint8_t index_val_3 = cmd_request.indexOf(','); String cmd_val_2 = cmd_request.substring(index_val_2 + 1, index_val_3); String cmd_val_3 = cmd_request.substring(index_val_3 + 1); if (cmd_val_1 == 1 || cmd_val_1 == 2) { int offset = 600 + (25 * (cmd_val_1 - 1)) + (cmd_val_1 - 1); for (int i = 0; i <= 25; i++) { eeprom_optimalization(offset + i, 0, 0, 1); } //EEPROM.commit(); byte len = cmd_val_2.length(); for (int i = 0; i < len; i++) { eeprom_optimalization(offset + i, cmd_val_2[i], 0, 0); } if (EEPROM.commit() == 1) { cmd.println("Writting ssid - OK!"); } else { cmd.println("Writting ssid - ERROR!"); add_log(79, cmd_val_1, 2, 999); return; } offset = 600 + (25 * (cmd_val_1 + 1)) + (cmd_val_1 + 1); for (int i = 0; i <= 25; i++) { eeprom_optimalization(offset + i, 0, 0, 0); } //EEPROM.commit(); len = cmd_val_3.length(); for (int i = 0; i < len; i++) { eeprom_optimalization(offset + i, cmd_val_3[i], 0, 0); } if (EEPROM.commit() == 1) { cmd.println("Writting pass - OK!"); } else { cmd.println("Writting pass - ERROR!"); add_log(79, cmd_val_1, 2, 999); return; } add_log(79, cmd_val_1, 1, 999); } else { cmd.println("Wrong value! Range(1-2)"); add_log(79, cmd_val_1, 0, 999); } } else if (cmd_request.startsWith("display_wifi=") || cmd_request.startsWith("dw=")) { uint8_t index_val_1 = cmd_request.indexOf('='); int cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); if (cmd_val_1 == 1 || cmd_val_1 == 2) { cmd.print("E_ssid->"); cmd.print(read_eeprom_wifi(cmd_val_1 - 1)); cmd.print("<- \ E_pass->"); cmd.print(read_eeprom_wifi(cmd_val_1 + 1)); cmd.println("<-"); cmd.flush(); } else if (cmd_val_1 == 0) { cmd.print("E_ssid->"); cmd.print(network0_E_ssid); cmd.print("<- \ E_pass->"); cmd.print(network0_E_pass); cmd.println("<-"); cmd.flush(); } else { cmd.println("Wrong value! Range(0-2)"); add_log(80, cmd_val_1, 0, 999); return; } add_log(80, cmd_val_1, 1, 999); } else if (String("Clear_all_wifi=AquaDzz@iadek19") == cmd_request) { for (int i = 600; i <= 703; i++) { eeprom_optimalization(i, 0, 0, 1); } //EEPROM.commit(); add_log(81, 1, 999, 999); } else if (String("daw") == cmd_request || String("display_all_wifi") == cmd_request) { for (int i = 0; i < 3; i++) { cmd.print(i); cmd.print("["); for (int j = 0; j <= 25; j++) { cmd.print((char)(EEPROM.read(600 + (25 * i) + i + j))); if (j != 25) { cmd.print(","); } } cmd.println("]"); cmd.flush(); } add_log(82, 1, 999, 999); } else if (cmd_request.startsWith("auto_feeding_off=") || cmd_request.startsWith("afo=")) { uint8_t index_val_1 = cmd_request.indexOf('='); byte cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); if (cmd_val_1 >= 0 && cmd_val_1 <= 2) { eeprom_optimalization(513, cmd_val_1, 1, 0); //EEPROM.commit(); if (cmd_val_1 == 0) { cmd.println("This function is off, feeding every day"); } else if (cmd_val_1 == 1) { cmd.println("feeding today is off, next feeding tomorrow"); } else if (cmd_val_1 == 2) { cmd.println("feeding every day is off, next feeding when this function will be off"); } else { cmd.println("Error"); add_log(83, cmd_val_1, 0, 999); return; } cmd.flush(); add_log(83, cmd_val_1, 1, 999); } else { cmd.println("Wrong value! range(0-2: no/one_day/every_day)"); add_log(83, 3, 0, 999); } } else if (cmd_request.startsWith("set_light_speed=") || cmd_request.startsWith("sls=")) { uint8_t index_val_1; uint8_t cmd_val_1; uint8_t index_val_2; uint8_t cmd_val_2; uint8_t sval_1 = 127; uint8_t sval_2 = 127; uint8_t sval_3 = 127; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1).toInt(); index_val_2 = cmd_request.indexOf(','); cmd_val_2 = cmd_request.substring(index_val_2 + 1).toInt(); if (cmd_val_1 >= 1 && cmd_val_1 <= 4) { if (cmd_val_2 >= 0 && cmd_val_2 <= 255 && cmd_val_2 != 127) { //-------------------------------0----1----2---3---4---5---6----7---8---9---10--11-- //send_light_time_value[12] = {0x7E, 'L', 'I', 0, 'Q', 0, 'H', '|', 0, '@', 0, 0x7F}; if (cmd_val_1 == 1) { sval_1 = cmd_val_2; cmd.print("Setting led_step_button value to "); cmd.println(sval_1); } else if (cmd_val_1 == 2) { sval_2 = cmd_val_2; cmd.print("Setting led_step_app value to "); cmd.println(sval_2); } else if (cmd_val_1 == 3) { sval_3 = cmd_val_2; cmd.print("Setting led_step_alarm value to "); cmd.println(sval_3); } else if (cmd_val_1 == 4) { sval_1 = cmd_val_2; sval_2 = cmd_val_2; sval_3 = cmd_val_2; cmd.print("Setting led_step_button value to "); cmd.println(sval_1); cmd.print("Setting led_step_app value to "); cmd.println(sval_2); cmd.print("Setting led_step_alarm value to "); cmd.println(sval_3); } else { cmd.println("Error!"); } send_light_time_value[3] = sval_1; send_light_time_value[5] = sval_2; send_light_time_value[8] = sval_3; send_command(send_light_time_value, 12); } else { cmd.println("Wrong second value! Range(0-255) without 127"); } } else { cmd.println("Wrong value! Range (1-4)"); } cmd.flush(); add_log(84, cmd_val_1, cmd_val_2, 999); } else if (String("wifi_info") == cmd_request || String("wi") == cmd_request) { cmd.clear(); cmd.print("* Selected wifi id = "); cmd.println(network_id); if (network_id != 0) { cmd.print("* wifi eeprom SSID = "); cmd.println(read_eeprom_wifi(network_id - 1)); cmd.print("* wifi eeprom PASS = "); cmd.println(read_eeprom_wifi(network_id + 1)); } else { cmd.print("* wifi default SSID = "); cmd.println(network0_E_ssid); cmd.print("* wifi default PASS = "); cmd.println(network0_E_pass); } cmd.flush(); cmd.print("* wifi ssid value = "); cmd.println(E_ssid); cmd.print("* wifi pass value = "); cmd.println(E_pass); cmd.flush(); cmd.println("*******************************"); cmd.print("digitalRead P6 = "); cmd.println(pcf8574.digitalRead(wifi_network_sw_a)); cmd.print("digitalRead P7 = "); cmd.println(pcf8574.digitalRead(wifi_network_sw_b)); cmd.flush(); add_log(85, network_id, 999, 999); } else if (String("esp_info") == cmd_request || String("ei") == cmd_request) { cmd.clear(); cmd.print("* Firmware version: "); cmd.println(F_version); cmd.print("* EEPROM eeprom_optimalization writing succesful counter = "); cmd.println((EEPROM.read(514) << 8) + EEPROM.read(515)); cmd.print("* EEPROM eeprom_optimalization writing fail counter = "); cmd.println((EEPROM.read(516) << 8) + EEPROM.read(517)); cmd.println(ESP.getFlashChipId()); cmd.flush(); cmd.print("* Free heap: "); cmd.println(ESP.getFreeHeap()); cmd.print("* save log counter: "); cmd.println(save_log_counter()); cmd.print("* buffor log counter: "); cmd.println(log_buffor_counter()); cmd.print("* file name = "); cmd.println(read_file_name()); cmd.flush(); add_log(86, 1, 999, 999); } else if (String("display_log") == cmd_request || String("dl") == cmd_request) { //send_data_log = true; int dl_counter = display_log(); add_log(87, dl_counter, 999, 999); } else if (String("log_info") == cmd_request || String("li") == cmd_request) { //send_data_log = true; cmd.print("* LOGS counter = "); int li_counter = log_buffor_counter(); cmd.println(li_counter); cmd.print("* kolejka size = "); cmd.println(kolejka_size()); cmd.flush(); cmd.print("* kolejka empty = "); cmd.println(kolejka_empty()); cmd.print("* current_log_filename = "); cmd.println(current_log_filename); cmd.flush(); add_log(88, li_counter, kolejka_size(), (byte)kolejka_empty()); } else if (String("send_all_logs") == cmd_request) { cmd.println("ESP8266 will restart!"); EEPROM.write(522, 1); EEPROM.commit(); save_data(1); //save_data(0); delay(100); add_log(89, 1, 999, 999); ESP.restart(); } else if (cmd_request.startsWith("send_log=")) { uint8_t index_val_1; String cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1); if (!SD.begin(D8)) { send_mail_serial("initialization failed!", 1); add_log(32, 1, 999, 999); add_log(92, 0, 999, 999); return; } if (SD.exists(cmd_val_1)) { cmd.println("ESP8266 will restart!"); EEPROM.write(522, 2); EEPROM.commit(); save_file_name(cmd_val_1); cmd.println(read_file_name()); cmd.flush(); save_data(1); //save_data(0); delay(1000); ESP.restart(); } else { cmd.println("This file doesn't exist!"); cmd.flush(); } } else if (cmd_request.startsWith("change_filename=")) { uint8_t index_val_1; String cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1); save_file_name(cmd_val_1); delay(50); read_file_name(); } else if (String("sd_card_info") == cmd_request || String("sci") == cmd_request) { if (!SD.begin(D8)) { cmd.println("initialization failed!"); add_log(32, 1, 999, 999); delay(50); add_log(90, 0, 999, 999); return; } cmd.println("initialization done."); cmd.flush(); File root = SD.open("/"); printDirectory(root, 0); cmd.println("done!"); cmd.flush(); add_log(90, 1, 999, 999); } else if (cmd_request.startsWith("remove_file=")) { uint8_t index_val_1; String cmd_val_1; index_val_1 = cmd_request.indexOf('='); cmd_val_1 = cmd_request.substring(index_val_1 + 1); if (!SD.begin(D8)) { cmd.println("initialization failed!"); add_log(32, 1, 999, 999); return; } cmd.println("initialization done."); cmd.flush(); File root = SD.open("/"); if (!SD.exists(cmd_val_1)) { cmd.println("This file doesn't exist!"); cmd.flush(); add_log(91, 0, 999, 999); return; } SD.remove(cmd_val_1); if (!SD.exists(cmd_val_1)) { cmd.println("The file was deleted successfully !"); cmd.flush(); } else { cmd.println("The file could not be deleted !"); cmd.flush(); } add_log(91, 1, 999, 999); } else if (String("Remove_All_Files") == cmd_request) { if (!SD.begin(D8)) { cmd.println("initialization failed!"); add_log(32, 1, 999, 999); return; } cmd.println("initialization done."); cmd.flush(); File root = SD.open("/"); delay(100); rm(root, rootpath); if (!DeletedCount && !FailCount && !FolderDeleteCount) { add_log(17, DeletedCount, FailCount, 0); } else { cmd.print("Deleted "); cmd.print(DeletedCount); cmd.print(" file"); if (DeletedCount != 1) { cmd.print("s"); } cmd.print(" and "); cmd.print(FolderDeleteCount); cmd.print(" folder"); if (FolderDeleteCount != 1) { cmd.print("s"); } cmd.println(" from SD card."); if (FailCount > 0) { cmd.print("Failed to delete "); cmd.print(FailCount); cmd.print(" item"); if (FailCount != 1) { cmd.print("s"); } } add_log(17, DeletedCount, FailCount, 0); FailCount = 0; FolderDeleteCount = 0; DeletedCount = 0; } } else if (String("help") == cmd_request) { cmd.clear(); cmd.println("You can use this command: "); cmd.println("-------------------------------------------"); cmd.println("* cls"); cmd.println("* show_alarms or sha"); cmd.println("* alarms_counter or ac"); cmd.println("* display_alarm=[nr.szuf.] or da"); cmd.println("* set_alarm=[check in alarm_help or ah] or sa"); cmd.flush(); cmd.println("* rm_alarm=[remove alarm nr.] or rma"); cmd.println("* rm_all_alarm"); cmd.println("* millis_usage or mu"); cmd.println("* read_ard_eeprom=[nr.szuf.] or rae"); cmd.println("* write_ard_eeprom=x,y[szuf.0-1023/val.0-255] or wae"); cmd.println("* sync_time=[H:Min-D/M.Y]H=0-23|Min=0-59|Y=1-100"); cmd.flush(); cmd.println("* auto_sync_time or ast"); cmd.println("* Arduino_info=[check in arduino_info_help or aih] or ai"); cmd.println("* read_esp_alarm=[0-used/1-all] or rea"); cmd.println("* rm_esp_all_alarm"); cmd.println("* write_esp_eeprom=x,y[E.case 0-1023/val.0-255 or wee"); cmd.flush(); cmd.println("* read_esp_eeprom=x[nr.szuf.] or ree"); cmd.println("* clear_arduino_eeprom"); cmd.println("* clear_esp_eeprom"); cmd.println("* what's_time[arduino rtc time] or wt"); cmd.flush(); cmd.println("* debug_help or dh"); cmd.println("* debug_mode=[0/10] or dm"); cmd.println("* feed_info or fi"); cmd.println("* feed_sett=[check in feed_sett_info or fsi]"); cmd.println("* set_display_brightnes= x[x= 0-3] or sdb=x"); cmd.flush(); cmd.println("* add_wifi=x-y,z[nr.0-3 | ssid | pass]"); cmd.println("* display_wifi=x[nr.0-3]"); cmd.println("* reset_device=[0-arduino/1-esp/2-all]"); cmd.println("* display_all_wifi or daw"); cmd.println("* auto_feeding_off=[0-2:no/one_day/every_day]"); cmd.flush(); cmd.println("* set_light_speed=[x=1-4:button/app/alarm/all | y=0-100] or sls"); cmd.println("* wifi_info or wi"); cmd.println("* esp_info or ei"); cmd.println("* display_log or dl"); cmd.println("* log_info or li"); cmd.flush(); cmd.println("* clear_all_logs"); cmd.println("* sd_card_info or sci"); cmd.println("* send_all_logs"); cmd.println("* send_log=(log name, log0001.txt)"); cmd.flush(); cmd.println("* change_filename=(log name, log0001.txt)"); cmd.println("* remove_file=(filename - L0001.txt) or Remove_All_Files"); cmd.println("-------------------------------------------"); cmd.flush(); add_log(100, 1, 999, 999); } else { cmd.print("Wrong command: "); cmd.write(param.getBuffer(), param.getLength()); cmd.println(" <- Try command: help"); add_log(100, 0, 999, 999); } cmd.flush(); } Jak to działa? Istnieją dwie opcje, użytkownik wpisał poprawną komendę lub wpisał nie znaną komendę. W pierwszym przypadku wykonuje się fragment kodu przypisany do danej komendy, np. komenda cls odpowiada za wyczyszczenie terminala. Jeżeli podano nieznaną komendę, to na terminalu wyświetli się napis Wrong command: (podana komenda) <- Try command: help. Zaprogramowałem także coś w stylu spisu treści, po wpisaniu komendy help, na terminalu pojawią się wszystkie dostępne komendy oraz ich parametry. Spis komend: * cls * show_alarms or sha * alarms_counter or ac * display_alarm=[nr.szuf.] or da * set_alarm=[check in alarm_help or ah] or sa * rm_alarm=[remove alarm nr.] or rma * rm_all_alarm * millis_usage or mu * read_ard_eeprom=[nr.szuf.] or rae * write_ard_eeprom=x,y[szuf.0-1023/val.0-255] or wae * sync_time=[H:Min-D/M.Y]H=0-23|Min=0-59|Y=1-100 * auto_sync_time or ast * Arduino_info=[check in arduino_info_help or aih] or ai * read_esp_alarm=[0-used/1-all] or rea * rm_esp_all_alarm * write_esp_eeprom=x,y[E.case 0-1023/val.0-255 or wee * read_esp_eeprom=x[nr.szuf.] or ree * clear_arduino_eeprom * clear_esp_eeprom * what's_time[arduino rtc time] or wt * debug_help or dh * debug_mode=[0/10] or dm * feed_info or fi * feed_sett=[check in feed_sett_info or fsi] * set_display_brightnes= x[x= 0-3] or sdb=x * add_wifi=x-y,z[nr.0-3 | ssid | pass] * display_wifi=x[nr.0-3] * reset_device=[0-arduino/1-esp/2-all] * display_all_wifi or daw * auto_feeding_off=[0-2:no/one_day/every_day] * set_light_speed=[x=1-4:button/app/alarm/all | y=0-100] or sls * wifi_info or wi * esp_info or ei * display_log or dl * log_info or li * clear_all_logs * sd_card_info or sci * send_all_logs * send_log=(log name, log0001.txt) * change_filename=(log name, log0001.txt) * remove_file=(filename - L0001.txt) or Remove_All_Files Logi: Dodałem tę funkcję jedynie w celach edukacyjnych (chciałem przetestować, czy tworzenie logów ma jakikolwiek sens oraz czy jest to trudne). Okazało się jednak, że logi mogą być pomocne, dzięki nim mogłem testować, czy alarmy wykonują się poprawnie o zadanej godzinie, dlaczego i kiedy resetuje się samoczynnie sterownik itp. Tworzenie logów polega na zapisywaniu w buforze w pamięci RAM informacji o wykonaniu się jakiegoś zdarzenia, o czasie wykonania się tego zdarzenia oraz o jego parametrach. Przykładowo, zauważyłem, że karta sieciowa resetuje się samoczynnie co jakiś czas. Dzięki zapisanym logą mogę dowiedzieć się kiedy dokładnie sterownik zresetował się, (znam moment zakończenia się funkcji setup) oraz przeanalizować co mogło taki reset wykonać. void setup() { ... add_log(48, datalog_d, datalog_m, datalog_y); add_log(30, 1, 999, 999); //Karta sieciowa wystartowała check_day_change(); } Jestem w stanie sprawdzić dzień, godzinę i minutę wykonania się praktycznie każdego elementu programu sterownika lub karty sieciowej, co jest bardzo pomocne. Każdy log składa się z 8 bajtów danych: D- dzień | H- godzina | M- minuta | A- co to jest za log | Val1_lB / Val1_hB- zapisany na maksymalnie dwóch bajtach parametr | Val2- drugi parametr | Val3- trzeci, ostatni parametr przykładowy log: 100->D19H20M14A1&1#127, 127 Taki log mówi mi, że 19-stego dnia miesiąco o godzinię 20:14 ktoś wcisnął na obudowie sterownika przycisk niebieski i teraz stan na przekaźniku nr.1 jest wysoki. Wiem także, że nie podano 2 i 3 parametru ponieważ mają one wartość 127, nie wiem dokładnie dlaczego wybrałem tą liczbę a nie na przykład 255, ale stosuję tą zasadę w mechanizmie logów oraz podczas odczytywania alarmów. 100-> oznacza, że jest to setny zapisany log od czasu wyzerowania licznika logów, ten licznik zapisywany jest także w pamięci "EEPROM". Spis wszystkich logów: LOG ACTION: -> ARDUINO <- * 1 - blue switch was press in arduino (val1 = 1[on] / 2[off]) * 2 - red switch was press in arduino (val1 = 1[on] / 2[off]) * 3 - green switch was press in arduino (val1 = 0-10) * 4 - feeding button was press in arduino (val1 = None) * 5 - arduino was lock feeding option * 6 - arduino execute alarm (val1 = alarm number | val2 = alarm value) * 7 - arduino was reset * 8 - The arduino eeprom has been cleared -> ESP <- * 17 - delete all sd card files (val1= rm_file counter \ val2 = fail_counter \ val3 = status[1-ok/0-error]) * 18 - repair data function is running (val1=val2repair|val2=1-repair_automaticly/2-repair_manual/0-error value) * 19 - sending error data value log (val1 = 1) * 20 - V0 button has been pressed (val1 = 1[on] / 2[off]) * 21 - V1 button has been pressed (val1 = 1[on] / 2[off]) * 22 - v2 led pwm slider (val1 = 0-10) * 23 - feeding button has been pressed * 24 - feeding has been ended * 25 - feeding has been cancled (val1 = 1[to much feeding] | 2[feeding is off today] | 3[feeding is off] | 4[feeding is now running]) * 26 - setting alarm (val1 = number of allarm | val2 = function) * 27 - executing terminal command (val1 = command number | val2 = ?) * 28 - clear all logs * 29 - log_counter (val1 = counter) * 30 - esp8266 boot up succesful val1 =1 * 31 - write to esp eeprom (val1 = eeprom case | val2 = value | val3 = 0-error value/1-safe_mode_trigered/2-eeprom write/3-eeprom don't write) * 32 - sd card initialization failed * 33 - email was send * 34 - save log on sd card (val1= 0 Can't create file!/ 1-save ok /) val2 = send_counter * 35 - recived button blue change state command (val1 = 1-on/0-off) * 36 - recived button red change state command (val1 = 1-on/0-off) * 37 - recived button green change state command (val1 = 0-10) * 38 - recived button yellow press - feeding (val1 = 1-feeding_ok/0-feeding error/2-feeding_is_now_run | val2= feeding_today_counter) * 39 - It's new day (val1 = yesterday | val2 = today) * 40 - resetting esp trigered by alarm (val1=1) * 41 - syncing time trigered by alarm (val1 = 1) * 42 - recived alarm synchronized counter from arduino (val1 = counter) * 43 - recived millis from arduino (val1 = ard_millis) * 44 - recived arduino eeprom case value (val1 = arduino eeprom case / val2 = eeprom value) * 45 - recived send to esp eeprom succesful writing counter (1-eeprom succesful writing counter|2-eeprom fail writing counter|?-wrong value) * 46 - recived and printing in cmd (val1 = funkcja | val2 = day | val3 = value) * 47 - recived arduino rtc time (val1 = hour | val2 = day | val3 = week day) * 48 - changed log filename (val1 = day | val2 = month | val3 = year) * 49 - save_file_name (val1 = 1-ok/0-error) 51 - Action = cls | val1 = 1 | val2 = None | val3 = None 52 - Action = show_alarms | val1 = cmd_val | val2 = None | val3 = None 53 - Action = alarms_co unter | val1 = e_count | val2 = None | val3 = None 54 - Action = display_alarm | val1 = ter_val | val2 = None | val3 = None 55 - Action = alarm_help | val1 = 1 | val2 = None | val3 = None 56 - Action = rm_alarm | val1 = 1(done)/0(error) | val2 = None | val3 = None 57 - Action = rm_all_alarm | val1 = 1 | val2 = None | val3 = None 58 - Action = millis_usage | val1 = millis_esp | val2 = None | val3 = None 59 - Action = read_ard_eeprom | val1 = arduino_eeprom_case | val2 = 1(done)/0(error) | val3 = None 60 - Action = write_ard_eeprom | val1 = arduino_eeprom_case | val2 = value_to_write | val3 = 1(done)/0(error) 61 - Action = sync_time | val1 = godzina | val2 = minuta | val3 = dzien 62 - Action = auto_sync_time | val1 = godzina | val2 = minuta | val3 = dzien 63 - Action = Arduino_info | val1 = cmd_val_1 | val2 = None | val3 = None 64 - Action = arduino_info_help | val1 = 1 | val2 = None | val3 = None 65 - Action = read_esp_alarm | val1 = 0(only alarm)/1(all eeprom cases)/?(error, wrong value 0 or 1) | val2 = ile_alarmow_odczytano | val3 = None 66 - Action = rm_esp_all_alarm | val1 = 1 | val2 = None | val3 = None 67 - Action = write_esp_eeprom | val1 = EEPROM case | val2 = value to write | val3 = 0(error!Wrong value 1 or 2)/1(Done) 68 - Action = read_esp_eeprom | val1 = EEPROM case | val2 = read value | val3 = 0(error!Wrong eeprom case)/1(Done) 69 - Action = clear_arduino_eeprom | val1 = 1 | val2 = None | val3 = None 70 - Action = clear_esp_eeprom | val1 = 1 | val2 = None | val3 = None 71 - Action = what's_time | val1 = 1 | val2 = None | val3 = None 72 - Action = debug_help | val1 = 1 | val2 = None | val3 = None 73 - Action = debug_mode | val1 = debug_option | val2 = (0-error value/1-ok) | val3 = None 74 - Action = feed_info | val1 = (0-feeding\1-not today\2-off\3-wrong value) | val2 = None | val3 = None 75 - Action = feed_sett | val1 = 1-5 | val2 = cmd_val_2 | val3 = 1(Done)/0(Error) 76 - Action = feed_sett_info | val1 = 1 | val2 = None | val3 = None 77 - Action = set_display_brightnes | val1 = 0-3 | val2 = 1(Done)/0(Error) | val3 = None 78 - Action = reset_device | val1 = 0-2 | val2 = 1(Done)/0(Error) | val3 = None 79 - Action = add_wifi | val1 = wifi_number | val2 = 1(Done)/0(Error)/2(commit error) | val3 = None 80 - Action = display_wifi | val1 = number of wifi to displayed | val2 = 1(Done)/0(Error) | val3 = None 81 - Action = Clear_all_wifi=AquaDzz@iadek19 | val1 = 1 | val2 = None | val3 = None 82 - Action = display_all_wifi | val1 = 1 | val2 = None | val3 = None 83 - Action = auto_feeding_off | val1 = 1 | val2 = None | val3 = None 84 - Action = set_light_speed | val1 = cmd_val_1 | val2 = cmd_val_2 | val3 = None 85 - Action = wifi_info | val1 = network_id | val2 = None | val3 = None 86 - Action = esp_info | val1 = 1 | val2 = None | val3 = None 87 - Action = display_log | val1 = display_log_counter | val2 = None | val3 = None 88 - Action = log_info | val1 = log_counter | val2 = kolejka_size | val3 = kolejka_empty 89 - Action = send_all_logs | val1 = 1 | val2 = None | val3 = None 90 - Action = sd_card_info | val1 = (1-sd card read ok\ 0-sd card read error) | val2 = None | val3 = None 91 - Action = remove_file | val1 = 1-delete ok/0-error | val2 = None | val3 = None 92 - Action = send_log | val1 = 1-ok/0-error | val2 = None | val3 = None 93 - Action = | val1 = 1 | val2 = None | val3 = None 94 - Action = | val1 = 1 | val2 = None | val3 = None 95 - Action = | val1 = 1 | val2 = None | val3 = None 96 - Action = | val1 = 1 | val2 = None | val3 = None 97 - Action = | val1 = 1 | val2 = None | val3 = None 98 - Action = | val1 = 1 | val2 = None | val3 = None 99 - Action = | val1 = 1 | val2 = None | val3 = None 100 - Action = help or kommand unknown | val1 = (1-help ok/0-command unknown) | val2 = None | val3 = None Funkcja dodająca logi: void add_log(byte action, int val1, int val2, int val3) { /* if (kolejka_size() >= 400) { cmd.println("Buffor is full! Saving data on sd card!"); cmd.flush(); save_data(1); //save_data(0); } */ if (val1 < 0 || val1 > 65000) { cmd.print("Error! Log_add function - val1 out of range(0-65000) ->"); cmd.println(val1); cmd.flush(); return; } byte val1_hb = 0, val1_lb = 0; val1_hb = highByte(val1); val1_lb = lowByte(val1); byte log_D = day(); byte log_H = hour(); byte log_M = minute(); if (log_D < 0 || log_D > 31) { cmd.print("Error! add_log function - problem with day() value! Out of range(0-31) ->"); cmd.println(log_D); return; } if (log_H < 0 || log_H > 23) { cmd.print("Error! add_log function - problem with hour() value! Out of range(0-23) ->"); cmd.println(log_H); return; } if (log_M < 0 || log_M > 59) { cmd.print("Error! add_log function - problem with minute() value! Out of range(0-59) ->"); cmd.println(log_M); return; } int push_case = kolejka_push(log_D); kolejka_push(log_H); kolejka_push(log_M); kolejka_push(action); kolejka_push(val1_hb); kolejka_push(val1_lb); if (val2 > 255 || val2 < 0) { kolejka_push(127); } else { kolejka_push(val2); } if (val3 > 255 || val3 < 0) { kolejka_push(127); } else { kolejka_push(val3); } if (D_add_log == 1) { cmd.print("Setting log case "); cmd.print(push_case / 8); cmd.print("-"); cmd.print(kolejka_size()); cmd.print(" at "); cmd.print(message_aray[push_case + 1]); cmd.print(":"); cmd.flush(); cmd.print(message_aray[push_case + 2]); cmd.print(", day "); cmd.print(message_aray[push_case]); cmd.print(" -> Action "); cmd.print(message_aray[push_case + 3]); cmd.print(" with val1: "); cmd.flush(); cmd.print((message_aray[push_case + 4] << 8) + message_aray[push_case + 5]); cmd.print(" with val2: "); cmd.print(message_aray[push_case + 6]); cmd.print(" with val3: "); cmd.println(message_aray[push_case + 7]); cmd.flush(); } } Zapisywanie danych na karcie SD: Tworzenie logów już działa, ale co z ich przechowywaniem w pamięci RAM? Jak wiadomo pamięć RAM kiedyś się skończy, nie mogę więc zapisywać w niej setek czy tysięcy logów, kolejnym problemem jest to, że po utracie zasilania wszystkie moje logi przepadną. W pierwszym momencie planowałem zapisywać logi w pamięci EEPROM, szybko zrezygnowałem jednak z tego pomysłu, ponieważ ograniczał mnie rozmiar tej pamięci. Zdecydowałem się na użycie karty pamięci jako magazynu na dane. Nie zrezygnowałem jednak z buffera na dane, powodem tej decyzji jest to, że dla karty SD nie jest zdrowe ciągłe otwieranie i zamykanie plików, np. 30 razy na minutę, 24 godziny na dobę przez cały rok. Lepszym pomysłem jest hurtowe zapisywanie kilku logów podczas jednego otwarcia i zamknięcia pliku. Aktualnie zapisuję na karcie pamięci 75 logów na jedno otwarcie pliku, buffer może pomieścić maksymalnie 110 logów, mam więc zapas w razie problemu z zapisem w danej chwili. wywołanie funkcji zapisującej na karcie SD: if (kolejka_size() >= 600) { save_data(1); } Co 5 sekund urządzenie sprawdza czy w bufferze znajduje się więcej niż 74 logi (ponieważ 600 komórek / 8 komórek na log = 75 logów). Jeżeli tak to następuje próba zapisu. Próba zapisania danych występuje także podczas resetowania się karty sieciowej przy użyciu alarmu lub komendy (sterownik główny lub karta sieciowa mogą resetować się auto magicznie, jeżeli ustawie taki alarm), ma to na celu zapobieganie przed utraceniem danych. funkcja zapisująca dane: void save_data(bool datafile_all) { if (!SD.begin(D8)) { cmd.println("initialization failed!"); cmd.flush(); add_log(32, 1, 999, 999); return; } else { sd_card_work = true; } long log_counter = readLongFromEEPROM(518); cmd.println("Saving data from buffer to sd card!"); cmd.print("Saving "); cmd.print(kolejka_size()); cmd.println(" logs !"); cmd.print("Log name = "); cmd.println(current_log_filename); cmd.print("Log counter = "); cmd.println(log_counter); cmd.flush(); File dataFile; String full_datalog_name = "DATALOG.txt"; if (datafile_all == 1) { full_datalog_name = current_log_filename; } if (!SD.exists(full_datalog_name)) { cmd.println(full_datalog_name + " doesn't exist! Creating file in progress... "); dataFile = SD.open(full_datalog_name, FILE_WRITE); dataFile.close(); if (!SD.exists(full_datalog_name)) { cmd.println("Can't create file!"); cmd.flush(); add_log(34, 0, 999, 999); return; } else { cmd.println("File was created!"); cmd.flush(); } } else { cmd.println("File " + full_datalog_name + " existing!"); cmd.flush(); } log_counter = readLongFromEEPROM(518); byte wd = 0; String dataString = ""; int send_counter = 0; byte k_size = kolejka_size(); while (kolejka_empty() == false) { if (wd == 0) { dataString = ""; dataFile = SD.open(full_datalog_name, FILE_WRITE); if (!dataFile) { cmd.println("Error When opening file " + full_datalog_name); dataFile.close(); return; } } dataString += String(log_counter); dataString += "->D"; dataString += String(kolejka_pop()); dataString += 'H'; dataString += String(kolejka_pop()); dataString += 'M'; dataString += String(kolejka_pop()); dataString += 'A'; dataString += String(kolejka_pop()); dataString += '&'; byte war1, war2; war1 = kolejka_pop(); war2 = kolejka_pop(); int wartosc = (war1 << 8) + war2; dataString += String(wartosc); dataString += '#'; dataString += String(kolejka_pop()); dataString += '$'; dataString += String(kolejka_pop()); dataString += '\n'; send_counter++; if (wd == 25 || kolejka_empty() == true) { dataFile.println(dataString); cmd.println("Writing " + full_datalog_name + " OK!"); dataFile.close(); dataFile = SD.open("DATALOG.txt", FILE_WRITE); if (!dataFile) { cmd.println("Error When opening file DATALOG.txt"); dataFile.close(); return; } dataFile.println(dataString); cmd.println("Writing DATALOG.txt OK!"); dataFile.close(); wd = -1; } wd++; log_counter++; } writeLongIntoEEPROM(518, log_counter); cmd.print("Log counter -> "); cmd.println(readLongFromEEPROM(518)); cmd.print("Succesful save "); cmd.print(send_counter); cmd.println(" logs"); cmd.flush(); if (dataString.length() > 0) { cmd.println("String is no empty!"); cmd.flush(); dataFile.close(); dataFile = SD.open(full_datalog_name, FILE_WRITE); if (!dataFile) { cmd.println("Error When opening file " + full_datalog_name); dataFile.close(); return; } dataFile.println(dataString); cmd.println("Writing " + full_datalog_name + " OK!"); dataFile.close(); dataFile = SD.open("DATALOG.txt", FILE_WRITE); if (!dataFile) { cmd.println("Error When opening file DATALOG.txt"); dataFile.close(); return; } dataFile.println(dataString); cmd.println("Writing DATALOG.txt OK!"); dataFile.close(); } SD.end(); add_log(34, 1, send_counter, 999); } Na początku sprawdzam, czy mogę odczytać / zapisać coś na karcie pamięci, jeżeli tak to wypisuje na terminalu w aplikacji Blynk informację o nazwie pliku do którego zostaną zapisane dane, ilość zapisywanych logów, licznik wszystkich zapisanych logów. Następnie sprawdzane jest czy plik o podanej nazwie istnieje, ta nazwa powstaje podczas uruchamiania się karty sieciowej na podstawie aktualnej daty (przykładowa nazwa L010321.txt). Jeżeli nie istnieje, to jest tworzony. Kolejnym krokiem jest odczytanie wszystkich komórek z buffera, zapisując je jednocześnie w postaci stringów z dodanymi przerywaczami w postaci litery i znaków na karcie SD. Na koniec na terminalu wypisuje status zapisu, zamykam plik, dodaje log z informacją o zapisie oraz liczbie zapisanych logów. Wysyłanie danych w wiadomości email: Podczas programowania tego urządzenia chciałem w jakiś sposób przesyłać dane z karty pamięci na zewnętrzny serwer, pocztę, dysk google lub jako dane w tabelkach w google spreedsheet online. Testowałem właśnie to ostatnie rozwiązanie, korzystałem z tego poradnika. Działało to dość słabo, bez problemu mogłem wysyłać dane do 7 komórek na raz (jeden alarm), ale wysyłanie trwało około 5 sekund. Wysłanie 75 alarmów zajęło by około 7 minut, niestety nie był to jedyny problem. Podczas hurtowego wysyłania alarmów, esp często się zawieszało lub niektóre alarmy były pomijane. Zrezygnowałem więc z tego pomysłu. Kolejnym pomysłem było wysyłanie logów jako stringi w wiadomości email, postanowiłem użyć widżetu email z aplikacji Blynk. Pozwala on na proste wysyłanie wiadomości na podane adres email (nadal korzystam z tego rozwiązania podczas wysyłania kodów z błędami). Jedną z wad tego rozwiązania było ograniczenie do maks. 1200 znaków na wiadomość, jeden alarm może składać się maksymalnie z 33 znaków (np. 34432->D28H13M44A45&12345#127$127), czyli teoretycznie w jednej wiadomości mogę wysłać 36 logów, w praktyce mniej ponieważ tytuł wiadomość też zabiera znaki. Pomyślałem, że mogę wysyłać właśnie tak pocięte wiadomości odczytywane z karty SD, jednak jest to trochę karkołomne rozwiązanie. W jeden dzień jestem w stanie wyprodukować nawet 600 logów, wysyłanie ich wszystkich na pocztę jest trochę mało wygodne. Szukałem rozwiązania pozwalającego na wysłanie pliku txt na pocztę lub serwer, i tak tworze taki plik na karcie SD, więc dlaczego go nie wykorzystać. Znalazłem projekt EMailSender, jest moim zdaniem świetny. Pozwala on na przesyłanie różnych plików, np. .txt, .jpg na pocztę gmail. Działa on na esp8266, esp32, czy też arduino z podłączoną kartą sieciową. Pliki na poczcie otrzymuję jako załącznik. Jedyną wadą jest długi czas wysyłani się plików. Podczas wysyłania plików na pocztę, karta sieciowa resetuje się i wchodzi w tryb wysyłania wiadomości. Podczas tego trybu aplikacja Blynk ani inne funkcje karty sieciowej nie działają, działa jednak wysyłanie komend do sterownika głównego. Zmodyfikowałem trochę bibliotekę EMailSender, dzięki temu mogę obliczyć ile procent wiadomości zostało wysłane, te informację wysyłam do sterownika głównego, a on pokazuje je na wyświetlaczu. Podczas wysyłania wiadomości na serial monitorze mogą pojawić się dane, np. o błędzie podczas wysyłania, błędzie podczas logowania się lub informacja o pomyślnym wysłaniu wiadomości, wszystkie te dane zapisuję w bufferze, a po połączeniu się esp z aplikacją Blynk, wyświetlam je na terminalu. void send_mail(bool send_own_file) { String datafile_name = "DATALOG.txt"; send_mail_serial("Starting!", 1); send_mail_serial("Initializing SD card...", 1); if (!SD.begin(D8)) { send_mail_serial("initialization failed!", 1); add_log(32, 1, 999, 999); return; } if (send_own_file == 1) { datafile_name = read_file_name(); } send_mail_serial("initialization done.", 1); File root = SD.open("/"); delay(100); if (WiFi.status() != WL_CONNECTED) { static uint16_t attempt = 0; send_mail_serial("Connecting to ", 0); WiFi.begin(E_ssid.c_str(), E_pass.c_str()); send_mail_serial(String(E_ssid.c_str()), 1); uint8_t i = 0; while (WiFi.status() != WL_CONNECTED && i++ < 50) { delay(200); send_mail_serial(".", 0); } ++attempt; send_mail_serial("", 1); if (i == 51) { send_mail_serial("Connection: TIMEOUT on attempt: ", 0); send_mail_serial(String(attempt), 1); if (attempt % 2 == 0) send_mail_serial("Check if access point available or SSID and Password", 1); return; } send_mail_serial("Connection: ESTABLISHED", 1); //send_mail_serial("Got IP address: ", 0); //send_mail_serial(String(WiFi.localIP()), 1); } byte datalog_d, datalog_m, datalog_y; datalog_d = EEPROM.read(523); datalog_m = EEPROM.read(524); datalog_y = EEPROM.read(525); String dd_str = ""; if (datalog_d < 10) { dd_str = "0"; } dd_str += String(datalog_d); String dm_str = ""; if (datalog_m < 10) { dm_str = "0"; } dm_str += String(datalog_m); String dy_str = ""; if (datalog_y < 10) { dy_str = "0"; } dy_str += String(datalog_y); current_log_filename = "L" + dd_str + dm_str + dy_str + ".txt"; EMailSender::EMailMessage message; message.subject = "Logi z sterownika"; message.message = "Pobierz plik z logami :)<br>"; message.mime = MIME_TEXT_PLAIN; byte message_size = 0; if (send_own_file == 0) { message_size = 2; } else { message_size = 1; } EMailSender::FileDescriptior fileDescriptor[message_size]; fileDescriptor[0].filename = (datafile_name); fileDescriptor[0].url = ("/" + datafile_name); fileDescriptor[0].storageType = EMailSender::EMAIL_STORAGE_TYPE_SD; if (message_size > 1) { fileDescriptor[1].filename = (current_log_filename); fileDescriptor[1].url = ("/" + current_log_filename); fileDescriptor[1].storageType = EMailSender::EMAIL_STORAGE_TYPE_SD; } EMailSender::Attachments attachs = {message_size, fileDescriptor}; EMailSender::Response resp = emailSend.send("jkgrzelak1@gmail.com", message, attachs); send_mail_serial("Sending status: ", 1); send_mail_serial(String(resp.status), 1); send_mail_serial(String(resp.code), 1); send_mail_serial(String(resp.desc), 1); SD.end(); EEPROM.write(522, 3); EEPROM.commit(); add_log(33, 1, 999, 999); } kod karty sieciowej (Zalecam użyć dołączonych prze zemnie bibliotek): kod_karty_sieciowej.zip Automatyczny karmnik: Do budowy automatycznego karmnika wykorzystałem projekt ze strony thingiverse. Dodałem tylko własną obudowę na silnik oraz zrezygnowałem z górnego mieszalnika. W środku obudowy znajduje się: mostek h (LM293D), odpowiada on za sterowanie silniczkiem (4,5V) z przekładnią z zabawki 3 diody led, pierwsza odpowiada za informowanie o zasilaniu karmnika (odłączyłem ją ponieważ niepotrzebnie świeciła cały dzień), 2 i 3 dioda świeci odpowiednio podczas procesu podawania pokarmu lub powrotu do pojemnika Do działania karmnika potrzebny jest także stabilizator napięcia 12v na 5v, znajduję się on jednak poza obudową karmnika. Postanowiłem umieścić go w małej prostokątnej obudowie wraz z bezpiecznikiem i gniazdem LAN. Tak karmnik jest połączony ze sterownikiem głównym poprzez moduł z gniazdem LAN na skrętce komputerowej 😉 Wykorzystuje tam 6 żył na zasilanie oraz po jednej żyle na sterowanie mostkiem h. Aktualnie moduł karmnika oraz moduł z gniazdem LAN dla jego zasilania, odłączone są od sterownika głównego (mam zamiar wykorzystać je w swoim sterowniku). Obudowa: Obudowa sterownika głównego to puszka natynkowa hermetyczna z wyciętym otworem wentylacyjnym na wieczku oraz przyklejonej w tym miejscu kratki wydrukowanej na drukarce 3D, otworem na bezpiecznik z prawej strony, otworem do podwójnego gniazdka 230v, otworem i ramką dla wyświetlacza oraz otworami dla przycisków w wieczku obudowy. Obudowa dla karty sieciowe została w pełni wydrukowana na drukarce 3D, jest ona dość spora, ale i tak ledwo zmieściłem się ze wszystkimi potrzebnymi elementami. W pierwotnej wersji z sterownika głównego wystawał przymocowany na krótkim przewodzie prostokątny moduł z elektroniką sterującą automatycznym karmnikiem, znajdował się w niej bezpiecznik, stabilizator napięcia oraz złącze do podłączenia karmnika na długim przewodzie. Usunąłem go jednak, ponieważ dziadek nie korzystał z karmnika Nie jestem zadowolony z finalnego efektu, następnym razem wykorzystam dużo większą obudowę w której zmieszczę całą elektronikę sterownika głównego oraz karty sieciowej. Podsumowanie Jestem zadowolony z działania tego urządzenia, podczas testowania go w swoim akwarium z rybami nie sprawiał mi problemów. Co nie wyszło? moim zdaniem większość kodu mogła by być napisana lepiej, aktualnie widzę kilka błędów np. możliwość wykonania się tylko jednego alarmu na minutę, brak odpowiednich zabezpieczeń podczas wpisywania komend w terminalu, lekki bałagan w kodzie Moduł karmnika powinien być podłączony do sterownika a nie do karty sieciowej, takie podłączenie czasami uniemożliwia karmienie podczas braku dostępu do internetu lub podczas startu karty sieciowej Wysyłanie kilku dniowych plików z logami trwa nawet kilka godzin, nie jest to jednak do końca wada, ponieważ mogę wysyłać taki plik w nocy Automatyczny karmnik nie jest wykorzystywany przez dziadka, trochę mu nie ufa 😉 wygląd sterownika jest moim zdaniem ok, ale wygląd karty sieciowej jest dość śmieszny, tym bardziej doklejonej na gorący klej anteny od routera podczas testów nigdy nie miałem problemów z działaniem karty sieciowej nawet gdy była oddalona od routera w lini prostej na 15 metrów przez 3 ściany i podłogę, jednak podczas próby uruchomienia całego urządzenia u dziadka okazało się, że nowy router z światłowodem oddalony o 6 metrów w linii prostej przez 3 ściany nie jest w stanie połączyć się z kartą sieciową, problemem mogły być także inne sieci WiFi ponieważ mieszkanie znajduje się w bloku. Dlatego na szybko musiałem dodać zewnętrzną antenę ze starego routera TP-linka, po tej modyfikacji wszystko działa bez problemu Co wyszło? Urządzenie jest stabilne, nie zdarzają się mu już samo resety lub zawieszanie się Sterownik główny jest autonomiczny, działa nawet kiedy karta sieciowa jest wyłączona Alarmy działają bez problemu Pasek LED podłączony do sterownika działa bardzo dobrze, powoli rozjaśniające i ściemniające się światło nie straszy ryb Logi tworzone przez sterownik pozwalają na sprawdzenie co działa nie tak jak powinno, jest to moim zdaniem fajne narzędzie które będę używać w przyszłych projektach Najważniejsza rzecz, mój dziadek jest zadowolony z działania tego urządzenia, nie musi już codziennie kilka razy sięgać do listwy i wkładać/wyciągać wtyczkę od pompki, napowietrzacza oraz zasilacza do oświetlenia LED Plany na przyszłość Podczas budowy i programowania tego urządzenia nauczyłem się sporo rzeczy, aktualnie pracuję nad czymś w stylu uniwersalnego sterownika do oświetlenia zewnętrznego / systemu nawadniania / sterownika do mojego akwarium. Ma on mieć możliwość dodawania alarmów poprzez wyświetlacz lcd 20x4 i menu z 4 przyciskami, sterowania wyjściami poprzez telefon, odczytywanie danych z czujników itp. Chcę wykorzystać i ulepszyć kilka pomysłów z tego sterownik w moim nowym urządzeniu. Dziękuje za przeczytanie tego artykułu, mam nadzieję że ktoś z mojego rozwiązania coś wykorzysta. Powodzenia we wszystkich waszych przyszłych projektach 🙂
  20. Na łamach Forbot-a temat był już wcześniej poruszany i tak naprawdę cały artykuł powstał ze względu na duże niezrozumienie tematu oraz reakcje osób na forum. Celowo odnoszę się bezpośrednio do artykułu Sandry, ponieważ część z informacji warto przeczytać, a nie chcę powtarzać tego samego, dlatego przed przeczytaniem dalszej części polecam lekturę. 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 » Niestety autorka dopuściła się moim zdaniem błędu interpretacyjnego samych przepisów, jak i niezrozumieniu samej konstrukcji urządzeń DIY wykonywanych przez amatorów. Prowadzi to do błędnych wniosków, które miałem okazję czytać na forum pod tym wpisem. Niestety w tej części nie będzie już tak łatwo, każde z pytań ma kilka zmiennych na które nie da się odpowiedzieć wprost tak lub nie. Pytanie: Czy muszę umieszczać znak CE i robić certyfikaty jeśli chce sprzedać mój projekt DIY? Aby odpowiedzieć na to pytanie odpowiadają nam konkretnie cztery z kilkudziesięciu dyrektyw tzw. nowego podejścia (wcześniej było tzw. stare podejście i było trochę bardziej upierdliwe dla producentów): 2011/65/UE DYREKTYWA ROHS II I 2015/863 DYREKTYWA ROHS III (pomijam z opisu, ponieważ przyjmujemy założenie, że korzystamy z gotowych podzespołów) 2014/53/UE DYREKTYWA RADIOWA / DYREKTYWA RED 2014/35/UE DYREKTYWA NISKONAPIĘCIOWA LVD i teoretycznie 2014/30/UE DYREKTYWA KOMPATYBILNOŚCI ELEKTROMAGNETYCZNEJ EMC Definicje W związku z tym musimy odnieść się do rozporządzenia wprowadzającego obowiązek stosowania znaku CE na produktach określonych w dyrektywach tzw. nowego podejścia (Rozporządzenie Parlamenty Europejskiego i Rady (WE) NR 765/2008) Jako hobbystę amatora, który wymyśla sobie produkt, najczęściej z półproduktów, np. wspomniany przez autorkę poprzedniego artykułu budzik, interesują nas definicje: - udostępnienie produktu na rynku - producent Tutaj mamy tak naprawdę pierwszy zgrzyt interpretacyjny w poprzednim artykule. Z niewiadomych mi przyczyn definicja udostępniana produktu na rynku, określona przez ustawodawcę źródło: art. 2 pkt 1 Rozporządzenia Parlamentu Europejskiego i rady (WE) NR 765/2008 z dnia 9 lipca 2008 r. ustanawiające wymagania w zakresie akredytacji i nadzoru rynku odnoszące się do warunków wprowadzania produktów do obrotu i uchylające rozporządzenie (EWG) nr 339/93 Została odczytana jako alternatywa, czyli kiedy chociaż jeden z danych elementów ma miejsce. Jest to całkowicie błędne rozumowanie. Niestety jedną z opisywanych wcześniej wykładni jest wykładnia logiczna i nie została tutaj zastosowana. W naszym przykładzie nie bierzemy pod uwagę „działalności handlowej” tylko hobbystyczną. W związku z tym nie udostępniasz swojego projektu na rynku, ponieważ nie jesteś podmiotem profesjonalnym. Koledze możesz oddać wszystko (nawet dziewczynę/chłopaka) i „zła unia” nie ma nic do tego, oczywiście jeśli robisz to poza działalnością handlową. Aby rozróżnić czy jesteśmy hobbystą-amatorem, czy też jest to działalność handlowa, musimy wziąć pod uwagę cel w jakim dany przedmiot powstaje. Jeśli wykonujesz go dla siebie, ale później chcesz go odsprzedać, nie podchodzi to pod działalność handlową. Jeśli natomiast produkujesz dany przedmiot tylko w celu jego sprzedaży, jest to jak najbardziej przesłanka do zastosowania dyrektywy, niezależnie czy prowadzimy działalność gospodarczą czy nie. Podstawa prawna: 1. art. 3 ustawy prawo przedsiębiorców z dnia 6 marca 2018 r. (Dz.U. 2018 poz. 646) 2. art. 5a pkt 6 ustawy o podatku dochodowym od osób fizycznych z dnia 26 lipca 1991 r. (Dz.U. 1991 nr 80 poz. 350) 3. art. 3 pkt 9 ustawy ordynacja podatkowa z dnia 29 sierpnia 1997 r. (Dz.U. 1997 nr 137 poz. 926) 4. art. 43[1] Ustawy kodeks cywilny z dnia z dnia 23 kwietnia 1964 r (Dz.U. 1964 nr 16 poz. 93) No dobra ale co jeśli jednak chciałbym na tym zarabiać? Tutaj sprawa się komplikuje, ponieważ ma to się zupełnie odwrotnie do opisywanej wcześniej ustawy o prawach konsumenta (link do poprzedniej części). Mamy dwie istotne sprawy, pierwsza to czy prowadzimy działalność czy tez wykonujemy to jako zleceniobiorca. Dlaczego jest to istotne? Ponieważ zgodnie z definicją dyrektywy: źródło: art. 2 pkt 3 Rozporządzenia Parlamentu Europejskiego i rady (WE) NR 765/2008 z dnia 9 lipca 2008 r. ustanawiające wymagania w zakresie akredytacji i nadzoru rynku odnoszące się do warunków wprowadzania produktów do obrotu i uchylające rozporządzenie (EWG) nr 339/93 W skrócie – podpisując odpowiednio zredagowaną umowę o dzieło stajemy się jedynie wytwórcą danego produktu i to na zleceniodawcy spoczywa obowiązek przestrzegania przepisów związanych z dyrektywami CE. Fajne, nie? No tak, tylko trochę nieuczciwe 😉 Prawdopodobnie nie do wybronienia też w sądzie ze względu na czynność pozorną w celu ominięcia przepisów (art. 83 KC). Dlatego nie wchodźmy w szarą strefę i przyjmijmy, że chcemy jednak sprzedawać nasz produkt jako producent i pod naszą nazwą. Bez znaczenia jaką formę działalności przyjmiemy – zarabiamy na tym, więc jesteśmy producentem. Podstawa prawna: art. 83 Kodeksu Cywilnego Ok ale właściwie to ja przecież niczego nie robię sam, kupuję podzespoły i składam je tylko do kupy. One wszystkie mają znaczek CE, więc czemu się czepiać wyrobu końcowego. Niestety po złożeniu go w całość, wytwórca ponosi odpowiedzialność za całość układu (powrót do podstaw z poprzedniego artykułu – prawo autorskie). Wracamy wiec do dyrektyw określających do jakich produktów musi być zastosowany znak CE. 2014/53/UE DYREKTYWA RADIOWA / DYREKTYWA RED Jeśli twoje urządzenie posiada Bluetooth, Wifi 2,4G, nRF, czy też inny podobny protokół łączności radiowej, niestety twoje urządzenie powinno posiadać znak CE. Autorka poprzedniego artykułu błędnie zdefiniowała amatorską służbę radiolokacyjną, co niewiele ma wspólnego z wymienionymi protokołami, tym bardziej, że w ogóle nie dotyczy kwestii gdy chcemy na tym zarabiać. Martwić się? Pójdę do więzienia? Nie. Wytłumaczę to na końcu. 2014/35/UE DYREKTYWA NISKONAPIĘCIOWA LVD Dotyczy urządzeń pracujących w zakresie napięcia 50-1000V AC/DC i 74-1500V DC. Praktycznie do większości z urządzeń opartych na Raspberry Pi, Arduino, ESP, Pi Pico, Spresense itd., ta dyrektywa nie ma zastosowania. Wystarczy tylko aby zasilacz był oddzielnym peryferyjnym podzespołem, wtedy zamiast jego producentem stajemy dystrybutorem, czyli po prostu przerzucamy pełną odpowiedzialność ze znakiem CE na producenta który go wytworzył (czy też jego importera). W związku z tym dla własnego bezpieczeństwa lepiej kupować go od polskiego kontrahenta. Czyli w tym przypadku znak CE nie jest potrzebny. Podstawa prawna: art.1 dyrektywy 2014/35/UE 2014/30/UE DYREKTYWA KOMPATYBILNOŚCI ELEKTROMAGNETYCZNEJ EMC Jest to moim zdaniem najmniej prawdopodobny powód dla jakiego mielibyśmy przeprowadzać badania pod kątem zgodności produktu, ponieważ w przypadku większości zastosowań i niskich napięć, nie mamy możliwości powodować zaburzeń elektromagnetycznych (jedynie mogą być podatne na inne zakłócenia) Podstawa prawna: art 2 ust 2 lit d) dyrektywy 2014/30/UE Idąc za definicją zespołu podzespołów czyli aparatury: źródło: art 3 ust. 1 pkt 2 dyrektywy 2014/30/UE Przyjmujemy, że nasz budzik zasilany napięciem 3,3V nie będzie samoistnie włączać nam telewizora i powodować ciąży u sąsiadki. Po prostu z założenia jest to głupie. Oczywiście jeśli naszym projektem jest elektromagnes, warto jednak przeprowadzić by te cholerne badania 😉 Reasumując, znak CE na wyrobach hobbystycznych DIY typu budzik musimy zastosować jedynie w przypadku gdy podłączamy je bezpośrednio do gniazdka 230V (nie przez zasilacz innego producenta) i gdy mamy w nim zastosowany system łączności radiowej (czyli prawie każdy). Oczywiście tylko i wyłącznie jeśli robimy to w celach handlowych, a nie dajemy chłopakowi/dziewczynie prezent z okazji zadawania się z nami i naszymi gazami lutowniczymi. Pytanie: Czyli muszę odezwać się do firmy zajmującej się certyfikacją CE, żeby przebadała mój projekt i wystawiła certyfikat? Nie, zupełnie nic z tych rzeczy. „Zła unia” nie zmusza małych wytwórców do przeprowadzania badań za grube tysiące, tylko po to żeby sprzedali dziesięć budzików po 200 zł. Ogólnie nie istnieje coś takiego jak certyfikat CE. Przepisy definiują jedynie jednostki notyfikacyjne dla grup produktów, które muszą posiadać szczegółowe badania dla swoich produktów. Każdy z producentów ma w większości przypadków przeprowadzić badania we własnym zakresie i wystawić deklarację zgodności WE. Jest to dokument, który jako producent wystawiasz wg wzoru dostępnego na stronach Parlamentu UE i wkładasz sobie do szuflady, nic więcej z tym nie musisz robić. W przypadku gdyby pojawiła się kontrola, masz obowiązek go przedstawić. Dodatkowo na produkcie powinien pojawić się znaczek CE lub jeśli nie uda się go zamieścić na produkcie ma być dostępny na oddzielnym dokumencie dołączonym do produktu. Myślę, że w przypadku korzystania z gotowych podzespołów dostarczanych przez innych producentów, typu płytki rozwojowe, spokojnie możemy bazować na ich badaniach dotyczących protokołów radiowych działających zgodnie z normami. Również warto w tym przypadku korzystać z polskich podmiotów. Pozostaje jedynie kwestia odpowiedzialności prawnej jaka grozi w przypadku sprzedaży wadliwego produktu. Zapraszam do ostatniej części artykułu.
  21. Wstęp Tematyka uczenia maszynowego, sieci neuronowych oraz sztucznej inteligencji jest w ostatnich latach bardzo popularna nie tylko w środowisku naukowym, ale również wśród programistów i elektroników amatorów. Często jednak zdarza się, że znamy te zagadnienia z praktyki i umiemy je wykorzystywać przy pomocy popularnych bibliotek takich jak Keras, jednak nie wiemy co dzieje się na niższym poziomie. W artykule tym postaramy się jednak to zmienić. Naszym celem będzie zrozumienie podstaw teoretycznych i poznanie działania sztucznej inteligencji od pojedynczego neuronu, przez proces uczenia, aż do gotowej i wyuczonej sieci. 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 » Pojedynczy neuron Sztuczne sieci neuronowe składają się z węzłów (pojedynczych neuronów), warstw tworzonych przez węzły oraz połączeń między węzłami. Pojedynczy węzeł składa się z funkcji wejściowej, funkcji aktywacji oraz wyjścia. Połączenia między węzłami natomiast posiadają przypisane im wagi, przez które przemnażany jest sygnał przez nie płynący. Wyróżniamy również trzy warstwy węzłów: wejściową, ukrytą oraz wyjściową. Matematyczny model węzła, czyli pojedynczego sztucznego neuronu pokazano poniżej. Na początek skupmy się jednak na działaniu pojedynczego neuronu. Posiada on dowolną liczbę wejść oraz jedno wyjście. Wejściami mogą być, w zależności od warstwy w jakiej znajduje się nasz neuron, dane wejściowe lub wyjścia innego neuronu. Na rysunku mamy sześć sygnałów wejściowych opisanych x0-x5. Z reguły sygnały te powinny przyjmować wartości od 0 do 1 i następnie być przemnożone przez odpowiadające im wagi θ0-θ5. Wielkość tych wag jest dowolna (mogą być dodatnie oraz ujemne) i jest uzależniona od tego jak istotne dla działania naszej sieci jest dane połączenie. Podczas procesu uczenia sieci to właśnie te wagi ulegają zmianie tak by efekt działania sieci był jak najtrafniejszy. Wszystkie przemnożone przez swoje wagi wejścia są w kolejnym kroku sumowane i podawane jako wejście (czyli argument) funkcji aktywacji. Funkcja aktywacji jest niezwykle istotna. Z reguły wykorzystuje się tu funkcję sigmoidalną. Jej wykres przedstawiono na rysunku poniżej. Funkcja ta na podstawie swojego argumentu przyjmuje wartości z zakresu od 0 do 1. Zazwyczaj przyjmuje ona jednak tylko swoje skrajne wartości. Dlaczego więc stosujemy funkcję sigmoidalną zamiast zwykłego określania, że dla ujemnych argumentów funkcja aktywacji przyjmuje wartość 0, a dla dodatnich 1? Otóż funkcja ta jest w prosty sposób różniczkowalna1 co ma znaczący wpływ w procesie uczenia całej sieci, znacznie ułatwiając potrzebne obliczenia. Nie będziemy jednak tego tu dokładniej omawiać, temat doboru funkcji aktywacji mogłyby posłużyć na osobny artykuł. Wracając do działania neuronu wystarczy dodać, że wyjście funkcji aktywacji jest zarazem wyjściem całego węzła. Mówimy więc, że neuron jest w stanie spoczynku, gdy wartość funkcji aktywacji jest równa 0, lub w stanie pobudzenia gdy jest równa 1. W skrócie: Neuron jest w stanie spoczynku lub pobudzenia w zależności od wartości zsumowanych wejść tego neuronu pomnożonych przez przypisane im wagi. Od neuronu do sieci Mamy już pojedynczy neuron i wiemy jak on działa, ale co dalej? Teraz stworzymy całą sieć. W tym celu wyjścia jednego neuronu połączymy z wejściami innych. Warto jednak pamiętać, że część neuronów jako wejścia będzie posiadać dane wejściowe na podstawie których nasza sieć będzie wyliczać swoje wyjście. Neurony te stanowią warstwę wejściową. Z drugiej strony część neuronów będzie posiadała wyjścia nie podłączone do innych neuronów, lecz stanowiące wyjście całej sieci. Neurony te tworzą więc warstwę wyjściową. Cała reszta, która może łączyć się ze sobą w dowolny sposób (zaprojektowany oczywiście przez projektanta) nazywana jest warstwą ukrytą. W warstwie tej z reguły jest najwięcej połączeń oraz węzłów i od niej w dużej mierze zależy prawidłowość działania całej sieci. Schemat bardzo prostej sieci pokazano poniżej. Podsumowanie Wiemy już z czego składa się cała sieć, aż do pojedynczych części pojedynczego neuronu. Wiemy, że sieć składa się z wielu połączeń i każde z nich ma swoją wagę. Pozostaje nam więc jedynie odpowiedzieć na pytanie na czym polega proces uczenia maszynowego? Otóż polega on tylko i wyłącznie na doborze najoptymalniejszych wag dla każdego połączenia. Jest to proces który wymaga nieprawdopodobnej liczby obliczeń nawet przy niewielkich sieciach, lecz raz “wyuczona” sieć już tak skomplikowanych obliczeń nie potrzebuje dla prawidłowego działania. Dlatego też proces uczenia często wymaga dużej mocy obliczeniowej i zajmuje niemało czasu, natomiast sama sieć potrafi działać szybko nawet na mniej wydajnym sprzęcie. Jeśli interesuje Was ten temat i chcielibyście, abym opisał cały proces projektowania i uczenia sieci, dajcie proszę znać w komentarzach. Na koniec chciałbym jeszcze przedstawić Wam cytat jednego z największych naukowców 2. połowy XX w.: "Gdy (AI) osiągnie IQ 100 czy 150, nie będzie problemu. Ale gdy 1000 albo 10000, to co wtedy? To może być największe osiągnięcie ludzkości. Ale zarazem jej koniec." Stephen Hawking Bibliografia i inspiracje: Brain-wiki contributors, https://brain.fuw.edu.pl/edu/index.php/Uczenie_maszynowe_i_sztuczne_sieci_neuronowe (data dostępu 10.04.2021 r.) Smith Steven W.: Cyfrowe przetwarzanie sygnałów, BTC,Warszawa 2007 https://www.youtube.com/watch?v=aircAruvnKk https://www.youtube.com/watch?v=437dvcP-09E 1-od tego momentu matematyczny bełkot wszedł na ten poziom, że zacząłeś przewijać dalej?
  22. CUDA na kiju (a nawet GPU) cz. 1 Wstęp Tym artykułem chciałbym zapoczątkować cykl artykułów na temat programowania równoległego z wykorzystaniem kart graficznych (GPU) w języku CUDA C. W części pierwszej przedstawię krótki wstęp teoretyczny tzn. omówię dlaczego równoległe wykonywanie obliczeń może istotnie przyspieszyć działanie programu, opiszę budowę procesorów graficznych, a w dalszych częściach będą przedstawione praktyczne wskazówki z fragmentami kodu programu do mnożenia macierzy. Zapraszam do lektury! Treść artykułu została usunięta, ponieważ użytkownik publikujący ten poradnik wykorzystał w nim materiały, do których nie posiadał odpowiednich praw. Usunięcie nastąpiło na prośbę autora zapożyczonych materiałów.
  23. Dlaczego akurat ten temat? Znaczna część młodzieńców interesujących się elektroniką z oczywistych względów jako szkołę średnią wybiera technikum elektroniczne, a następnie studia powiązane z tymże kierunkiem. Tymczasem zapał u wielu młodych entuzjastów elektroniki jest skutecznie studzony przez system edukacji, który serwuje im zagadnienia czy całe przedmioty, które są zwyczajnie zbędne, a nie uczy ich tego, co naprawdę jest przydatne po zakończeniu edukacji. Z tego całego procesu można wyciągnąć kilka wniosków, którymi warto podzielić się z innymi – mianowicie, dlaczego nie powinno się zniechęcać oraz jak pomimo wątpliwego poziomu edukacji zdobyć umiejętności, które mogą wyróżnić nas pośród przyszłych, potencjalnych pracodawców. 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 » Technikum i 8051 Około 8 lat temu, patrząc na nazwę przedmiotu „układy mikroprocesorowe” przypuszczaliśmy, że będzie on dotyczył AVR. Jednak podczas lekcji powiedziano nam, że będziemy przyswajać architekturę i programowanie mikrokontrolera Intel 8051. Mikrokontrolery te można nazwać technologicznymi dinozaurami; zostały zaprojektowane w 1980 roku i obecnie nie są w zasadzie nigdzie używane – z wyjątkiem polskich szkół i uczelni, dla których przygotowano specjalne urządzenie do nauczania programowania oparte na tym układzie scalonym, nazwane DSM-51, kosztujące na chwilę obecną prawie 2000 zł brutto! Urządzenie DSM-51 Z jednej strony oryginalne mikrokontrolery 8051 są protoplastami całego szeregu dzisiejszych układów, ich język programowania (asembler) jest bardzo niskopoziomowy. Dzięki temu łatwo można zrozumieć np. działanie rejestrów roboczych oraz co się dzieje w momencie wykonywania poszczególnych instrukcji. Z drugiej strony – czy należy poświęcać temu cały semestr? Warto pokazać, że coś takiego istnieje i przy okazji krótko omówić podstawowe zasady działania tego systemu mikroprocesorowego. Ale przez resztę zajęć można wziąć do ręki inny mikrokontroler i spędzić czas nad czymś, co faktycznie jest używane gdzieś poza szkołą – jeżeli nie bezpośrednio w przyszłej pracy zawodowej, to przy nauce programowania na studiach. Do tego celu idealne wydaje się Arduino – przyjazna platforma, która nie jest używana powszechnie chociażby w przemyśle, ale za to świetnie nadająca się do nauki programowania. Właśnie dzięki tej drugiej właściwości wielu amatorów elektroniki używa Arduino w zaciszu domowym, przy okazji poznając podwaliny języków C i C++ (język programowania Arduino jest w czymś rodzaju uproszczonego połączenia tych dwóch języków). Samo Arduino jest relatywnie tanie – oryginalną płytkę Arduino Uno można kupić za kwotę poniżej 100 zł (cena na moment pisania artykułu). Dlaczego zatem nie zastąpić mocno przestarzałego i horrendalnie drogiego DSM-51 czymś, co jest tańsze, nowocześniejsze, bardziej użyteczne i rozwojowe? Wbudowane “peryferia” używane w DSM-51 to m.in.: Wyświetlacze: alfanumeryczny LCD 2x16, Wyświetlacz segmentowy, Klawisze, Dioda LED, Buzzer. Co oferuje Arduino w porównaniu do DSM-51? Modularność i możliwość podłączenia mnóstwa nowocześniejszych, bardziej zaawansowanych technologicznie urządzeń peryferyjnych (wystarczy zajrzeć do kursów dot. Arduino). Już sama znajomość podstaw prosperujących języków C i C++ oraz zaznajomienie się z nowocześniejszą technologią (tu warto przytoczyć specyficzne płytki Arduino z wbudowaną komunikacją Ethernet czy Wi-Fi) deklasuje mikrokontrolery 8051, których dogłębna znajomość w dzisiejszych czasach jest bezcelowa. Arduino Ethernet Rev3 Studia inżynierskie i kolejne rozczarowanie Po nauce programowania w technikum pojawiła się nadzieja, że szkoła wyższa to faktycznie wyższy poziom i że uda się zasiąść do czegoś nowocześniejszego, jak np. AVR czy STM32. Na trzecim roku w planie figuruje przedmiot o analogicznej nazwie do tego, który był w technikum. Na blatach w sali leży ten sam rodzaj płyt edukacyjnych z 8051, które programowano w szkole średniej… Znów semestr poświęcony technologii, której znajomość się nigdzie nie przyda; do tego „kształcenie” odbywa się z legendarną, akademicką surowością – zrozumienie fragmentu programu nie jest zadowalające. Na pytanie wykładowcy nie wystarczy odpowiedzieć poprawnie, własnymi słowami. Odpowiedź powinna zostać wyuczona na pamięć dokładnie tak, jak podano na wykładzie – w przeciwnym razie była uważana za błędną. Programy tworzone podczas zajęć również nie były wyjątkowo finezyjne, bo nie pozwalał na to sprzęt: jednym z zadań było zaprogramowanie kontrolera tak, aby sterował sygnalizacją świetlną na makiecie skrzyżowania (trochę bardziej skomplikowane zapalanie i gaszenie diod po upływie określonego czasu, wykorzystując przerwania). W drugim etapie zajęć do wejść cyfrowych podłączono czujniki przeszkody IR. Pojawienie się przeszkód na danym czujniku i w odpowiedniej kolejności powodowało odpowiednio zwiększenie lub zmniejszenie zawartości licznika, co z kolei miało odpowiadać liczeniu pasażerów w autobusie. Zawartość licznika przedstawiał wyświetlacz 7-segmentowy. Takie programy jedynie uczą pewnych zależności, które równie dobrze da się pokazać na nowocześniejszym sprzęcie, jednocześnie zaznajamiając z jego architekturą czy językiem programowania, co może dać oczywiste benefity w przyszłości. Mikrokontroler Intel P8051 używany w DSM-51. Warto zwrócić uwagę na rok produkcji mikrokontrolera (1982). Szczęśliwie, w następnym semestrze pojawiły się upragnione AVR. Niestety, sam prowadzący zajęcia przyznał, że jeden semestr na porządne poznanie AVR to za mało. Pojawia się pytanie, czy nie lepiej byłoby poświęcić dwa semestry na naukę AVR? Skąd zatem czerpać wiedzę i chęci? Czytając ten artykuł można odnieść wrażenie, że autor próbuje jeszcze bardziej zrazić czytelnika do uczenia się programowania mikrokontrolerów. Nic bardziej mylnego – chce pokazać swoje doświadczenia i dać przekaz młodemu człowiekowi żądnemu wiedzy, że nie ma się co przejmować, gdy po wejściu do klasy zastanie zakurzony i podniszczony sprzęt, który jest być może starszy od jego rodziców. Trzeba wtedy wziąć sprawy we własne ręce - jeżeli szkoła nie jest w stanie przekazać potrzebnej nam wiedzy, to należy zdobyć ją gdzie indziej. Cóż, edukacja w polskich szkołach i na uczelniach jest, jaka jest i ciężko będzie to przeskoczyć; na zmiany też zapewne przyjdzie poczekać. Jednak nie ma się co martwić – przecież istnieją fora czy kursy internetowe. STM32 NUCLEO-F103RB wykorzystywane do nauki programowania w jednym z kursów. Umiejętność programowania tego typu modułów jest pożądana na rynku pracy. Ogromna ilość ludzi pomimo ukończenia edukacji wciąż pogłębia swoją wiedzę, wykorzystując do tego Internet. Nic dziwnego, na rynku pracy pojawiają się interesujące i dobrze płatne oferty, które mogą być poza ich zasięgiem – a to dlatego, że nie są wystarczająco dobrze zaznajomieni z układami takimi jak na przykład AVR czy STM32. Osobom w takiej sytuacji nie pozostaje nic innego, jak zakupienie odpowiedniego sprzętu czy całego zestawu edukacyjnego i wzięcie się do nauki – wtedy uda się poszerzyć horyzonty i znaleźć lepszą pracę, a tak namiętnie męczone przez szkołę mikrokontrolery z poprzedniej epoki wcale nie będą do tego niezbędne.
  24. Trendy współczesnych badaniach naukowych, czyli od elektroniki lamp poprzez spintronikę do dolinotroniki. Mówiąc i pisząc o projektowanych, konstruowanych i testowanych jakichkolwiek układach lub urządzeniach elektronicznych wskazane jest ogólne poszperanie w powstaniu i historii elektroniki. Zgłębiając zasoby literatury przedmiotu można w uporządkowany sposób opisać fakty od odkrycia elektronu, powstania i rozwoju elektroniki, aż do nowych trendów w dociekaniach fizyków jakim jest spintronika oraz coraz intensywniej wkraczająca w naukowe pracownie dolitronika. Kończąc technikum elektroniczne mieliśmy za zadanie wykonanie i obronienie pracy dyplomowej. Był to czas zaniku stosowania lam elektronicznych, przy jednoczesnym zwiększaniu zastosowania elementów półprzewodnikowych w elektronice - tranzystorów i układów scalonych. W ramach tematu obrony wykonaliśmy urządzenie nauczające – testujące, którego działanie było właśnie oparte o te elementy. Na profilu teletransmisja poznaliśmy podstawy elektroniki opartej na układach lampowych, uzupełniając wiedzą o budowie i funkcjonowaniu tranzystorów i tyrystorów. Natomiast szczegóły katalogowe budowy i funkcjonowania układów cyfrowych (scalonych) uzupełnialiśmy już sami, w ramach własnych dociekań, poszukiwań praktycznych i budowaniu prototypów. Wykonana, wspomniana powyżej, praca dyplomowa zawierała elektroniczne lampy wyświetlające czołowo oraz układy scalone- bramki logiczne, realizowane w układzie UCY 7400 (SN 7400) CEMI. Wiadomo, że Lampa elektronowa – element elektroniczny czynny[1] składający się z elektrod umieszczonych w bańce z wypompowanym powietrzem (lampa próżniowa, ang. vacuum tube) lub gazem pod niewielkim ciśnieniem (lampa gazowana, ang. gas-filled tube), w którym wykorzystuje się wiązki elektronów lub jonów poruszające się między elektrodami lampy i sterowane elektrycznie elektrodami. [2] Tak przeczytamy w dostępnych ujęciach medialnych encyklopedycznych. A jak przedstawia rys historyczny odkrycia tych elementów, tak powszechnie królujących w połowie lat XX wieku. Kilka chwil z Historii odkryć fizycznych i technicznych Z praw elektrolizy odkrytych przez Michaela Faradaya (1791-1867) wynika, że ładunek, który przepływa przez elektrolit, zawsze daje się podzielić na pewną ilość równych porcji. Ale sam Faraday nie dostrzegł tego, że właśnie na mocy jego praw, ładunki wszystkich jonów złożone są z całkowitej liczby ładunków elementarnych. Gdy przez z oznaczyć wartościowość jonu, ładunek q, jaki przepłynie przez elektrolit, będzie równy iloczynowi - z ·e, przy czym e jest owym ładunkiem elementarnym (jednostkowym) q = z · e W roku 1874 irlandzki fizyk George Johnstone Stoney badając procesy elektrochemiczne nazywa jednostkowy ładunek, towarzyszący przemianie atomu w jon - elektronem, co więcej, zdołał nawet wyznaczyć, choć niedokładnie, wartość takiego ładunku. Mimo to, że była ona blisko 20 razy mniejsza niż obecnie przyjmowana, widać już grunt dla późniejszych dokonań Thomsona. Fotografia 1. Joseph John Thomson, znany także jako J.J. Thomson (1856-1940) –fizyk angielski związany z Laboratorium Cavendisha w University of Cambridge. W 1906 r. otrzymał Nagrodę Nobla w dziedzinie fizyki „w uznaniu zasług za teoretyczne i eksperymentalne badania nad przewodnictwem elektrycznym gazów”, które doprowadziły do odkrycia elektronu. Schemat doświadczenia Thomsona A-anoda, K- katoda, P- przegroda ze szczeliną, C-kondensator, E-ekran pokryty siarczkiem sodu Źródło: https://upload.wikimedia.org/wikipedia/commons/b/b6/Jj-thomson2.jpg https://fizyka.umk.pl/~marta_985/prezentacja1/odkrycie_elektronu.html Fotografia 2. Sir John Ambrose Fleming (1849-1945) – fizyk, elektrotechnik i radiotechnik angielski. W 1904 r. skonstruował diodę próżniową – schemat po prawej stronie. Była to pierwsza lampa elektronowa – diodę. Źródło: https://pl.wikipedia.org/wiki/John_Ambrose_Fleming https://pl.wikipedia.org/wiki/Dioda_próżniowa#/media/Plik:Vacuum_tubes_pl.svg Fotografia 3. Lee De Forest (1873-1961) amerykański radiotechnik i wynalazca. W 1906 roku zbudował triodę, co umożliwiło rozwój radia, telewizji, radaru oraz komputera. Prawdziwy potencjał tego wynalazku został wykorzystany dopiero w roku 1912, poprzez włączenie go do obwodu wzmacniającego sygnały zarówno radiowe jak i audio. Lampa ta zwana także zaworem triodowym była stosowana przez 50 lat w elektronice, do czasu aż została zastąpiona przez tranzystor, i później przez układy scalone. Jest autorem opracowania metody optycznego zapisywania dźwięku na taśmie filmowej (metoda Photion). W ciągu swego długiego życia uzyskał ponad 300 patentów, ale niestety niewiele z nich odniosło sukces. Źródło: https://upload.wikimedia.org/wikipedia/commons/6/65/Lee_De_Forest.jpg Patent konstrukcje i zastosowanie na stronie https://www.freepatentsonline.com/0879532.pdf Schematy umieszczone w dokumentacji patent na stronie do zapoznania na stronie o adresie https://www.freepatentsonline.com/0879532.pdf Fotografia 4. Irving Langmuir (1881- 1957) – amerykański fizykochemik, pionier badań nad wyładowaniami elektrostatycznymi w gazach i zjawiskami powierzchniowymi. Laureat Nagrody Nobla w dziedzinie chemii w 1932 roku za osiągnięcia w chemii powierzchni. Zatrudnił się w nowo powstałym General Electric Research Laboratory w Schenectady, by pracować nad przepływem prądu przez zjonizowane gazy, konstruując nowe przyrządy do badań. Źródło: https://pl.wikipedia.org/wiki/Irving_Langmuir Pracując dla General Electric w latach 1909–1916 udoskonalił znacznie technikę próżniową i wynalazł pompę dyfuzyjną, co umożliwiło osiąganie wysokiej próżni i poprawiło znacznie parametry produkowanych lamp elektronowych. Opracował podstawy teorii lamp elektronowych, co miało duży wpływ na późniejszy rozwój ich konstrukcji i produkcji. Fotografia 5. Walter Schottky (1886–1976) – niemiecki fizyk Źródło: https://pl.wikipedia.org/wiki/Walter_Schottky W trakcie I wojny światowej Walter Schottky zbudował w zakładach Siemens & Halske lampę z dwiema siatkami – tetrodę. Tetroda – rodzaj lampy elektronowej posiadającej cztery elektrody: anodę, katodę i dwie siatki umieszczone pomiędzy anodą i katodą. Więcej informacji na stronie internetowej o adresie https://pl.wikipedia.org/wiki/Tetroda Widok: lampa tetroda, symbol elektroniczny tej lampy i elementy konstrukcji Źródło: https://upload.wikimedia.org/wikipedia/commons/0/08/B442.JPG https://upload.wikimedia.org/wikipedia/commons/7/7f/Tetrode-Symbol_de.svg https://upload.wikimedia.org/wikipedia/commons/3/30/RE20_int.jpg Studiował fizykę na Uniwersytecie Fryderyka Wilhelma w Berlinie. W roku 1912 obronił pracę doktorską na temat szczególnej teorii względności, ogłoszonej zaledwie siedem lat wcześniej. Do najważniejszych osiągnięć Schottky’ego należą: wynalezienie (wspólnie w 1924 roku z Erwinem Gerlachem) mikrofonu wstęgowego – używanego do dziś w profesjonalnej rejestracji dźwięku, udoskonalenie lamp elektronowych (w latach 1915–1919, podczas pracy dla Siemensa). Jego wynalazki obejmują lampę próżniową z siatką ekranową (1915), tetrodę (system z dwiema siatkami zapobiegający niepożądanym oscylacjom, 1919). Przeprowadzał rewolucyjne badania teoretyczne na temat szumów termicznych i śrutowych w urządzeniach elektronowych, odkrycie dziur w paśmie walencyjnym półprzewodnika, zaproponowanie dyfuzyjnej teorii przepływu prądu przez złącze metal–półprzewodnik oraz wyjaśnił zmianę elektrostatyki na granicy metal / półprzewodnik (efekt Schottky'ego) i odkrył barierę komórkową na granicy między metalem, a półprzewodnikiem (bariera Schottky'ego), która doprowadziła do powstania diody półprzewodnikowej z gorącym nośnikiem (dioda Schottky'ego). Element ten wykazuje nieliniową zależność napięcie / prąd. Jest także imiennikiem wady Schottky'ego, szczególnego rodzaju defektu punktowego w sieci krystalicznej. Przeszedł na emeryturę w 1958 roku i resztę życia spędził w Pretzfeld. Zmarł w dwa lata po wprowadzeniu na rynek przez koncern Siemens diod opartych na złączu metal–półprzewodnik do zastosowań mikrofalowych. Fotografia 6. Przekrój przez 9-milimetrową diodę Schottky’ego Źródło: https://upload.wikimedia.org/wikipedia/commons/9/9c/Schottky_Diode_Section.JPG W tym miejscu kilka informacji o Diodzie Schottky’ego (wym. szotkiego). Jest to dioda półprzewodnikowa, w której w miejsce złącza p-n zastosowano złącze metal-półprzewodnik. Charakteryzuje się małą pojemnością złącza, dzięki czemu typowy czas przełączania wynosi tylko około 100 ps. Diody Schotky'ego o małych wymiarach mogą działać przy częstotliwości dochodzącej do kilkudziesięciu GHz. Odmianą są diody na duże prądy znajdujące zastosowanie w impulsowych urządzeniach energoelektronicznych. Pozwala to na znaczną miniaturyzację tych urządzeń, jak również osiągnięcie dużej sprawności dochodzącej do 90%. Należy podkreślić, że diody Schottky’ego mają dwukrotnie mniejszy spadek napięcia w kierunku przewodzenia (UF = 0,3 V) niż diody krzemowe (UF = 0,6-0,7 V). Należy pamiętać, że maksymalne napięcie wsteczne jest niewielkie, do około 100 V. Następny krok w historii elektroniki to czas dominacji technologii TTL, czyli na przełomie lat 70 i 80. Był to jeden z podstawowych elementów tworzących cyfrowe układy elektroniczne – masowa produkcja rozpoczęta w 1964 roku. Pierwsze patenty na tranzystor zostały udzielone w latach 1925–1930 w Kanadzie, USA i Niemczech Juliusowi Edgarowi Lilienfeldowi. Jego projekty były zbliżone do tranzystora MOSFET, jednak ze względów technologicznych, szczególnie ze względu na czystości materiałów, tranzystora nie udało się skonstruować. Nastąpiło to dopiero w drugiej połowie XX wieku. Fotografia 7. Julius Edgar Lilienfeld (1882-1963) polski fizyk pochodzenia żydowskiego, twórca tranzystora polowego (podstawowego elementu współczesnych układów cyfrowych). Jednak w 1925 wniosek o patent kanadyjski układu będącego protoplastą tranzystora polowego złożył jako obywatel Polski Rysunek obok, to uproszczona budowa tranzystora JFET. S – źródło, G – bramka, D – dren, 1 – obszar zubożony, 2 – kanał. Jest to tranzystor polowy, tranzystor unipolarny (ang. Field Effect Transistor, FET) – tranzystor, w którym sterowanie prądem odbywa się za pomocą pola elektrycznego. Źródło: https://pl.wikipedia.org/wiki/Tranzystor_polowy https://upload.wikimedia.org/wikipedia/commons/5/59/Julius_Edgar_Lilienfeld_(1881-1963).jpg Pierwszy raz opatentowany w 1926 roku przez Juliusa Edgara Lilienfelda. http://skfiz.wdfiles.com/local--files/archiwum-2007-08/Spotkanie SKFiz 14I2008 - Tomasz Dietl.pdf Pierwszy działający tranzystor ostrzowy został skonstruowany 16 grudnia 1947 r. John Bardeen, Walter Houser Brattain oraz William Bradford Shockley, otrzymali Nagrodę Nobla z fizyki w 1956 roku za wynalazek tranzystora. https://upload.wikimedia.org/wikipedia/commons/b/bf/Replica-of-first-transistor.jpg Fotografia 8. John Bardeen (1908-1991) jest jedyną osobą, która dwukrotnie otrzymała Nagrodę Nobla w dziedzinie fizyki: w roku 1956 wspólnie z Williamem Shockleyem i Walterem Brattainem za badania półprzewodników i wynalezienie tranzystora William Bradford Shockley (1910-1989) odkrywca w roku 1950 opisującego diody prawa nazwanego jego imieniem i konstruktor, rok później, tranzystora bipolarnego. Walter Houser Brattain (1902-1987) Źródło: https://upload.wikimedia.org/wikipedia/commons/4/4a/Bardeen.jpg https://pl.wikipedia.org/wiki/William_Shockley https://upload.wikimedia.org/wikipedia/commons/f/f8/William_Shockley%2C_Stanford_University.jpg https://upload.wikimedia.org/wikipedia/commons/c/c4/Brattain.jpg Spintronika to elektronika przyszłości "Wyobraźmy sobie „kanapkę”, w skład której wchodzi materiał ferromagnetyczny, taki jak żelazo, między którym umieszczamy materiał „normalny”, jak miedź. Okazuje się, że opór elektryczny takiej „kanapki” jest bardzo silnie uzależniony od pola magnetycznego" - tłumaczy fizyk, prof. Tomasz Dietl, pracujący w Instytucie Fizyki PAN. Badania dotyczące półprzewodników ferromagnetycznych stały się podstawą nowej dziedziny wiedzy – spintroniki, której początki sięgają lat 80. XX wieku i jednocześnie wykrytego tzw. gigantycznego zjawiska magnetooporu. Spintronika półprzewodnikowa, nowa dziedzina elektroniki, to szczególnie badania nad materiałami, które łączą zalety materiałów magnetycznych, takich jak żelazo i półprzewodnikowych oraz takich jak krzem. Unikatowe własności półprzewodników wykorzystują np. tranzystory w mikroprocesorach oraz lasery półprzewodnikowe w odtwarzaczach płyt kompaktowych, DVD, a także w komunikacji światłowodowej. Cała idea opiera się na fakcie, że elektron ma nie tylko ładunek elektryczny, o odkryciu którego napisano wcześniej, dotychczas widoczny w "klasycznej" elektronice, ale także spin. To kierunek, w którym elektron obraca się wokół własnej osi. Właściwości spinu wykorzystywane są m.in. w pamięciach magnetycznych komputerów, a od tysięcy lat w igle magnetycznej, która jest zbudowana ze spinów elektronowych, ustawionych w jednym kierunku. Tranzystor spinowy działający według nowych zasad został skonstruowany przez zespół fizyków z Instytutu Fizyki Polskiej Akademii Nauk w Warszawie i Uniwersytetu w Ratyzbonie. Doświadczalna demonstracja tranzystora jest kolejnym krokiem ku upowszechnieniu spintroniki, dziedziny nauki i techniki, która w przyszłości w istotnej części zastąpi elektronikę. „Tranzystor spinowy wykorzystuje nie ładunek elektryczny, a inną cechę kwantową elektronu: spin, czyli jego wewnętrzny momentu pędu", wyjaśnia w literaturze przedmiotu prof. Tomasz Wojtowicz z IF PAN. Elektronika i spintronika to nie koniec. Czas na dolinotronikę! Elektrony w grafenie niosą nie tylko ładunek elektryczny i informację o spinie, ale również informację o tzw. pseudospinie. Intensywne badania ostatniej z tych cech umożliwić mogą rozwój zupełnie nowego działu technologii – dolinotroniki. Analogicznie do elektroniki i spintroniki. Teorię, która może pomóc w rozwoju dolinotroniki tworzy prof. Adam Rycerz z Instytutu Fizyki Uniwersytetu Jagiellońskiego. "Moje badania dotyczyły teoretycznych podstaw działania części składowych grafenowego komputera kwantowego" - mówi profesor. Badacz na warsztat wziął to, co dzieje się w gazie elektronów uwięzionych w grafenie. Grafen to atomowej grubości struktura złożona z węgla. Ma bardzo interesujące właściwości, wśród których wymienia się: świetne przewodzenie prądu i ciepła oraz rozciągliwość i wytrzymały. Wyjątkowe cechy grafenu związane są m.in. z dziwaczną naturą kwantową elektronów z atomów węgla, które poruszają się w tej strukturze. Otóż elektrony - cząstki przecież obdarzone masą, w grafenie zachowują się - przynajmniej z punktu widzenia obserwatora - jakby nie miały masy - opowiada naukowiec i dodaje: "Elektrony nie są takie jak ludzie - one są identyczne, nierozróżnialne". Wyjaśnia jednak, że cząstki takie w grafenie daje się opisać za pomocą liczb kwantowych - stopni swobody. "Najprostszą liczbą kwantową jest ładunek. Bo elektron ma swój ładunek elektryczny. Drugą liczbą kwantową jest spin. Można powiedzieć, że elektron ma mały magnesik, który może być obrócony w górę lub w dół" - opowiada. Dodatkowo jednak, kiedy elektron znajduje się np. w grafenie, cząstka zyskuje kolejny stopień swobody - tzw. sztuczny spin, czy też pseudospin. To cecha niezależna od zwykłego spinu. Może się więc okazać, że elektron w grafenie znajduje się w dolinie K albo K\'. "Matematycznie jest to tak, jakby istniał fikcyjny magnesik w innej przestrzeni" - opowiada fizyk. Operowanie ładunkami umożliwiło rozwój elektroniki (prąd - elektron z ładunkiem albo jest, albo go nie ma). Manipulowanie kolejnym stopniem swobody elektronów - spinem - umożliwiło powstanie spintroniki. A umiejętność operowania pseudospinem otwiera przed nami nową dziedzinę technologii - dolinotronikę (valleytronics). Przeprowadzono i opisano w literaturze przedmiotu mnóstwo doświadczeń i eksperymentów oraz zastosowań, że śmiało można wskazać, iż o spintronice i dolinotronice można napisać oddzielne artykuły i opracowania. Teks ten na wstępie ukazał rys historyczny powstania elektroniki, zarówno lam jak i przejścia do elektroniki półprzewodników, ale ma zachęcić czytelnika do dociekań o nowych trendach w badaniach naukowców, których efekty wkrótce odczujemy. [1] Element elektroniczny czynny (aktywny) – element elektroniczny, który jest w stanie wzmocnić prąd elektryczny w odpowiednim układzie elektronicznym. Taki układ można określić jako przetwornik energii elektrycznej na siłę elektromotoryczną. Najważniejsze elementy elektroniczne czynne to: tranzystor, lampa elektronowa. Układy elektroniczne zawierające elementy czynne zwane są układami aktywnymi. https://pl.wikipedia.org/wiki/Element_elektroniczny_czynny [2] https://pl.wikipedia.org/wiki/Lampa_elektronowa Artykuł_2.pdf
  25. Na wstępie chciałbym zaznaczyć, że jest to artykuł napisany pod kątem wyłącznie hobbystów, pasjonatów i osób, tak jak ja, uczących się elektroniki i programowania. Ponieważ niektóre zagadnienia są dużo bardziej skomplikowane, nie mogą być przenoszone 1:1 dla producentów seryjnych podzespołów czy też działalności stricte zawodowej. Natomiast postaram się w tej serii artykułów opisać podstawowe problemy prawne na jakie może natrafić każda osoba chcąca sprzedać wykonane projekty. 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 » Spis treści: 1. Podstawy – Czy mogę sprzedawać projekty DIY? 2. Znak CE – co to jest i co się z tym wiąże 3. Odpowiedzialność twórcy 1. Podstawy Często spotykam się z nierozumieniem rozmówców dotyczącym pojęcia prawodawstwa i całego systemu prawnego. W świecie prawniczym istnieje metoda rozumienia tekstów ustaw, dyrektyw, rozporządzeń itd., zwana wykładnią. Jest to zestaw zasad pomagający rozumieć tekst w sposób w jaki autor (ustawodawca) chciał aby to było zrozumiane. Działa to trochę na zasadzie tekstu źródłowego i bibliotek: teoretycznie bez nich program mógłby działać, ale za bardzo nie wykonuje tego co miał robić. Dlatego nie wystarczy przeczytać jeden akt prawny, a trzeba spojrzeć na sprawę szerzej i korzystać z definicji legalnych ustanowionych przez autora. Czyli po ludzku musimy przeczytać więcej dokumentów napisanych trudnym językiem oraz mając na uwadze podstawy prawodawstwa. Głównym aktem normatywnym (ustawą), na którym opieramy większość relacji międzyludzkich, jest kodeks cywilny. Reguluje on praktycznie wszystko od zasad sprzedaży, poprzez sprawy spadkowe, aż do szkód wyrządzonych przez zwierzęta wchodzące w miedzę. Dlatego też do niego zawsze będziemy się odnosić w przypadku różnego rodzaju umów sprzedaży, gwarancji, rękojmi oraz odpowiedzialności twórcy. Natomiast należy pamiętać, że są to tylko ogólniki i większość z tych przepisów regulowana jest szczegółowo przez inne ustawy jak np. Ustawa o prawach konsumenta, Ustawa o prawie autorskim i prawach pokrewnych. Przy niektórych specjalistycznych zagadnieniach mogłyby jeszcze być istotne rozporządzenia ale tak jak wspomniałem na wstępie, bierzemy pod uwagę sytuację tylko wariant hobbysty, amatora, konstruującego „migającą diodę”. Nie bez przyczyny wspomniałem o tych dwóch dodatkowych ustawach, ponieważ są one dosyć istotne w przypadku chęci sprzedaży naszych projektów. Jeśli zdecydujemy się na sprzedaż online, jesteśmy zobligowani do przestrzegania przepisów Ustawy o prawach konsumenta, brać pod uwagę możliwość zwrotu towaru bez podania przyczyny, możliwość reklamacji i pełną odpowiedzialność za sprzedawany produkt. Ale czy zawsze? Odpowiedź brzmi: NIE Po pierwsze – jeśli jesteśmy hobbystą, nie posiadającym własnej działalności gospodarczej, nie podlegamy pod tę ustawę. Po prostu jesteśmy zwykłym chałturnikiem, który w świetle prawa nie jest podmiotem profesjonalnym – coś jak grajek na weselach, robi to tylko okazjonalnie. Teoretycznie jeśli przedmiot powstaje tylko po, żeby go sprzedać, powinniśmy jednak zarejestrować naszą działalność, gdyż jest to delikt podatkowy. Jednakże w przypadku gdy tworzymy sobie jakiś projekt dla zabawy i później po prostu chcemy go sprzedać, nie ma tutaj przeciwskazań, a powyższa ustawa nie ma zastosowania. Dlatego też sprzedając go na portalu aukcyjnym, kupujący nie ma prawa zwrotu bez podania przyczyny. Podstawa prawna: art. 27 w zw. z art. 1 pkt. 1 ustawy o prawach konsumenta z dnia 30 maja 2014 r. (Dz. U. 2014 poz. 827) Dodatkowo istnieje wyłączenie w powyższej ustawie dotyczącej sprzedaży na odległość produktów wytworzonych na zlecenie kupującego, personalizowanych itp. W takim przypadku również następuje wyłączenie z prawa do zwrotu towaru, nawet jeśli jesteście przedsiębiorcą. Piszę to nie przez przypadek, gdyż przy małej modyfikacji kodu, jesteście w stanie pozbyć się sporego problemu związanego ze zwrotami, szczególnie jeśli sprawa dotyczy projektów DIY. Podstawa prawna: art. 38 pkt. 3 ustawy o prawach konsumenta z dnia 30 maja 2014 r. (Dz. U. 2014 poz. 827) Drugą ustawą o której wspomniałem jest Ustawa o prawie autorskim i prawach pokrewnych. Bardzo często w różnych dyskusjach na forach internetowych pomijany jest fakt, że w świetle prawa zarówno kod źródłowy programu jak, projekt układu elektronicznego czy też sama płytka z zainstalowanymi konkretnymi podzespołami elektronicznymi, jest chroniona prawem autorskim. Oczywiście w przypadku udostępnianego oprogramowania istnieją licencje na zasadzie których autor je udostępnia, jednakże większość z nich jest tylko do użytku osobistego bez możliwości zarabiania na ich odsprzedaży. Czyli nie każdy projekt DIY możemy sprzedawać masowo ale jeśli wykonaliśmy go dla siebie, zawsze możemy go odsprzedać komuś innemu (natomiast pozwać mogą nas i tak). Podstawa prawna: art. 1 ust. 2 pkt. 1 w zw. z art. 1 ust. 1 ustawy o prawie autorskim i prawach pokrewnych z dnia 4 lutego 1994 r. (Dz. U. 1994 Nr 24 poz. 83) Pytanie: Czy w związku mogę sprzedać swój projekt DIY? Odpowiedź brzmi: TAK Co więcej, kodeks cywilny umożliwia przetwarzanie rzeczy zakupionych/znalezionych, daje dodatkowe prawa kiedy podnosi się jej wartość. Z punktu widzenia polskich przepisów nie jest zabroniona sprzedaż materiałów elektronicznych, składanych z innych podzespołów, które dodatkowo wyposażone są w autorskie oprogramowanie. Oczywiście nie mogą być to produkty niebezpieczne, czy też specjalistyczne, które obowiązują oddzielne przepisy – jak np. sprzęt medyczny czy zapalniki do materiałów wybuchowych lub broń. Mógłbym zakończyć w tym miejscu, niestety pozostaje nam jeszcze kwestia sprawy związanej ze znakiem CE, który jest wymagany na produktach wprowadzonych do obrotu na ternie Unii Europejskiej. O tym w kolejnej części.
×
×
  • 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.