[105] Arduino i sieć cz. 2

[105] Arduino i sieć cz. 2

„Zobacz przez kamerkę czy jest miejsce przed domem!” – a figa, nie ma połączenia. Klimy też nie włączymy zdalnie ani pieca, bo apka się nie łączy. Coś się popsuło i statystycznie najczęściej „coś” naprawia się za pomocą wyciągnięcia wtyczki z kontaktu, policzenia do dwudziestu i wsadzenia jej ponownie. Przynajmniej ja tak robię od 30 lat, na szczęście coraz rzadziej.


Skoro w poprzednim artykule powstał szkic diagnostyczny, trzeba go przerobić w szkic użytkowy. I tutaj dygresja: odkąd świat światem, człowiek lubił sobie oznaczać różne stany kolorkami. Zwykle zielone znaczyło: dobrze, czerwone – źle, żółte – coś nie tak albo „zaraz się zobaczy”. Kolorki mają jedną zaletę: są tanie i łatwe w realizacji. Wadę też mają: są powściągliwe i mało rozmowne. Jednak w maszynie do resetowania routera wiele nie potrzeba. A w ogóle stworzymy model hybrydowy, gadający przez terminal i świecący kolorami jednocześnie.

Na początek wrócę do mojej ulubionej płytki edukacyjnej TME, bo tam już dioda jest i to RGB – siedzi na pinach 9, 10 i 11. Wykorzystam tylko pierwsze dwie struktury, mogąc prezentować trzy barwy, w tym żółtą. Czas zabrać się za program i na razie nie będziemy tu nic zmieniać ideą, a tylko dołożymy elementy związane ze zmianą koloru diody świecącej.

#include "WiFiS3.h"              // Biblioteka obsługująca WiFi na Arduino R4
char ssid[] = "Smialek";         // Nazwa sieci WiFi.
char pass[] = "1234";            // Hasło do sieci WiFi.
int modulWiFi = WL_IDLE_STATUS;  // Zmienna stanu WiFi.

const char* adresWWW = "www.google.com";  // Adres stromy do badania dostępności internetu.
const int opoznienie = 3000;              // Opóźnienie dla ustabilizowania połączeń z WiFi.
int czas;                                 // Czas w milisekundach dla funkcji ping.
const byte czerwonyLed = 9;               // Czerwona dioda.
const byte zielonyLed = 10;               // Zielona dioda.

void setup() {
  pinMode(czerwonyLed, OUTPUT);
  pinMode(zielonyLed, OUTPUT);
  czerwony();            // Włącz czerwone światło.
  Serial.begin(115200);  // Inicjuj monitor.
  while (!Serial) {}     // Czekaj dopóki nie będzie połączenia z portem szeregowym.

  Serial.print("Dzień dobry. Jestem aplikacją nadzorującą punkt dostępu WiFi. Czekaj...");
  if (WiFi.status() == WL_NO_MODULE) {  // Sprawdź połączenie z modułem WiFi na płytce.
    Serial.println(" Niestety moduł WiFi na płytce Arduino nie odzywa się. Spróbuj zaktualizować firmware.");
    while (true) {}  // Brak połączenia z modułem WiFi, zakończ pracę.
  }
  Serial.println(" Inicjacja przebiegła prawidłowo.");
  Serial.print("Spróbuję teraz połączyć się z punktem dostępu WiFi o nazwie \"");
  Serial.print(ssid);  // SSID
  Serial.print("\". Czekaj...");
  zolty();                               // Włącz żółte światło.
  while (modulWiFi != WL_CONNECTED) {    // Czekaj, dopóki nie połączysz się z punktem dostępu WiFi.
    modulWiFi = WiFi.begin(ssid, pass);  // Procedura łączenia WiFi z punktem dostępu.
    delay(opoznienie);                   // Opóźnienie niezbędne do ustabilizowania się połączenia.
  }
  zielony();  // Włącz zielone światło.
  Serial.println(" Połączenie powiodło się.");
  Serial.print("Adres bramy: ");
  Serial.println(WiFi.gatewayIP());  // IP bramy.
  Serial.print("Przydzielony adres: ");
  Serial.println(WiFi.localIP());  // Przydzielone IP
  Serial.print("Siła sygnału WiFi (RSSI): ");
  Serial.print(WiFi.RSSI());  // RSSI
  Serial.println(" dBm");
  Serial.println("");
}
void loop() {
  czerwony();                                                // Włącz czerwone światło.
  Serial.println("Wysyłam ping na adres bramy. Czekaj...");  // Badamy lokalne połączenie z bramą.
  czas = WiFi.ping(WiFi.gatewayIP());                        // Ping adresu bramy.
  if (czas > 0) {
    zolty();  // Włącz żółte światło.
    Serial.print("Czas odpowiedzi: ");
    Serial.print(czas);
    Serial.println(" ms");
  } else {  // Brak odpowiedzi z bramy.
    Serial.println("Połączenie z bramą zostało zerwane. Będę próbował połączyć się ponownie.");

    modulWiFi = WiFi.disconnect();         // Odłącz się od sieci WiFi
    delay(opoznienie);                     // Opóźnienie niezbędne do ustabilizowania się połączenia.
    while (modulWiFi != WL_CONNECTED) {    // Czekaj, dopóki nie połączysz się z punktem dostępu WiFi.
      modulWiFi = WiFi.begin(ssid, pass);  // Procedura łączenia WiFi z punktem dostępu.
      delay(opoznienie);                   // Opóźnienie niezbędne do ustabilizowania się połączenia.
    }
  }
  Serial.print("Siła sygnału WiFi (RSSI): ");  // Badamy poziom sygnału WiFi
  Serial.print(WiFi.RSSI());                   // RSSI
  Serial.println(" dBm");

  Serial.print("Wysyłam ping na adres \"");  // Badamy połączenie z wybraną stroną internetową.
  Serial.print(adresWWW);
  Serial.println("\". Czekaj...");
  czas = WiFi.ping(adresWWW);  // Ping wybranej strony internetowej.
  if (czas > 0) {
    zielony();  // Włącz zielone światło.
    Serial.print("Czas odpowiedzi: ");
    Serial.print(czas);
    Serial.println(" ms");
  } else {  // Brak odpowiedzi z wybranej strony internetowej.
    Serial.println("Internet jest niedostępny.");
  }
  Serial.println("");
  delay(opoznienie);
}
void czerwony() {
  digitalWrite(czerwonyLed, HIGH);
  digitalWrite(zielonyLed, LOW);
}
void zolty() {
  digitalWrite(czerwonyLed, HIGH);
  digitalWrite(zielonyLed, HIGH);
}
void zielony() {
  digitalWrite(czerwonyLed, LOW);
  digitalWrite(zielonyLed, HIGH);
}

Na początku więc osadzimy deklaracje pinów związanych z ledami i pozamieniamy je w porty, jak to się zwykle robi. Żeby sprawę sobie uprościć, powołałem do życia trzy podprogramy: czerwony, żółty i zielony. W każdym w prosty sposób co trzeba świecę, a co nie – pogaszę.

Na samym wstępie włączę światło czerwone. Bo jeszcze nic nie działa. Po dogadaniu się z modułem WiFi na płytce zmienię kolor na żółty i będę czekał na połączenie z routerem. Jeśli połączenia nie będzie, pozostaniemy w żółci do czasu, aż połączenie nastąpi. Innymi słowy: świecący się w nieskończoność żółty kolor oznacza, że router wyjechał na wakacje.

Jeśli jednak połączenie się powiedzie, dioda zmieni kolor na zielony, nastąpi wspomniana w ostatnim artykule odpytująca litania i przejdziemy do głównej pętli. Na samym początku dioda zmieni kolor na czerwony i do routera zostanie wysłany ping. Jeśli powróci, zmienimy kolor na żółty, jeśli nie – dioda nadal pozostanie czerwona. Gdy jednak ta część operacji uda się, ping wyślemy na wybraną stronę, czyli w tym wypadku na google. I tak samo, gdy powróci – dioda zmieni kolor na zielony. Jeśli nie – pozostanie żółta. Jak więc interpretować kolory diody już po pierwszym połączeniu się z routerem?

  1. Praca prawidłowa będzie oznajmiana kolorem zielonym, przerywanym na chwilę czerwonym – to pingowanie routera i żółtym – wysyłanie pinga na Google.

  2. Brak połączenia z internetem – ale nie z routerem – będzie pozbawiony fazy zielonej.

  3. Gdy router w ogóle się nie odezwie, będziemy widzieć tylko kolor czerwony.

Skoro dorobiliśmy sobie fajny wskaźnik, czas z teorii przejść w praktykę: zróbmy urządzenie, które w razie awarii na chwilę odłączy zasilanie od routera. Przy czym każdy już musi zadecydować co chce uzyskać: można na przykład resetować router, podkradając się pod przycisk resetu, zrównoleglając go transoptorem – jak to robiłem w serii artykułów o hackowaniu interfejsów. Bardziej radykalne działanie to odłączenie zasilania i to nie na ułamek sekundy, a nieco dłuższy czas. W przypadku krakowskiej neostrady warto odłączyć się na co najmniej 30 sekund. Podobno przydzielany jest wtedy nowy adres. Nie wiem ile w tym prawdy, ale bywa, że pingi rosną w jakieś straszne wartości i takie działanie przywraca je do normalności. Postanowiłem więc dorobić sterowanie przekaźnikiem, który w razie potrzeby będzie aktywowany na ustalony czas.

Nie będę tutaj przedstawiał rozwiązań mechanicznych. Przypomnę tylko, że znakomita większość przekaźników posiada potrójne złącza, z czego jedno jest wspólne, a z pozostałych jedno jest z nim zwarte, gdy przekaźnik jest włączony, a drugie – gdy wyłączony. Nietypowo użyjemy zwykle nieużywanej pary, zwartej przy niezasilanym przekaźniku. Nieaktywny będzie dostarczał napięcia do listwy, do której będzie podłączony router i co tam jeszcze będzie potrzebne, a po aktywowaniu go – napięcie z listwy będzie odłączane. Takie rozwiązanie jest oszczędniejsze i bezpieczniejsze. Gdyby samo Arduino padło, listwa będzie zasilana cały czas. O tym jak podłączyć przekaźnik, wspominałem już w kilku artykułach, a internet jest pełen opisów zasilania tegoż za pomocą tranzystora. Pamiętajmy tylko o diodzie na cewce przekaźnika, by gasić przepięcia. Przekaźnik powinien rozłączać linię niskiego napięcia (zwykle 9 lub 12 woltów), łączącą zasilacz z routerem, co będzie znacznie bezpieczniejsze od odłączania napięcia sieciowego, a także bezpieczniejsze dla samego urządzenia zasilanego, które nie będzie narażone na podawanie napięć nieustalonych.

Idźmy do szkicu. Nie będzie to jednak poprzednia wersja uzupełniona elementami związanymi z przekaźnikiem, a mocno zreformowana. Nie jest bowiem dla nas istotne czy popsuł się router, czy jego połączenie z internetem – oczywiście z perspektywy bycia poza domem. Dlatego będziemy badać tylko pingi związane z internetem. W końcu te są nam do szczęścia potrzebne. Uprości to nieco program. Zrobimy także jeszcze jedno uproszczenie: w części wstępnej ograniczymy się wyłącznie do nawiązania połączeń między kontrolerami na płytce. Połączenia z routerem będą realizowane w głównej pętli.

// Deklaracje
char ssid[] = "Smialek";                  // Nazwa sieci WiFi.
char pass[] = "1234";                     // Hasło do sieci WiFi.
const char* adresWWW = "www.google.com";  // Adres stromy do badania dostępności internetu.
const int opoznienie = 3000;              // Opóźnienie w ms dla ustabilizowania połączeń z WiFi.
const int przerwa = 10000;                // Przerwa między wysyłaniem pingów.
const byte iloscProb = 10;                // Ilość prób połączenia z routerem, zanim zostanie zresetowany.
const int czasWylaczenia = 35000;         // Czas w ms wyłączenia routera.
const int czasWlaczenia = 60000;          // Czas w ms wyłączenia routera.

#include "WiFiS3.h"              // Biblioteka obsługująca WiFi na Arduino R4
int modulWiFi = WL_IDLE_STATUS;  // Zmienna stanu WiFi.
int czas;                        // Czas w milisekundach dla funkcji ping.
byte numerProby = iloscProb;     // Numer bieżącej próby połączenia się z wybraną stoną interneetową.
const byte czerwonyLed = 9;      // Czerwona dioda.
const byte zielonyLed = 10;      // Zielona dioda.
const byte przekaznik = 13;      // Przekaźnik rozłączający router.

void setup() {
  pinMode(czerwonyLed, OUTPUT);
  pinMode(zielonyLed, OUTPUT);
  pinMode(przekaznik, OUTPUT);
  czerwone();            // Włącz czerwone światło.
  Serial.begin(115200);  // Inicjuj monitor.
  while (!Serial) {}     // Czekaj dopóki nie będzie połączenia z portem szeregowym.
  Serial.print("Dzień dobry. Jestem aplikacją nadzorującą punkt dostępu WiFi. Czekaj...");
  if (WiFi.status() == WL_NO_MODULE) {  // Sprawdź połączenie z modułem WiFi na płytce.
    Serial.println(" Niestety moduł WiFi na płytce Arduino nie odzywa się. Spróbuj zaktualizować firmware.");
    while (true) {}  // Brak połączenia z modułem WiFi, zakończ pracę.
  }
  Serial.println(" Inicjacja przebiegła prawidłowo.");
}
void loop() {
  zolte();  // Badamy połączenie z wybraną stroną internetową, włącz żółte światło.
  Serial.println("");
  Serial.print("Nazwa punktu dostępu WiFi: \"");
  Serial.print(ssid);  // SSID
  Serial.println("\"");
  Serial.print("Adres bramy: ");
  Serial.println(WiFi.gatewayIP());  // IP bramy.
  Serial.print("Przydzielony adres: ");
  Serial.println(WiFi.localIP());  // Przydzielone IP
  Serial.print("Siła sygnału WiFi (RSSI): ");
  Serial.print(WiFi.RSSI());  // RSSI
  Serial.println(" dBm");
  Serial.print("Wysyłam ping na adres \"");
  Serial.print(adresWWW);
  Serial.println("\". Czekaj...");
  czas = WiFi.ping(adresWWW);  // Wyślij ping do wybranej strony internetowej.

  if (czas > 0) {  // Jeśli wrócił...
    zielone();     // Włącz zielone światło.
    Serial.print("Czas odpowiedzi: ");
    Serial.print(czas);
    Serial.println(" ms");
    numerProby = iloscProb;  // Resetuj licznik nieudanych połączeń.

  } else {       // Jeśli nie wrócił...
    czerwone();  // Włącz czerwone światło.
    Serial.println("Internet jest niedostępny. Będę próbował połączyć się ponownie.");
    numerProby--;  // Zmniejszaj licznik nieudanych połączeń.
    Serial.print("Pozostało prób: ");
    Serial.println(numerProby);
    resetWifi();  // Odłącz i podłącz się ponownie do WiFi.

    if (numerProby == 1) {  // Jeśli wyczepał się limit prób połączeń...
      Serial.println("");
      Serial.print("Nastąpi teraz rozłączenie zasilania routera na ");
      Serial.print(czasWylaczenia);
      Serial.println(" ms. Czekaj...");
      digitalWrite(przekaznik, HIGH);  // Wyłącz zasilanie routera.
      delay(czasWylaczenia);           // Czas, przez jaki router będzie odłączony od zasilania.
      digitalWrite(przekaznik, LOW);   // Włącz zasilanie routera.
      Serial.print("Router został włączony ponownie. Za ");
      Serial.print(czasWlaczenia);
      Serial.println(" ms nastąpi próba ponownego połączenia się z routerem. Czekaj...");
      delay(czasWlaczenia);    // Czas na ustabilizowanie się routera po jego włączeniu.
      resetWifi();             // Odłącz i podłącz się ponownie do WiFi.
      numerProby = iloscProb;  // resetuj licznik prób połączeń.
    }
  }
  delay(przerwa);  // Przerwa między wysyłaniem pingów.
}
void resetWifi() {
  modulWiFi = WiFi.disconnect();       // Odłącz się od sieci WiFi.
  delay(opoznienie);                   // Opóźnienie niezbędne do ustabilizowania się połączenia.
  modulWiFi = WiFi.begin(ssid, pass);  // Połącz się z siecią WiFi.
  delay(opoznienie);                   // Opóźnienie niezbędne do ustabilizowania się połączenia.
}
void czerwone() {  // Włącz czerwone światło.
  digitalWrite(czerwonyLed, HIGH);
  digitalWrite(zielonyLed, LOW);
}
void zolte() {  // Włącz żółte światło.
  digitalWrite(czerwonyLed, HIGH);
  digitalWrite(zielonyLed, HIGH);
}
void zielone() {  // Włącz zielone światło.
  digitalWrite(czerwonyLed, LOW);
  digitalWrite(zielonyLed, HIGH);
}

Zacznijmy jednak od początku: wydzieliłem blok Deklaracje i tutaj siedzą znane już: nazwa sieci, hasło, strona do pingowania, opóźnienia konfiguracyjne, ale też nowe pozycje. przerwa oznacza czas między wysyłaniem pingów na stronę. Trzeba ją dobrać rozsądnie i myślę, że nawet minuta wystarczyłaby. Na pewno nie ma sensu słać je z maksymalną szybkością. Strony tego nie lubią i moglibyśmy nawet dostać bana. Tak do końca nie wiem czy stałe pingowanie z jednego adresu, nawet tak rzadkie, ale regularne nie jest jakoś niechętnie widziane, ale internety zalecają google i nikt nie pisze, że miał z tym jakieś problemy. Tak czy inaczej, 10 sekund to dobry czas minimalny.

iloscProb połączeń także jest istotna. Jedno zgubienie pinga to jeszcze nie koniec świata. Dopiero jak padną wszystkie po kolei, warto resetować router. Pamiętać należy, że ilość prób razy opóźnienie da nam czas reakcji i reset routera. W tym wypadku jest to sto sekund.

Pozostała para określająca czas wyłączenia routera oraz opóźnienie po jego włączeniu. Pierwszy należy dobrać do zastanych warunków. Jak mówiłem – Orange wymaga minimum 30 sekund. Czas włączenia zaś także dobieramy doświadczalnie na podstawie obserwacji, jak długo router budzi się do życia, wraz z możliwością logowania się do niego i korzystania z internetu.

Pozostałe deklaracje powielają się, z tym że dołożyłem tutaj wyjście na przekaźnik. To pin trzynasty, na którym siedzi fabryczna dioda świecąca, zdublowana na płytce edukacyjnej. Zawsze przyda się widzieć, jaki jest stan przekaźnika.

Z części wstępnej wyleciało większość rzeczy związanych z WiFi. Pozostawiłem tylko blok inicjacji samego modułu sieciowego na płytce Arduino. Ma to tę wadę, że wpadając do głównej pętli na wstępie nie będziemy widzieć sieci, nawet jeśli ta będzie udostępniona. Jednak blok obsługi awarii od razu się z nią połączy. Przeanalizujmy więc pętlę główną.

zolte();  // Badamy połączenie z wybraną stroną internetową, włącz żółte światło.
Serial.println("");
Serial.print("Nazwa punktu dostępu WiFi: \"");
Serial.print(ssid);  // SSID
Serial.println("\"");
Serial.print("Adres bramy: ");
Serial.println(WiFi.gatewayIP());  // IP bramy.
Serial.print("Przydzielony adres: ");
Serial.println(WiFi.localIP());  // Przydzielone IP
Serial.print("Siła sygnału WiFi (RSSI): ");
Serial.print(WiFi.RSSI());  // RSSI
Serial.println(" dBm");

Włączamy diodę żółtą – oznaczającą fazę testującą. Po kolei wysyłamy na terminal informację o nazwie sieci WiFi, IP bramy, przydzielonym IP i sile sygnału. Tak przy okazji, cudzysłów jest znakiem nielegalnym, dlatego by go zobaczyć na terminalu, należy dopisać przed nim slash.

Serial.print("Wysyłam ping na adres \"");
Serial.print(adresWWW);
Serial.println("\". Czekaj...");
czas = WiFi.ping(adresWWW);  // Wyślij ping do wybranej strony internetowej.

if (czas > 0) {  // Jeśli wrócił...
  zielone();     // Włącz zielone światło.
  Serial.print("Czas odpowiedzi: ");
  Serial.print(czas);
  Serial.println(" ms");
  numerProby = iloscProb;  // Resetuj licznik nieudanych połączeń.
}

Następnie na ustalony adres wysyłamy ping. Jeśli powrócił, wyświetlamy zielone światło, wysyłamy na terminal informację o opóźnieniu, resetujemy licznik prób połączeń do wartości domyślnej i po ustalonej przerwie zaczynamy przepytywankę od początku. Jeśli jednak ping nie nadejdzie, zacznie się procedura obsługi awarii.

czerwone();  // Włącz czerwone światło.
Serial.println("Internet jest niedostępny. Będę próbował połączyć się ponownie.");
numerProby--;  // Zmniejszaj licznik nieudanych połączeń.
Serial.print("Pozostało prób: ");
Serial.println(numerProby);
resetWifi();  // Odłącz i podłącz się ponownie do WiFi.

Zaczynamy od ustawienia czerwonego światła i wysłania smutnego komunikatu. Następnie będziemy zmniejszać licznik prób, bo ten jest tu odliczany do tyłu, a to po to, żeby wiedzieć ile prób jeszcze pozostało. Pojawia się tutaj nowa funkcja – przeniesiona do podprogramu: odłączmy się od WiFi.

void resetWifi() {
  modulWiFi = WiFi.disconnect();       // Odłącz się od sieci WiFi.
  delay(opoznienie);                   // Opóźnienie niezbędne do ustabilizowania się połączenia.
  modulWiFi = WiFi.begin(ssid, pass);  // Połącz się z siecią WiFi.
  delay(opoznienie);                   // Opóźnienie niezbędne do ustabilizowania się połączenia.
}

Do końca nie wiem czy tak trzeba, ale z zasady przed podłączeniem się do czegoś, z czym już się łączyliśmy, warto się odłączyć – nawet jak to coś trafiło do hadesu. Zaraz po odłączeniu spróbujemy się połączyć ponownie. I tutaj wracamy do głównej pętli, chyba że…

if (numerProby == 1) {  // Jeśli wyczepał się limit prób połączeń...
  Serial.println("");
  Serial.print("Nastąpi teraz rozłączenie zasilania routera na ");
  Serial.print(czasWylaczenia);
  Serial.println(" ms. Czekaj...");
  digitalWrite(przekaznik, HIGH);  // Wyłącz zasilanie routera.
  delay(czasWylaczenia);           // Czas, przez jaki router będzie odłączony od zasilania.
  digitalWrite(przekaznik, LOW);   // Włącz zasilanie routera.
  Serial.print("Router został włączony ponownie. Za ");
  Serial.print(czasWlaczenia);
  Serial.println(" ms nastąpi próba ponownego połączenia się z routerem. Czekaj...");
  delay(czasWlaczenia);    // Czas na ustabilizowanie się routera po jego włączeniu.
  resetWifi();             // Odłącz i podłącz się ponownie do WiFi.
  numerProby = iloscProb;  // resetuj licznik prób połączeń.
}

Chyba że licznik prób osiągnął minimum. Interpretujemy tę sytuację, że internetu nie ma na dobre i trzeba zresetować router. Najpierw wyślemy stosowny komunikat. Następnie włączymy obwód przekaźnika, który rozłączy zasilanie listwy. Zaczekamy wspomniany czasWylaczenia, odłączymy napięcie od przekaźnika, wyślemy informację, znowu zaczekamy przez deklarowany czasWlaczenia, po czym spróbujemy połączyć się z routerem i zresetujemy licznik prób. Oczywiście jeśli internetu nadal nie będzie, cykl się powtórzy: dziesięć prób łączenia i znowu resetowanie routera.

I to wszystko. W zasadzie to należałoby jeszcze dorobić kolejny licznik prób, prób resetów tym razem – żeby nie wyłączać i włączać routera w nieskończoność, lecz zakładam, że przy takich parametrach zdarzy się to 10 razy w ciągu godziny. A przecież w domu czasem sypiamy, więc podejmiemy jakieś działania już na miejscu. Co innego, jeśli system jest eksploatowany gdzieś w terenie – wówczas warto o tym pomyśleć i zrobić na przykład godzinne przerwy przy nieudanych połączeniach. Albo po prostu zwiększyć timingi w obecnej konfiguracji.

Pozostaje sprawa dyskusyjna – delay’e. Są okropne, ale… to urządzenie nic więcej nie robi. Więc niech sobie będą. Jak ktoś się uprze, może skomplikować program, tylko po co? To ma robić swoje i dopóki nie potrzebne będą nam dodatkowe moce, można tak to zostawić. No, może nie do końca, bo skoro Arduino pilnuje routera, kto popilnuje Arduino? Ale do tego wrócę wkrótce.

Powiązane tematy

Płytka edukacyjna TME-EDU-ARD-2Płytka edukacyjna TME-EDU-ARD-2Sprawdź tutaj

Przeczytaj również

Nasi partnerzy

TMETech Master EventTME EducationPoweredby
Copyright © 2025 arduino.pl