Trong bài viết này chúng ta sẽ biết cách cập nhật thời gian từ Internet với NTP thông qua một NTP server miễn phí của Google đó là time.google.com.
Trong các bào viết trước trước (HTTP, WebSockets), chúng ta chỉ sử dụng các kết nối TCP, nhưng NTP dựa trên UDP.
Sự khác biệt chính giữa TCP và UDP là TCP cần kết nối để gửi tin nhắn: Đầu tiên client gởi tín hiệu handshake (bắt tay) đến server. Khi server đồng ý thì kết nối được thiết lập và client có thể gửi tin nhắn cho nó. Sau khi client nhận được phản hồi của server, kết nối sẽ bị đóng (trừ khi sử dụng WebSockets). Để gửi một tin nhắn mới, client phải mở một kết nối mới đến server nữa. Điều này gây ra độ trễ và tốn chi phí.
UDP không sử dụng kết nối, client chỉ có thể gửi tin nhắn trực tiếp đến server và server chỉ có thể gửi tin nhắn trả lời lại cho client khi đã xử lý xong. Tuy nhiên, không có gì đảm bảo rằng các thông điệp sẽ đến đích của chúng, và không có cách nào để biết liệu họ có đến hay không (mà không gửi một tín hiệu xác nhận – ACK). Điều này có nghĩa là chúng ta không thể tạm dừng chương trình để chờ phản hồi, vì yêu cầu hoặc gói phản hồi có thể đã bị mất trên Internet và ESP8266 sẽ rơi vào một vòng lặp vô hạn.
Thay vì chờ phản hồi, chúng ta sẽ gửi nhiều yêu cầu, với khoảng thời gian cố định giữa hai yêu cầu và chỉ thường xuyên kiểm tra xem có nhận được phản hồi hay không.
#include <ESP8266WiFi.h> // Thư viện dùng để kết nối WiFi của ESP8266 #include <WiFiUdp.h> // Thư viện WiFiUdp dùng để truy vấn đến NTP server const char* ssid = "Blocky AP"; // Tên của mạng WiFi mà bạn muốn kết nối đến const char* password = "password_ap"; // Mật khẩu của mạng WiFi WiFiUDP UDP; // Tạo đối tượng UDP để gửi và nhận thông tin thời gian IPAddress timeServerIP; // Lưu địa chỉ IP của NTP server time.google.com const char* NTPServerName = "time.google.com"; const int NTP_PACKET_SIZE = 48; // Bộ nhớ đệm 48 bytes cho gói tin NTP byte NTPBuffer[NTP_PACKET_SIZE]; // Bộ nhớ đệm để giữ các gói tin NTP
Ta cần sử dụng thư viện WiFiUdp dùng để truy vấn đến NTP server. Các gói tin UDP sẽ được gởi đến NTP server là time.google.com.
void setup() { Serial.begin(115200); // Khởi tạo kết nối Serial để truyền dữ liệu đến máy tính delay(10); Serial.println("\r\n"); startWiFi(); startUDP(); if (!WiFi.hostByName(NTPServerName, timeServerIP)) { // Nhận địa chỉ IP của NTP server Serial.println("DNS lookup failed. Rebooting."); Serial.flush(); ESP.reset(); } Serial.print("Time server IP: "); Serial.println(timeServerIP); Serial.println("\r\nSending NTP request..."); sendNTPpacket(timeServerIP); }
Chúng ta cần địa chỉ IP của NTP server để gởi gói tin NTP bằng cách thực hiện tra cứu DNS. Nếu tra cứu DNS không thành công, ta sẽ khởi động lại ESP.
unsigned long intervalNTP = 60000; // Truy vấn NTP sau mỗi phút (60000 mili giây) unsigned long prevNTP = 0; unsigned long lastNTPResponse = millis(); uint32_t timeUNIX = 0; unsigned long prevActualTime = 0; void loop() { unsigned long currentMillis = millis(); if (currentMillis - prevNTP > intervalNTP) { // Gởi tiếp truy vấn NTP sau mỗi phút prevNTP = currentMillis; Serial.println("\r\nSending NTP request ..."); sendNTPpacket(timeServerIP); } uint32_t time = getTime(); // Kiểm tra nếu có phản hồi từ NTP server if (time) { timeUNIX = time; Serial.print("NTP response:\t"); Serial.println(timeUNIX); lastNTPResponse = currentMillis; } else if ((currentMillis - lastNTPResponse) > 3600000) { Serial.println("More than 1 hour since last NTP response. Rebooting."); Serial.flush(); ESP.reset(); } uint32_t actualTime = timeUNIX + (currentMillis - lastNTPResponse) / 1000; if (actualTime != prevActualTime && timeUNIX != 0) { // Cập nhật lại thời gian thực tế nếu có thay đổi prevActualTime = actualTime; Serial.printf("\r\nUTC time:\t%d:%d:%d ", getHours(actualTime), getMinutes(actualTime), getSeconds(actualTime)); } }
Phần đầu tiên của vòng lặp sẽ gửi một truy vấn NTP đến server sau mỗi phút. Sau đó, chúng ta gọi hàm getTime để kiểm tra xem nhận được phản hồi từ server hay không. Nếu nhận được phản hồi, chúng ta cập nhật biến timeUNIX với mốc thời gian mới từ server. Nếu không nhận được bất kỳ phản hồi nào trong một giờ thì có thể có sự cố gì đó nên chúng ta sẽ khởi động lại ESP. Phần cuối cùng tính thời gian thực tế. Thời gian thực tế là thời gian NTP cuối cùng nhận được cộng với thời gian chờ để nhận được thông báo NTP đó.
void startWiFi() { WiFi.begin(ssid, password); // Kết nối vào mạng WiFi Serial.print("Connecting to "); Serial.print(ssid); // Chờ kết nối WiFi được thiết lập while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.print("."); } Serial.println("\n"); Serial.println("Connection established!"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // Gởi địa chỉ IP đến máy tinh } void startUDP() { Serial.println("Starting UDP"); UDP.begin(123); // Lắng nghe các gói tin UDP ở cổng 123 Serial.print("Local port:\t"); Serial.println(UDP.localPort()); Serial.println(); } uint32_t getTime() { if (UDP.parsePacket() == 0) { return 0; } UDP.read(NTPBuffer, NTP_PACKET_SIZE); uint32_t NTPTime = (NTPBuffer[40] << 24) | (NTPBuffer[41] << 16) | (NTPBuffer[42] << 8) | NTPBuffer[43]; const uint32_t seventyYears = 2208988800UL; uint32_t UNIXTime = NTPTime - seventyYears; return UNIXTime; } void sendNTPpacket(IPAddress& address) { memset(NTPBuffer, 0, NTP_PACKET_SIZE); NTPBuffer[0] = 0b11100011; UDP.beginPacket(address, 123); UDP.write(NTPBuffer, NTP_PACKET_SIZE); UDP.endPacket(); } inline int getSeconds(uint32_t UNIXTime) { return UNIXTime % 60; } inline int getMinutes(uint32_t UNIXTime) { return UNIXTime / 60 % 60; } inline int getHours(uint32_t UNIXTime) { return UNIXTime / 3600 % 24; }
Ngoài hàm startWiFi dùng để kết nối WiFi quen thuộc, ta có các hàm mới sau:
NTP server trả về thời gian UTC. Nếu bạn muốn giờ địa phương, ví dụ như ở Việt Nam (UTC+7), bạn phải cộng thêm 7*3600 vào biến timeUNIX.
Sau khi nạp code thành công, bạn mở Serial Monitor để kiểm tra.