[123] Zegar z sieci cz. 1
![[123] Zegar z sieci cz. 1](/_next/image?url=https%3A%2F%2Farduino.pl%2Fimgproxy%2FAjNCQGyIftGPnpRS6T4f8IPTGXHsXmFdVEXs9yB87WU%2Ff%3Awebp%2Fw%3A1200%2FbG9jYWw6Ly8vaW1hZ2VzLzAtNjhhL2VlNDg0Lzc0Y2Q1LzAtNjhhZWU0ODQ3NGNkNTUxNzExMDY1OC5wbmc%3D.webp&w=3840&q=75)
Na koniec serii artykułów o współczesnych źródłach dokładnego czasu koniecznie należy omówić najbardziej dziś oczywisty sposób na posiadanie zegarka, czyli pozyskiwanie czasu z sieci. Oczywiście do takich zadań niezbędne będzie Arduino, które z siecią potrafi się porozumieć, ale dziś to nie problem, bo modułów takich jest mnóstwo. Jak zwykle użyję Arduino Uno R4, lecz od razu dodam, że wcale nie jest to konieczne i zaprezentowane przykłady powinny bez problemu pracować z płytkami bazującymi na ESP32, które zresztą siedzi w nowym Uno.
Cóż, na początek – nie będziemy potrzebowali niczego oprócz samej płytki Arduino. Szkic także będzie prosty dzięki bibliotekom robiącym za nas wszystko… no, prawie.
#include <WiFi.h> // Biblioteka obsługująca WiFi
#include <WiFiUdp.h> // Biblioteka obsługująca protokół UDP
#include <NTPClient.h> // Biblioteka pobierająca czas z sieci.
const char* nazwaSieciWiFi = "nazwaSieciWiFi"; // Nazwa sieci WiFi
const char* hasloSieciWiFi = "hasloSieciWiFi"; // Hasło sieci WiFi
const int przesuniecieCzasu = 7200; // Różnica w sekundach między czasem UTC (dla południka zerowego i czasu zimowego).
WiFiUDP zrodloCzasu; // Nazwa obiektu dla serwera czasu.
NTPClient timeClient(zrodloCzasu, "pool.ntp.org", przesuniecieCzasu); // Adres serwera czasu.
void setup() {
Serial.begin(115200); // Inicjuj monitor.
Serial.print("Lacze sie z siecia WiFi o nazwie ["); // Wyślij tekst.
Serial.print(nazwaSieciWiFi); // Wyślij nazwę sieci WiFi.
Serial.print("] ... "); // Wyślij tekst.
WiFi.begin(nazwaSieciWiFi, hasloSieciWiFi); // Łącz się z siecią WiFi.
while (WiFi.status() != WL_CONNECTED) { // Czekaj na połączenie.
delay(250); // Opóźnienie dla stabilizacji procesu łączenia.
}
Serial.println("sukces!"); // Udało się połączyć, wyślij tekst.
timeClient.begin(); // Inicjuj klienta serwera czasu.
delay(2000); // Opóźnienie dla ustabilizowania się połączenia.
}
void loop() {
timeClient.update(); // Pobierz czas z serwera czasu.
Serial.println(timeClient.getFormattedTime()); // Wyświetl czas.
delay(1000);
}
Tak więc będą potrzebne trzy: od obsługi WiFi WiFi.h, protokołu UDP WiFiUdp.h i pobierania czasu NTPClient. Pierwsza jest związana z kontrolerem ESP32 siedzącym na płytce Uno R4, bądź – jak wspomniałem – innych płytkach, które wykorzystują tę kostkę. Druga obsługuje protokół UDP. W uproszczeniu to sposób porozumiewania się urządzeń w sieci, który jest prostszy od klasycznego TCP, a przez to bardziej zawodny, ale też mniej zasobożerny i szybszy. Używa się go do zadań niekrytycznych, jak przesyłanie obrazu czy dźwięku, zakładając że jeśli część pakietów przepadnie, to nie będzie tragedii. W tym narzeczu będziemy rozmawiać z serwerem dostarczającym czas.
W końcu trzecia z bibliotek tym się właśnie będzie zajmować: pozyskiwaniem czasu z wybranego serwera. Następnie będziemy musieli zadeklarować nazwę sieci WiFi oraz hasło dostępu. Należy wybrać sieć pracującą w niższym paśmie, czyli 2,4 GHz, bo wyższej moduły zwykle nie obsługują.
Stała przesuniecieCzasu będzie zawierać – jak sama nazwa wskazuje – przesunięcie w sekundach względem czasu uniwersalnego UTC, czyli wyznaczonego dla południka zerowego i czasu zimowego. W Polsce należy dodać 3600 sekund w zimie bądź 7200 w lecie. To oczywiście można zautomatyzować, ale na razie nie będę się tym zajmować.
Kolejno znajdziemy nazwę obiektu dla serwera czasu: zrodloCzasu. Jak zwykle przypominam: dla odróżnienia stałych i zmiennych od funkcji programu i bibliotek, nadaję im polskie nazwy. To oczywiście nie ma znaczenia dla programu i kto chce być bardziej standardowy, może użyć skrótów angielskich.
Kolejna linia zawiera: nazwę przed chwilą stworzonego obiektu zrodloCzasu, adres serwera czasu "pool.ntp.org" oraz przesunięcie przesuniecieCzasu, które ustaliliśmy wcześniej dla naszej strefy czasowej i czasu letniego. Jeśli chodzi o adres, ten nie jest jedynym i możemy sobie wybrać któryś z poniższej listy.
Serwery firmowe
time.windows.com (Microsoft)
time.apple.com (Apple)
time.google.com (Google)
ntp.ubuntu.com (Canonical - Ubuntu)
Serwery państwowe
north-america.pool.ntp.org (Ameryka Północna)
europe.pool.ntp.org (Europa)
ntp1.t-online.de (Niemcy)
ntp2.ptb.de (Niemcy)
ntp.nict.jp (Japonia)
Polskie serwery
0.pl.pool.ntp.org
1.pl.pool.ntp.org
2.pl.pool.ntp.org
3.pl.pool.ntp.org
W pewnych wypadkach można ustalić czwarty parametr: interwał aktualizacji. Jeśli go nie podamy, domyślnie przyjmuje wartość 60 sekund.
W części wstępnej programu inicjujemy monitor, na który wysyłamy tekst informujący o próbie nawiązania łączności przez WiFi. Będzie się to odbywało w pętli o czasie trwania ¼ sekundy. Gdy uzyskamy połączenie, dostaniemy stosowną informację i będziemy mogli zainicjować kontakt z serwerem. Po zainicjowaniu konieczne jest dwusekundowe opóźnienie. Jego brak zwróci nam pierwszy odczyt fałszywy i opóźniony o kilka sekund.
Jesteśmy gotowi, można przejść do głównej pętli. Nic tam prawie nie ma: pierwsza linia pobiera czas z serwera, druga – wysyła sformatowany łańcuch w klasycznej postaci godzin, minut i sekund dzielonych dwukropkami. Opóźnienie zapobiega zasypaniu nas odczytami i to wszystko. Kompilujemy i w oknie monitora możemy zobaczyć coś takiego.

Spróbujmy nieco zmodyfikować kod, by otrzymać dane w postaci bardziej użytecznej. Zmodyfikujemy samą główną pętlę, dobierając się już nie do sformatowanego łańcucha, a konkretnych danych: godzin, minut i sekund i teraz formatowanie będziemy przeprowadzać samemu.
void loop() {
timeClient.update(); // Pobierz czas z serwera czasu.
byte godzina = timeClient.getHours(); // Pobierz elementy czasu.
byte minuta = timeClient.getMinutes();
byte sekunda = timeClient.getSeconds();
if (godzina < 10) Serial.print("0"); // Wstaw zero dla wartości mniejszych od 10
Serial.print(godzina); // Wyświetl godzinę.
Serial.print(":"); // Wyświetl dwukropek.
if (minuta < 10) Serial.print("0");
Serial.print(minuta);
Serial.print(":");
if (sekunda < 10) Serial.print("0");
Serial.println(sekunda);
delay(1000);
}
Powołujemy do życia trzy zmienne: godzina, minuta, sekunda i ładujemy do nich dane, za pomocą stosownych komend. Teraz z tych danych formujemy łańcuch dla wyświetlacza, dbając o wiodące zera dla wartości mniejszych od dziesięciu. Po co tak sobie życie utrudniać, skoro poprzednia funkcja dawała nam wszystko od razu? Ano po to, by móc załadować te dane na przykład do zegara RTC. Ale o tym napiszę później, zróbmy użytek z wyświetlacza, który znajduje się na płytce edukacyjnej TME.
#include <WiFi.h> // Biblioteka obsługująca WiFi
#include <WiFiUdp.h> // Biblioteka obsługująca protokół UDP
#include <NTPClient.h> // Biblioteka pobierająca czas z sieci.
const char* nazwaSieciWiFi = "nazwaSieciWiFi"; // Nazwa sieci WiFi
const char* hasloSieciWiFi = "hasloSieciWiFi"; // Hasło sieci WiFi
const int przesuniecieCzasu = 7200; // Różnica w sekundach między czasem UTC (dla południka zerowego i czasu zimowego).
WiFiUDP zrodloCzasu; // Nazwa obiektu dla serwera czasu.
NTPClient timeClient(zrodloCzasu, "pool.ntp.org", przesuniecieCzasu); // Adres serwera czasu.
#include <Wire.h> // Biblioteka obsługująca magistralę I2C
#include <hd44780.h> // Biblioteka obsługująca wyświetlacze HD44780
#include <hd44780ioClass/hd44780_I2Cexp.h> // Dodatek obsługujący wyświetlacze podłączone do ekspandera I2C
hd44780_I2Cexp lcd(0x20, I2Cexp_MCP23008, 7, 6, 5, 4, 3, 2, 1, HIGH); // Konfiguracja połączeń wyświetlacza LCD
void setup() {
lcd.begin(16, 2); // Inicjuj wyświetlacz LCD
lcd.setCursor(0, 0); // Ustaw kursor.
lcd.print("Lacze sie z WiFi"); // Wyślij tekst.
lcd.setCursor(0, 1); // Ustaw kursor.
lcd.print(nazwaSieciWiFi); // Wyślij nazwę sieci WiFi.
WiFi.begin(nazwaSieciWiFi, hasloSieciWiFi); // Łącz się z siecią WiFi.
while (WiFi.status() != WL_CONNECTED) { // Czekaj na połączenie.
delay(250); // Opóźnienie dla stabilizacji procesu łączenia.
}
timeClient.begin(); // Inicjuj klienta serwera czasu.
delay(2000); // Opóźnienie dla ustabilizowania się połączenia.
lcd.clear(); // Wyczyść ekran.
}
void loop() {
timeClient.update(); // Pobierz czas z serwera czasu.
byte godzina = timeClient.getHours(); // Pobierz elementy czasu.
byte minuta = timeClient.getMinutes();
byte sekunda = timeClient.getSeconds();
lcd.setCursor(4, 0); // Ustaw kursor.
if (godzina < 10) lcd.print("0"); // Wstaw zero dla wartości mniejszych od 10
lcd.print(godzina); // Wyświetl godzinę.
lcd.print(":"); // Wyświetl dwukropek.
if (minuta < 10) lcd.print("0");
lcd.print(minuta);
lcd.print(":");
if (sekunda < 10) lcd.print("0");
lcd.print(sekunda);
delay(1000);
}
Ogólnie rzecz ujmując, przekierujemy wszystkie przesłania z monitora na wyświetlacz, korzystając z wielokrotnie już przeze mnie prezentowanych sposobów. Przypomnę więc, będą potrzebne trzy kolejne biblioteki: obsługująca magistralę I2C Wire.h, wyświetlacze z układem HD44780 hd44780.h oraz takież, dołączone przez ekspander do magistrali szeregowej hd44780ioClass/hd44780_I2Cexp.h. Zaraz potem skonfigurujemy ten konkretny wyświetlacz: adres, organizację i numery pinów. W programie będziemy odwoływać się do wyświetlacza, zamieniając serial.print na lcd.print Trzeba pamiętać także o podawaniu pozycji, w których mają się pojawiać napisy. Po skompilowaniu kodu możemy się już cieszyć zegarkiem.

Skoro mamy zegar, przydałby się kalendarz. Ale o tym i rozwiązaniu praktycznym zegara pozyskanego z sieci, czyli współpracy z zegarem RTC napiszę w drugiej części.