Cập nhật thời gian từ Internet với NTP

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.

Thiết bị cần chuẩn bị

  • 1 x Node WiFi

 

Chương Trình

Khai báo thư viện, hằng số và các biến toàn cục

#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.

Hàm Setup

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.

Hàm Loop

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 đó.

Các hàm con

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:

  • startUDP: khởi tạo UDP dùng cho việc gởi nhận các gói tin NTP
  • sendNTPpacket: dùng để gởi các gói tin đến NTP server
  • getTime: trả về Unix Time từ server. Unix Time là một loại thời gian đặc biệt, nó là tổng số giây tính từ mốc thời gian 0 giờ, 0 phút, 0 giây – ngày 1, tháng 01, năm 1970 (UTC). Như vậy mỗi ngày Unix Time sẽ tăng lên 86400 (Nếu bạn nào biết bào hát “8 vạn 6 ngàn 400 lần nhớ em” sẽ hiểu con số này 🙂 )
  • getHours, getMinutes, getSeconds: Để chuyển đổi từ Unix Time sang định dạng thời gian thông dụng. Ví dụ 17:35:20.

 

Chuyển đổi múi giờ

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.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Fill out this field
Fill out this field
Vui lòng nhập địa chỉ email hợp lệ.
You need to agree with the terms to proceed

1 Bình luận.

  • Đây là khóa học IoT với Arduino phần nâng cao, giúp các bạn làm quen với các khái niệm về phần truyền và nhận dữ liệu lên Internet thông qua kết nối WiFi và nhiều phương thức (protocol) khác nhau.

Menu