W tym poście chciałbym opisać implementację klienta NTP.
Pakiet NTP składa się z 48 bajtów danych.
- 0 - 1 bajt - LI 2 bity, VN - 3 bity, Mode - 3 bity.
- 1 - 1 bajt - Stratum - Poziom Serwera
- 2 - 1 bajt - Poll Interval - Sugerowany interwał miedzy zapytaniami
- 3 - 1 bajt - Precision - Precyzja między zapytaniami
- 4-7 - 4 bajty - Root Delay - Całkowite opóźnienie względem źródła zegara
- 8-11 - 4 bajty - Root Dispersion - Rozrzut zegara
- 12-15 - 4 bajty - Reference ID - Identyfikator źródła czasu
- 16-23 - 8 bajtów - Reference Timestamp - Czas ostatniej aktualizacji zegara serwera
- 24-31 - 8 bajtów - Originate Timestamp - Czas wysłania żądania przez klienta
- 32-39 - 8 bajtów - Receive Timestamp - Czas odebrania żądania przez serwer
- 40-47 - 8 bajtów - Transmit Timestamp - Czas wysłania odpowiedzi przez serwer
Na początku tworzony jest nowy blok sterowania protokołu UDP (UDP PCB) za pomocą funkcji udp_new(). Następnie konfigurowana jest funkcja odbiorcza za pomocą udp_recv(), która zostanie wywołana po otrzymaniu odpowiedzi od serwera NTP. Adres IP serwera jest konwertowany z formatu tekstowego na binarny za pomocą funkcji ipaddr_aton().
Kolejnym krokiem jest przygotowanie pakietu NTP — zgodnie ze specyfikacją, pakiet ten ma 48 bajtów, z pierwszym bajtem ustawionym na wartość 0x1B, co oznacza wersję 3 i tryb klienta. Pakiet jest następnie umieszczany w buforze pbuf, który jest przesyłany do serwera NTP za pomocą funkcji udp_sendto() na porcie 123.
Tutaj wykorzystujemy bezpośrednie wywołanie pakietu przez UDP. Nie ma konieczności uruchamiania dodatkowych elementów w LWIP.
- #define NTP_PORT 123
- #define NTP_SERVER_IP "194.146.251.100"
- #define NTP_EPOCH_DIFF 2208988800UL // Between 1900 and 1970 in seconds
- static struct udp_pcb *ntp_pcb;
- void ntp_request(void) {
- if (ntp_pcb == NULL) {
- ntp_pcb = udp_new();
- if (ntp_pcb == NULL) {
- printf("Failed to create UDP PCB\n");
- return;
- }
- udp_recv(ntp_pcb, ntp_udp_recv, NULL);
- }
- ip_addr_t ntp_ip;
- ipaddr_aton(NTP_SERVER_IP, &ntp_ip);
- uint8_t ntp_packet[48] = {0};
- ntp_packet[0] = 0x1B;
- struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, sizeof(ntp_packet), PBUF_RAM);
- if (p != NULL) {
- memcpy(p->payload, ntp_packet, sizeof(ntp_packet));
- udp_sendto(ntp_pcb, p, &ntp_ip, NTP_PORT);
- pbuf_free(p);
- }
- }
Przetwarzanie odebranych danych odbywa się w funkcji ntp_udp_recv(), która obsługuje dane otrzymane z serwera NTP. Na początku sprawdzana jest długość pakietu — czy wynosi co najmniej 48 bajtów, co jest standardowym rozmiarem pakietu NTP.
Następnie funkcja odczytuje 4 bajty z pozycji 40–43, które zawierają tzw. Transmit Timestamp — czyli znacznik czasu przesłany przez serwer NTP. Wartość ta wyrażona jest jako liczba sekund od 1 stycznia 1900 roku (epoka NTP).
Ponieważ typ time_t w systemie Unix liczy sekundy od 1 stycznia 1970 roku, konieczne jest odjęcie stałej NTP_EPOCH_DIFF, aby uzyskać zgodność między formatami czasu.
Po przekształceniu danych funkcja przekazuje wynik do print_polish_local_time(), która przelicza i wyświetla zarówno czas UTC, jak i lokalny czas w Polsce, uwzględniając obowiązywanie czasu letniego.
- int is_summer_time_pl(struct tm *utc) {
- int year = utc->tm_year + 1900;
- // Ostatnia niedziela marca
- struct tm last_sunday_march = {
- .tm_year = year - 1900,
- .tm_mon = 2, // Marzec (0-based)
- .tm_mday = 31,
- .tm_hour = 2,
- .tm_min = 0,
- .tm_sec = 0,
- .tm_isdst = 0
- };
- mktime(&last_sunday_march);
- last_sunday_march.tm_mday -= last_sunday_march.tm_wday;
- mktime(&last_sunday_march);
- // Ostatnia niedziela października
- struct tm last_sunday_oct = {
- .tm_year = year - 1900,
- .tm_mon = 9, // Październik
- .tm_mday = 31,
- .tm_hour = 3,
- .tm_min = 0,
- .tm_sec = 0,
- .tm_isdst = 0
- };
- mktime(&last_sunday_oct);
- last_sunday_oct.tm_mday -= last_sunday_oct.tm_wday;
- mktime(&last_sunday_oct);
- time_t t = mktime(utc);
- time_t t_start = mktime(&last_sunday_march);
- time_t t_end = mktime(&last_sunday_oct);
- return (t >= t_start && t < t_end);
- }
- void print_pl_local_time(time_t timestamp) {
- struct tm *utc = gmtime(×tamp);
- int offset_hours = is_summer_time_pl(utc) ? 2 : 1;
- time_t local_ts = timestamp + offset_hours * 3600;
- struct tm *local_tm = gmtime(&local_ts);
- printf("UTC time : %s", asctime(utc));
- printf("Polish time: %s", asctime(local_tm));
- }
- void ntp_udp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p,
- const ip_addr_t *addr, u16_t port) {
- if (p->len == 48) {
- uint8_t *data = (uint8_t *)p->payload;
- uint32_t time_seconds = (data[40] << 24) | (data[41] << 16) |
- (data[42] << 8) | data[43];
- time_seconds -= NTP_EPOCH_DIFF;
- time_t timestamp = (time_t)time_seconds;
- print_pl_local_time(timestamp);
- }
- pbuf_free(p);
- }
W celu wywołania funkcji wystarczy uruchomić funkcję ntp_request().
Wywołanie funkcji należy umieścić po uruchomieniu biblioteki LWIP. Jak ustawione DHCP to po ustawienia adresu.