I. Wi-Fi
1. Chế độ AP (Điểm truy cập)
Chế độ AP cho phép ESP32-S3 hoạt động như một điểm truy cập Wi-Fi, tạo ra một mạng cục bộ mà các thiết bị khác có thể kết nối tới.
Cấu trúc phân cấp của chế độ AP
Chế độ AP bao gồm các thành phần cấu hình SSID, kênh, mật khẩu và chế độ bảo mật.
Quy trình chuẩn của chế độ AP trên ESP32
Quy trình bao gồm khởi tạo ESP-NETIF, tạo vòng lặp sự kiện, khởi tạo Wi-Fi, đăng ký trình xử lý sự kiện và cấu hình Wi-Fi ở chế độ AP.
Ví dụ mã nguồn (SoftAP):
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_mac.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
#define EXAMPLE_ESP_WIFI_CHANNEL CONFIG_ESP_WIFI_CHANNEL
#define EXAMPLE_MAX_STA_CONN CONFIG_ESP_MAX_STA_CONN
static const char *TAG = "wifi softAP";
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "station " MACSTR " join, AID=%d",
MAC2STR(event->mac), event->aid);
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
ESP_LOGI(TAG, "station " MACSTR " leave, AID=%d",
MAC2STR(event->mac), event->aid);
}
}
void wifi_init_softap(void)
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_ap();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
wifi_config_t wifi_config = {
.ap = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
.channel = EXAMPLE_ESP_WIFI_CHANNEL,
.password = EXAMPLE_ESP_WIFI_PASS,
.max_connection = EXAMPLE_MAX_STA_CONN,
.authmode = WIFI_AUTH_WPA_WPA2_PSK,
.pmf_cfg = {
.required = false,
},
},
};
if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) {
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
}
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL);
}
void app_main(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "ESP_WIFI_MODE_AP");
wifi_init_softap();
}
Quá trình biên dịch và nạp (Dòng lệnh)
- Chuyển đến thư mục dự án và đặt target:
idf.py set-target esp32s3 - Cấu hình các tham số (tên AP, mật khẩu, v.v.) bằng menuconfig:
idf.py menuconfig - Biên dịch toàn bộ mã hoặc chỉ ứng dụng:
idf.py allhoặcidf.py app - Nạp mã xuống flash:
idf.py flash - Giám sát cổng nối tiếp:
idf.py monitor
2. Chế độ Station (Thiết bị khách)
Chế độ Station cho phép ESP32-S3 kết nối tới một mạng Wi-Fi hiện có, giống như một điện thoại thông minh hoặc máy tính.
Kiến trúc phân cấp của chế độ STA
Tương tự như chế độ AP, nhưng tập trung vào việc kết nối với một điểm truy cập đã có.
Kết nối với điểm truy cập di động
Sử dụng các ví dụ có sẵn trong ESP-IDF và cấu hình thông tin kết nối Wi-Fi qua menuconfig.
II. TCP
1. TCP Client dựa trên LwIP Socket
Triển khai một ứng dụng client TCP sử dụng API Socket của LwIP để giao tiếp với một server TCP.
Luồng giao tiếp Client-Server:
- Tạo socket.
- Kết nối tới địa chỉ IP và cổng của server.
- Gửi dữ liệu tới server.
- Nhận dữ liệu từ server.
- Xử lý dữ liệu nhận được (ví dụ: gửi lại hoặc thoát).
- Đóng kết nối.
Ví dụ mã nguồn (TCP Client):
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "addr_from_stdin.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#if defined(CONFIG_EXAMPLE_IPV4)
#define HOST_IP_ADDR "10.77.93.176" // Hoặc cấu hình qua menuconfig
#elif defined(CONFIG_EXAMPLE_IPV6)
#define HOST_IP_ADDR CONFIG_EXAMPLE_IPV6_ADDR
#else
#define HOST_IP_ADDR ""
#endif
#define PORT 3333 // Hoặc cấu hình qua menuconfig
static const char *TAG = "example";
static const char *payload = "Hello this's Message from little";
int err;
static void tcp_client_task(void *pvParameters)
{
char rx_buffer[128];
char host_ip[] = HOST_IP_ADDR;
int addr_family = 0;
int ip_protocol = 0;
bool bShutdown = false;
while (1)
{
#if defined(CONFIG_EXAMPLE_IPV4)
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = inet_addr(host_ip);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(PORT);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
#elif defined(CONFIG_EXAMPLE_IPV6)
struct sockaddr_in6 dest_addr = {0};
inet6_aton(host_ip, &dest_addr.sin6_addr);
dest_addr.sin6_family = AF_INET6;
dest_addr.sin6_port = htons(PORT);
dest_addr.sin6_scope_id = esp_netif_get_netif_impl_index(EXAMPLE_INTERFACE);
addr_family = AF_INET6;
ip_protocol = IPPROTO_IPV6;
#elif defined(CONFIG_EXAMPLE_SOCKET_IP_INPUT_STDIN)
struct sockaddr_storage dest_addr = {0};
ESP_ERROR_CHECK(get_addr_from_stdin(PORT, SOCK_STREAM, &ip_protocol, &addr_family, &dest_addr));
#endif
int sock = socket(addr_family, SOCK_STREAM, ip_protocol);
if (sock < 0)
{
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created, connecting to %s:%d", host_ip, PORT);
err = connect(sock, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr_in6));
if (err != 0)
{
ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Successfully connected");
err = send(sock, payload, strlen(payload), 0);
if (err < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
while (1)
{
int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
if (len < 0)
{
ESP_LOGE(TAG, "recv failed: errno %d", errno);
break;
}
else
{
rx_buffer[len] = 0;
ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip);
ESP_LOGI(TAG, "%s", rx_buffer);
if (rx_buffer[0] == 's')
{
bShutdown = true;
break;
}
else
{
err = send(sock, rx_buffer, strlen(rx_buffer), 0);
if (err < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
}
}
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
if (sock != -1)
{
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);
}
if (bShutdown == true)
{
break;
}
}
vTaskDelete(NULL);
}
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
xTaskCreate(tcp_client_task, "tcp_client", 4096, NULL, 5, NULL);
}
Hướng dẫn:
- Cấu hình địa chỉ IP và cổng của server.
- Cấu hình thông tin Wi-Fi để kết nối.
- Sử dụng một ứng dụng server TCP (ví dụ: "Net Assistant" trên điện thoại) để tạo server.
- Client (ESP32) gửi dữ liệu, nhận phản hồi và gửi lại.
- Server có thể gửi ký tự 's' để yêu cầu client đóng kết nối.
2. TCP Server dựa trên LwIP Socket
Triển khai một ứng dụng server TCP sử dụng API Socket của LwIP để lắng nghe và xử lý các kết nối từ client.
Ví dụ mã nguồn (TCP Server):
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>
#define PORT CONFIG_EXAMPLE_PORT
#define KEEPALIVE_IDLE CONFIG_EXAMPLE_KEEPALIVE_IDLE
#define KEEPALIVE_INTERVAL CONFIG_EXAMPLE_KEEPALIVE_INTERVAL
#define KEEPALIVE_COUNT CONFIG_EXAMPLE_KEEPALIVE_COUNT
static const char *TAG = "example";
static void do_retransmit(const int sock)
{
int len;
char rx_buffer[128];
do {
len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
if (len < 0) {
ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno);
} else if (len == 0) {
ESP_LOGW(TAG, "Connection closed");
} else {
rx_buffer[len] = 0;
ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer);
int to_write = len;
while (to_write > 0) {
int written = send(sock, rx_buffer + (len - to_write), to_write, 0);
if (written < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
}
to_write -= written;
}
}
} while (len > 0);
}
static void tcp_server_task(void *pvParameters)
{
char addr_str[128];
int addr_family = (int)pvParameters;
int ip_protocol = 0;
int keepAlive = 1;
int keepIdle = KEEPALIVE_IDLE;
int keepInterval = KEEPALIVE_INTERVAL;
int keepCount = KEEPALIVE_COUNT;
struct sockaddr_storage dest_addr;
if (addr_family == AF_INET) {
struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr_ip4->sin_family = AF_INET;
dest_addr_ip4->sin_port = htons(PORT);
ip_protocol = IPPROTO_IP;
}
#ifdef CONFIG_EXAMPLE_IPV6
else if (addr_family == AF_INET6) {
struct sockaddr_in6 *dest_addr_ip6 = (struct sockaddr_in6 *)&dest_addr;
bzero(&dest_addr_ip6->sin6_addr.un, sizeof(dest_addr_ip6->sin6_addr.un));
dest_addr_ip6->sin6_family = AF_INET6;
dest_addr_ip6->sin6_port = htons(PORT);
ip_protocol = IPPROTO_IPV6;
}
#endif
int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
if (listen_sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
vTaskDelete(NULL);
return;
}
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
#endif
ESP_LOGI(TAG, "Socket created");
err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
goto CLEAN_UP;
}
ESP_LOGI(TAG, "Socket bound, port %d", PORT);
err = listen(listen_sock, 1);
if (err != 0) {
ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
goto CLEAN_UP;
}
while (1) {
ESP_LOGI(TAG, "Socket listening");
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
break;
}
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int));
if (source_addr.ss_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
}
#ifdef CONFIG_EXAMPLE_IPV6
else if (source_addr.ss_family == PF_INET6) {
inet6_ntoa_r(((struct sockaddr_in6 *)&source_addr)->sin6_addr, addr_str, sizeof(addr_str) - 1);
}
#endif
ESP_LOGI(TAG, "Socket accepted ip address: %s", addr_str);
do_retransmit(sock);
shutdown(sock, 0);
close(sock);
}
CLEAN_UP:
close(listen_sock);
vTaskDelete(NULL);
}
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
#ifdef CONFIG_EXAMPLE_IPV4
xTaskCreate(tcp_server_task, "tcp_server", 4096, (void*)AF_INET, 5, NULL);
#endif
#ifdef CONFIG_EXAMPLE_IPV6
xTaskCreate(tcp_server_task, "tcp_server", 4096, (void*)AF_INET6, 5, NULL);
#endif
}
Hướng dẫn:
- Cấu hình cổng và loại IP.
- Cấu hình thông tin Wi-Fi để kết nối.
- Sử dụng một ứng dụng client TCP (ví dụ: "Net Assistant" trên điện thoại) để kết nối tới server.
- Client gửi dữ liệu, server nhận, in ra và gửi lại.
III. UDP
1. UDP và TCP
Điểm giống nhau
- Cả hai đều là các giao thức truyền tải dữ liệu.
Điểm khác nhau
- Mô hình lập trình: UDP không có quá trình thiết lập kết nối lắng nghe như TCP.
recvfrom()của UDP chặn tiến trình chờ dữ liệu và trả về cả IP/cổng của nguồn, trong khiaccept()của TCP không làm vậy.sendto()của UDP cần kèm IP/cổng nguồn, còn TCP thì không cần vì kết nối đã được thiết lập. - Độ tin cậy: TCP đáng tin cậy, UDP không đảm bảo dữ liệu đến nơi.
- Tốc độ: UDP nhanh hơn TCP.
- Phạm vi: TCP chỉ truyền điểm-tới-điểm, UDP hỗ trợ broadcast và multicast.
2. UDP Client
Triển khai ứng dụng UDP client để gửi và nhận dữ liệu không kết nối.
Ví dụ mã nguồn (UDP Client):
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>
#include "addr_from_stdin.h"
#if defined(CONFIG_EXAMPLE_IPV4)
#define HOST_IP_ADDR "10.48.33.164"
#elif defined(CONFIG_EXAMPLE_IPV6)
#define HOST_IP_ADDR CONFIG_EXAMPLE_IPV6_ADDR
#else
#define HOST_IP_ADDR ""
#endif
#define PORT 5001
static const char *TAG = "example";
static const char *payload = "Message from UDP Clinet";
static void udp_client_task(void *pvParameters)
{
char rx_buffer[128];
char host_ip[] = HOST_IP_ADDR;
int addr_family = 0;
int ip_protocol = 0;
while (1) {
#if defined(CONFIG_EXAMPLE_IPV4)
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = inet_addr(HOST_IP_ADDR);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(PORT);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
#elif defined(CONFIG_EXAMPLE_IPV6)
struct sockaddr_in6 dest_addr = { 0 };
inet6_aton(HOST_IP_ADDR, &dest_addr.sin6_addr);
dest_addr.sin6_family = AF_INET6;
dest_addr.sin6_port = htons(PORT);
dest_addr.sin6_scope_id = esp_netif_get_netif_impl_index(EXAMPLE_INTERFACE);
addr_family = AF_INET6;
ip_protocol = IPPROTO_IPV6;
#elif defined(CONFIG_EXAMPLE_SOCKET_IP_INPUT_STDIN)
struct sockaddr_storage dest_addr = { 0 };
ESP_ERROR_CHECK(get_addr_from_stdin(PORT, SOCK_DGRAM, &ip_protocol, &addr_family, &dest_addr));
#endif
int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
struct timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 0;
setsockopt (sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout);
ESP_LOGI(TAG, "Socket created, sending to %s:%d", HOST_IP_ADDR, PORT);
int err = sendto(sock, payload, strlen(payload), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Message sent");
while (1) {
struct sockaddr_storage source_addr;
socklen_t socklen = sizeof(source_addr);
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);
if (len < 0) {
ESP_LOGE(TAG, "recvfrom timeout");
}
else
{
rx_buffer[len] = 0;
ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip);
ESP_LOGI(TAG, "%s", rx_buffer);
if (strncmp(rx_buffer, "reset: ", 5) == 0) {
ESP_LOGI(TAG, "Received expected message, re-init");
break;
}
else
{
ESP_LOGI(TAG, "Message return");
err = sendto(sock, rx_buffer, len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err < 0)
{
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
}
}
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);
}
}
vTaskDelete(NULL);
}
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
xTaskCreate(udp_client_task, "udp_client", 4096, NULL, 5, NULL);
}
Hướng dẫn:
- ESP32 gửi dữ liệu ban đầu.
- Timeout sau 10 giây nếu không nhận được phản hồi.
- Nhận dữ liệu từ server và gửi lại.
- Nhận 'reset: ' để khởi động lại client UDP.
3. UDP Server
Triển khai ứng dụng UDP server để lắng nghe và phản hồi dữ liệu từ client.
Ví dụ mã nguồn (UDP Server):
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "protocol_examples_common.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include <lwip/netdb.h>
#define PORT CONFIG_EXAMPLE_PORT
static const char *TAG = "example";
static void udp_server_task(void *pvParameters)
{
char rx_buffer[128];
char addr_str[128];
int addr_family = (int)pvParameters;
int ip_protocol = 0;
struct sockaddr_in6 dest_addr;
while (1) {
if (addr_family == AF_INET) {
struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr_ip4->sin_family = AF_INET;
dest_addr_ip4->sin_port = htons(PORT);
ip_protocol = IPPROTO_IP;
} else if (addr_family == AF_INET6) {
bzero(&dest_addr.sin6_addr.un, sizeof(dest_addr.sin6_addr.un));
dest_addr.sin6_family = AF_INET6;
dest_addr.sin6_port = htons(PORT);
ip_protocol = IPPROTO_IPV6;
}
int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created");
#if defined(CONFIG_LWIP_NETBUF_RECVINFO) && !defined(CONFIG_EXAMPLE_IPV6)
int enable = 1;
lwip_setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &enable, sizeof(enable));
#endif
#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
if (addr_family == AF_INET6) {
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
}
#endif
struct timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 0;
setsockopt (sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout);
int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err < 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
}
ESP_LOGI(TAG, "Socket bound, port %d", PORT);
struct sockaddr_storage source_addr;
socklen_t socklen = sizeof(source_addr);
#if defined(CONFIG_LWIP_NETBUF_RECVINFO) && !defined(CONFIG_EXAMPLE_IPV6)
struct iovec iov;
struct msghdr msg;
struct cmsghdr *cmsgtmp;
u8_t cmsg_buf[CMSG_SPACE(sizeof(struct in_pktinfo))];
iov.iov_base = rx_buffer;
iov.iov_len = sizeof(rx_buffer);
msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);
msg.msg_flags = 0;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_name = (struct sockaddr *)&source_addr;
msg.msg_namelen = socklen;
#endif
while (1) {
ESP_LOGI(TAG, "Waiting for data");
#if defined(CONFIG_LWIP_NETBUF_RECVINFO) && !defined(CONFIG_EXAMPLE_IPV6)
int len = recvmsg(sock, &msg, 0);
#else
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);
#endif
if (len < 0) {
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
break;
}
else
{
if (source_addr.ss_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
#if defined(CONFIG_LWIP_NETBUF_RECVINFO) && !defined(CONFIG_EXAMPLE_IPV6)
for ( cmsgtmp = CMSG_FIRSTHDR(&msg); cmsgtmp != NULL; cmsgtmp = CMSG_NXTHDR(&msg, cmsgtmp) ) {
if ( cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_PKTINFO ) {
struct in_pktinfo *pktinfo;
pktinfo = (struct in_pktinfo*)CMSG_DATA(cmsgtmp);
ESP_LOGI(TAG, "dest ip: %s\n", inet_ntoa(pktinfo->ipi_addr));
}
}
#endif
} else if (source_addr.ss_family == PF_INET6) {
inet6_ntoa_r(((struct sockaddr_in6 *)&source_addr)->sin6_addr, addr_str, sizeof(addr_str) - 1);
}
u16_t iPort = ntohs(((struct sockaddr_in6 *)&source_addr)->sin6_port);
rx_buffer[len] = 0;
ESP_LOGI(TAG, "Received %d bytes from IP:%s: Port:%d", len, addr_str,iPort);
ESP_LOGI(TAG, "%s", rx_buffer);
int err = sendto(sock, rx_buffer, len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr));
if (err < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
}
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);
}
}
vTaskDelete(NULL);
}
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(example_connect());
#ifdef CONFIG_EXAMPLE_IPV4
xTaskCreate(udp_server_task, "udp_server", 4096, (void*)AF_INET, 5, NULL);
#endif
#ifdef CONFIG_EXAMPLE_IPV6
xTaskCreate(udp_server_task, "udp_server", 4096, (void*)AF_INET6, 5, NULL);
#endif
}
Hướng dẫn:
- ESP32 hoạt động như UDP server, lắng nghe trên một cổng cụ thể.
- Client (ví dụ: ứng dụng trên điện thoại) kết nối và gửi dữ liệu.
- Server nhận dữ liệu, hiển thị thông tin nguồn (IP, cổng) và gửi lại dữ liệu đã nhận.
IV. Giao thức HTTP
1. Vai trò của giao thức
Cho phép ESP32-S3 hoạt động như một máy chủ web, cung cấp dữ liệu và cho phép điều khiển thiết bị thông qua trình duyệt.
2. Định dạng gói tin
HTTP hoạt động theo mô hình Request-Response giữa client và server.
- HTTP Request: Bao gồm Request Line, Headers, và tùy chọn là Body.
- HTTP Response: Bao gồm Status Line, Headers, và tùy chọn là Body (chứa nội dung web).
- Methods: GET (truy xuất), POST (gửi dữ liệu), v.v.
- Status Codes: 200 (OK), 404 (Not Found), v.v.
3. Siêu liên kết URL
Cấu trúc URL xác định tài nguyên yêu cầu và các tham số đi kèm.
protocol://user:pass@domain:port/path?query#fragment
V. Phương thức HTTP GET
1. Hàm Callback
Trong ESP-IDF, các tài nguyên (URL) được đăng ký với các hàm xử lý tương ứng (callback handlers) cho các phương thức HTTP khác nhau (GET, POST, PUT).
httpd_register_uri_handler()dùng để đăng ký.
2. Hàm GET
Hàm xử lý GET phân tích yêu cầu, lấy thông tin từ Headers và URL query string, sau đó xây dựng và trả về gói tin Response.
Ví dụ về đăng ký handler GET:
static const httpd_uri_t hello = {
.uri = "/hello",
.method = HTTP_GET,
.handler = hello_get_handler,
.user_ctx = "Hello World!"
};
Hàm hello_get_handler sẽ xử lý các yêu cầu tới "/hello", phân tích header và query string, sau đó gửi phản hồi.
3. Thực nghiệm
Kết nối ESP32 và máy tính vào cùng mạng Wi-Fi. Truy cập địa chỉ IP của ESP32 kèm theo "/hello" trên trình duyệt. ESP32 sẽ ghi log các thông tin header client gửi tới.
VI. Phương thức HTTP POST
Cho phép client gửi dữ liệu (thường từ form HTML) tới server để xử lý.
1. Lấy HTML (form)
Sử dụng phương thức GET để yêu cầu một trang HTML chứa form.
Ví dụ HTML form:
const char formHtml[] = R"(
<html>
<body>
<form action="http://192.168.0.192/echo" method="POST">
name:<input type="text" name="username">
age:<input type="text" name="age">
<input type="submit" value="submit">
</form>
</body>
</html>
)";
2. Dữ liệu POST HTML
Khi người dùng gửi form, dữ liệu sẽ được gửi tới URL "/echo" bằng phương thức POST. Hàm xử lý POST (echo_post_handler) sẽ nhận dữ liệu này và có thể gửi lại cho client.
VII. Phương thức HTTP PUT
Thường được sử dụng để cập nhật tài nguyên trên server.
Trong ví dụ, hàm ctrl_put_handler có thể được dùng để thay đổi hành vi của server (ví dụ: vô hiệu hóa hoặc kích hoạt lại các handler GET/POST).
VIII. Lấy thông tin thời tiết bằng HTTP Request
ESP32 có thể hoạt động như một client HTTP, gửi yêu cầu tới các API thời tiết để lấy dữ liệu.
Thực hiện bằng cách:
- Tìm địa chỉ IP của server thời tiết bằng DNS lookup (
getaddrinfo). - Tạo socket và kết nối tới server.
- Gửi một HTTP Request (
GETrequest) tới API. - Đọc và phân tích HTTP Response nhận được.
IX. Giao thức WebSocket
1. Mô hình trao đổi dữ liệu
WebSocket cho phép giao tiếp hai chiều liên tục giữa client và server sau một lần bắt tay (handshake) ban đầu dựa trên HTTP.
Quá trình bắt tay:
- Client gửi HTTP Request với header
Upgrade: websocketvàSec-WebSocket-Key. - Server phản hồi HTTP 101 (Switching Protocols) cùng với
Sec-WebSocket-Accept(đã được tính toán từ client key). - Sau đó, cả hai bên có thể gửi/nhận dữ liệu dưới dạng các "frame" WebSocket mà không cần request/response lặp lại.
2. Định dạng khung dữ liệu WebSocket
Dữ liệu WebSocket được đóng gói thành các khung (frames) theo RFC 6455.
- Các bit: FIN (kết thúc frame), RSV (reserved), Opcode (loại dữ liệu: text, binary, ping, pong), MASK (cho client).
- Payload length: Độ dài dữ liệu.
- Masking key: Dùng để mã hóa dữ liệu từ client.
- Payload Data: Dữ liệu thực tế.
X. Script WebSocket
1. Trình duyệt đọc dữ liệu từ Server
1.1. Server
Sửa đổi server HTTP của ESP-IDF để hỗ trợ WebSocket. Khi nhận được dữ liệu text, server sẽ in ra và sau đó gửi lại các tin nhắn được đánh số i=1 đến i=10 sau mỗi 3 giây.
Ví dụ handler WebSocket (sơ lược):
static esp_err_t echo_handler(httpd_req_t *req)
{
// ... xử lý yêu cầu WebSocket ...
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && strcmp((char *)ws_pkt.payload, "Trigger async") == 0) {
// ...
}
ESP_LOGI(TAG, "Packet data: %s", ws_pkt.payload); // In dữ liệu nhận được
// Gửi lại dữ liệu
for (int i = 0; i < 10; i++) {
char char_arry[10];
sprintf(char_arry, "i=%d", i + 1);
ws_pkt.payload = (uint8_t *)char_arry;
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
ret = httpd_ws_send_frame(req, &ws_pkt);
// ... xử lý lỗi và delay ...
}
// ...
return ret;
}
Cần kích hoạt tùy chọn WebSocket trong menuconfig.
1.2. Trình duyệt
Sử dụng JavaScript để tạo kết nối WebSocket và hiển thị dữ liệu nhận được.
Ví dụ HTML/JavaScript:
<html>
<head>
<script type="text/javascript">
function WebSocketTese()
{
if("WebSocket" in window)
{
// Thay thế địa chỉ IP bằng địa chỉ của ESP32 khi nó hoạt động ở chế độ AP
var ws = new WebSocket("ws://192.168.4.2/ws");
ws.onopen = function() { ws.send("please send data"); };
ws.onmessage = function(evt) {
document.getElementById("demo").innerHTML = evt.data;
};
}
else { alert("Trình duyệt không hỗ trợ websocket!"); }
}
</script>
</head>
<body>
<div>
<a href="javascript:WebSocketTese()"> Run WebSocket Client </a>
<p id="demo"> demo data! </p>
</div>
</body>
</html>
Khi nhấn "Run WebSocket Client", trình duyệt sẽ gửi dữ liệu tới ESP32 và cập nhật nội dung thẻ <p id="demo"> với dữ liệu nhận được từ ESP32.
1.3. ESP32 làm Client kết nối Server
Ví dụ này cho thấy ESP32 có thể hoạt động như một WebSocket client, kết nối tới một server WebSocket.
client_init: Khởi tạo task client.register_event: Đăng ký callback.client_start: Bắt đầu kết nối.send_text: Gửi dữ liệu (5 lần).- Sau khi gửi 5 lần, client đợi tín hiệu từ timer. Khi timer hết hạn, client đóng kết nối.
XI. MQTT
1. Giới thiệu MQTT
MQTT (Message Queuing Telemetry Transport) là một giao thức nhắn tin nhẹ, được thiết kế cho các thiết bị IoT và mạng có băng thông thấp hoặc không đáng tin cậy.
- Kiến trúc Publish/Subscribe.
- Topic: Giống như một đường dẫn để phân loại tin nhắn.
2. Giao thức MQTT
Các gói tin MQTT bao gồm:
- Fixed Header: Loại gói tin (CONNECT, PUBLISH, SUBSCRIBE,...).
- Variable Header: Thông tin bổ sung tùy thuộc vào loại gói tin (ví dụ: tên topic, Packet ID).
- Payload: Dữ liệu thực tế của tin nhắn.
3. MQTT Client
Client MQTT kết nối tới một MQTT Broker (server). Nó có thể Publish (gửi) tin nhắn tới các topic và Subscribe (đăng ký nhận) tin nhắn từ các topic.
Ví dụ mã nguồn (MQTT Client):
//----------------------------------------------------------------------
static TimerHandle_t mqtt_timer;
int iID = 0;
esp_mqtt_client_handle_t client = NULL;
//----------------------------------------------------------------------
static void mqtt_timer_callback(TimerHandle_t *timer)
{
ESP_LOGE(TAG, "Time is up");
int msg_id;
char str_arr[10] = " ";
sprintf(str_arr, "data%d", iID);
iID++;
if (10 == iID) { iID = 0; }
if (client != NULL) {
msg_id = esp_mqtt_client_publish(client, "/topic/qos1", str_arr, 0, 1, 0); // Publish đến topic "/topic/qos1"
ESP_LOGI(TAG, "sent publish successful, msg_id=%d data=%s", msg_id, str_arr);
} else { ESP_LOGI(TAG, "client is null"); }
}
//----------------------------------------------------------------------
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id);
esp_mqtt_event_handle_t event = event_data;
client = event->client; // Lưu handle client
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 0); // Subscribe topic "/topic/qos1"
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
// Tạo và khởi động timer
mqtt_timer = xTimerCreate("mqtt timer", 5000 / portTICK_PERIOD_MS, pdTRUE, NULL, mqtt_timer_callback);
xTimerStart(mqtt_timer, portMAX_DELAY);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
xTimerStop(mqtt_timer, portMAX_DELAY); // Dừng timer khi disconnect
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA: // Nhận dữ liệu từ topic đã subscribe
ESP_LOGI(TAG, "MQTT_EVENT_DATA");
printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
printf("DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
// ... xử lý lỗi ...
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
Sử dụng MQTT Broker công cộng như test.mosquitto.org hoặc tự host Mosquitto.
XII. ESP-NOW
1. ESP-NOW là gì?
Giao thức Wi-Fi không kết nối dựa trên tầng liên kết dữ liệu, tối ưu hóa độ trễ và tiêu thụ năng lượng. Hỗ trợ giao tiếp 1-1, 1-nhiều, broadcast mà không cần router trung gian.
2. Định dạng khung dữ liệu ESP-NOW
Dữ liệu ứng dụng được đóng gói và truyền trực tiếp giữa các thiết bị Wi-Fi, với kích thước tối đa khoảng 250 byte.
3. Lấy địa chỉ MAC
Có thể lấy địa chỉ MAC của các giao diện khác nhau trên ESP32 (Wi-Fi STA, Wi-Fi AP, Bluetooth, Ethernet) bằng các hàm hệ thống.
// Ví dụ trong ESP-IDF examples/system/base_mac_address
// Output mẫu:
// I (332) BASE_MAC: Using "0xb0, 0xb2, 0x1c, 0xa8, 0x9c, 0xe4" as base MAC address
// I (332) WIFI_STA MAC: 0xb0, 0xb2, 0x1c, 0xa8, 0x9c, 0xe4
// I (342) SoftAP MAC: 0xb0, 0xb2, 0x1c, 0xa8, 0x9c, 0xe5
// ...
4. Gửi dữ liệu
Sử dụng API esp_now_send() để gửi dữ liệu tới một địa chỉ MAC đích.
Ví dụ mã nguồn (Gửi ESP-NOW):
static void example_espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status)
{
if (status == ESP_NOW_SEND_SUCCESS) { ESP_LOGI(TAG, "ESP_NOW_SEND_SUCCESS"); }
else { ESP_LOGE(TAG, "ESP_NOW_SEND_FAIL"); }
}
static uint8_t target_mac[ESP_NOW_ETH_ALEN] = {0xb0, 0xb2, 0x1c, 0xa8, 0x9c, 0xe4}; // Địa chỉ MAC của thiết bị nhận
char send_buf[] = "Hello I am little one";
static void example_espnow_task(void *pvParameter)
{
while (1) {
if (esp_now_send(target_mac, (uint8_t*)send_buf, sizeof(send_buf)) != ESP_OK) {
ESP_LOGE(TAG, "Send error");
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
static esp_err_t example_espnow_init(void)
{
ESP_ERROR_CHECK(esp_now_init());
ESP_ERROR_CHECK(esp_now_register_send_cb(example_espnow_send_cb));
esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t));
if (peer == NULL) { /* Xử lý lỗi */ return ESP_FAIL; }
memset(peer, 0, sizeof(esp_now_peer_info_t));
peer->channel = CONFIG_ESPNOW_CHANNEL;
peer->ifidx = ESPNOW_WIFI_IF;
peer->encrypt = false;
memcpy(peer->peer_addr, target_mac, ESP_NOW_ETH_ALEN);
ESP_ERROR_CHECK(esp_now_add_peer(peer));
free(peer);
xTaskCreate(example_espnow_task, "example_espnow_task", 2048, NULL, 4, NULL);
return ESP_OK;
}
5. Nhận dữ liệu
Đăng ký một hàm callback esp_now_register_recv_cb() để xử lý dữ liệu nhận được.
Ví dụ mã nguồn (Nhận ESP-NOW):
static void example_espnow_recv_cb(const uint8_t *mac_addr, const uint8_t *data, int len)
{
ESP_LOGE(TAG, "Receive data = %s", data); // In dữ liệu nhận được
}
static esp_err_t example_espnow_init(void)
{
ESP_ERROR_CHECK(esp_now_init());
ESP_ERROR_CHECK(esp_now_register_recv_cb(example_espnow_recv_cb)); // Đăng ký callback nhận
// ...
return ESP_OK;
}
6. Broadcast dữ liệu
Để gửi broadcast, đặt địa chỉ MAC đích thành địa chỉ broadcast: 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF.
static uint8_t target_mac[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // Địa chỉ broadcast
char send_buf[] = "This is broadcast";