LWIP – Light weight IP là một bộ thư viện mã nguồn mở được thiết kế dành cho những hệ thống có tài nguyên tương đối hạn chế, phù hợp với các hệ thống nhúng. Hỗ trợ tương đối đầy đủ các giao thức mạng trên nền TCP/IP. Có thể hỗ trợ giao tiếp với vi điều khiển thông qua Serial hoặc Ethernet. TAPIT chia sẻ đến các bạn chuỗi bài viết hướng dẫn về lý thuyết thư viện, porting các function, kết nối MQTT, kết nối HTTP trên nền tảng LWIP và tối ưu chương trình trên vi điều khiển STM32.
Phần 1, mình đã giới thiệu đến các bạn bộ thư viện LWIP, hướng dẫn cách add thư viện, cách để port các file và port các function khi sử dụng LWIP. Xem tại đây.
Phần 2, mình đưa ra hướng dẫn thiết kế chương trình hàm main, các hàm chức năng và một số Demo. Xem tại đây.
Tại phần này, mình sẽ thử nghiệm thực hiện kết nối HTTP và MQTT trên nền tảng LWIP.
1. Kết nối HTTP
Mình sẽ thử nghiệm thực hiện 1 lệnh HTTP Get đến đường dẫn http://httpbin.org/get
1.1. Thiết kế chương trình
Ý tưởng của mình như sau:
- Chờ cho PPP đã được kết nối.
- Nếu đã kết nối thì truyền các thông số của HTTP URL vào hàm cấu hình.
- Đăng kí các hàm callback nhận dữ liệu.
- Thực hiện lệnh HTTP Get.
1.2. Lưu đồ thuật toán
- Trong đó http_connection_t chứa các thông tin mà server sẽ kết nối đến, chứa các thông tin
http_connection_t | proxy_addr | Địa chỉ của server, có kiểu số nguyên dương 32 bit |
proxy_port | HTTP port (80,443…) | |
use_proxy | 1 hoặc 0 | |
httpc_result_fn | Hàm nhận dữ liệu trả về của http | |
httpc_headers_done_fn | Hàm callback khi socket nhận đủ dữ liệu của header | |
http_state_t | Chưa toàn bộ internal state-machine, timeout, bộ đệm… của HTTP |
- Implement hàm xử lí khi nhận đủ header trong HTTP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
err_t httpc_headers_done_callback(httpc_state_t *connection, void *arg, struct pbuf *hdr, u16_t hdr_len, u32_t content_len) { DEBUG_INFO("httpc_headers_callback, content length %d\r\n", content_len); if (content_len == 0xFFFFFFFF) { /* Content length không hợp lệ, có thể có những lí do sau - Trong header server trả về không có trường "Content-Length" - Server trả về dạng stream data, dữ liệu kết thúc khi server đóng kết nối - Các lí do khác */ DEBUG_INFO("Invalid content length\r\n"); } else { DEBUG_INFO("HTTP content length %u bytes\r\n", content_len); } return ERR_OK; } |
- Hàm nhận status của kết nối HTTP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
static void httpc_result_callback(void *arg, httpc_result_t httpc_result, u32_t rx_content_len, u32_t srv_res, err_t err) { DEBUG_INFO("result: %d, content len: %d, status code: %d\r\n", httpc_result, rx_content_len, srv_res); switch (err) { case HTTPC_RESULT_OK: /** File successfully received */ { DEBUG_INFO("HTTPC_RESULT_OK\r\n"); } break; case HTTPC_RESULT_ERR_UNKNOWN: /** Unknown error */ //break; case HTTPC_RESULT_ERR_CONNECT: /** Connection to server failed */ //break; case HTTPC_RESULT_ERR_HOSTNAME: /** Failed to resolve server hostname */ //break; case HTTPC_RESULT_ERR_CLOSED: /** Connection unexpectedly closed by remote server */ //break; case HTTPC_RESULT_ERR_TIMEOUT: /** Connection timed out (server didn't respond in time) */ //break; case HTTPC_RESULT_ERR_SVR_RESP: /** Server responded with an error code */ //break; case HTTPC_RESULT_ERR_MEM: /** Local memory error */ //break; case HTTPC_RESULT_LOCAL_ABORT: /** Local abort */ //break; case HTTPC_RESULT_ERR_CONTENT_LEN: /** Content length mismatch */ DEBUG_ERROR("Error content length\r\n"); break; default: DEBUG_INFO("httpc_result_callback error %d\r\n", err); break; } } |
- Hàm cuối cùng chúng ta cần implement để nhận toàn độ data từ HTTP, hàm này chỉ được gọi khi thiết bị kết nối thành công và bắt đầu nhận dữ liệu. Khi nhận data và xử lí xong, hệ thống cần giải phóng memory cho HTTP bằng hàm “pbuf_free(p)”. Trong trường hợp có lỗi xảy xa, cần phải đóng kết nối bằng cách gọi hàm “tcp_close”
- Dữ liệu nhận về có độ dài “q->len”, nằm tại địa chỉ “q->payload”, ở demo này mình sẽ in dữ liệu nhận về qua cổng debug.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
static err_t httpc_file_recv_callback(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { // Nếu có dữ liệu đến, kiểm tra buffer // Nếu buffer không hợp lệ (NULL) thì đóng kết nối TCP if (p) { struct pbuf *q; for (q = p; q; q = q->next) { // Đây là dữ liệu nhận về, có độ dài bằng q->len, và data là q->payload DEBUG_INFO("HTTP data %.*s\r\n", q->len, q->payload); } tcp_recved(tpcb, p->tot_len); pbuf_free(p); } else { DEBUG_WARN("tcp_close\r\n"); tcp_close(tpcb); return ERR_ABRT; } return ERR_OK; } |
Vậy là các hàm callback xử lí dữ liệu đã xong, chúng ta khởi tạo tham số và tiến hành kết nối. Giả sử bạn cần kết nối đến “http://httpbin.org/get” thì các tham số truyền vào httpc_get_file_dns như sau:
- Server name “org”
- Port 80
- File “/get”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* Init Http connection params */ m_conn_settings_try.use_proxy = 0; m_conn_settings_try.headers_done_fn = httpc_headers_done_callback; m_conn_settings_try.result_fn = httpc_result_callback; DEBUG_INFO("HTTP url %s%s, port %d\r\n", m_http_cfg.url, m_http_cfg.file, m_http_cfg.port); // Kết nối HTTP err_t error = httpc_get_file_dns((const char*)m_http_cfg.url, m_http_cfg.port, m_http_cfg.file, &m_conn_settings_try, httpc_file_recv_callback, NULL, &m_http_connection_state); m_conn_settings_try.headers_done_fn = httpc_headers_done_callback; m_conn_settings_try.result_fn = httpc_result_callback; if (error != ERR_OK) { DEBUG_INFO("Cannot connect HTTP server, error %d\r\n", error); return false; } |
1.3. Kết quả thử nghiệm
2. Kết nối MQTT
Design state machine – Thiết kế máy trạng thái
– Ý tưởng của mình như sau
- Kiểm tra xem PPP stack đã được kết nối hay chưa.
- Nếu PPP đã kết nối thành công thì khởi tạo các tham số của mqtt như broker address, port…
- Thực hiện phân giải domain name của mqtt broker sang địa chỉ IP.
- Sau khi phân giải IP thành công thì thực hiện kết nối MQTT, gán trạng thái MQTT thành “Connecting”.
- Nếu kết nối thành công thì gán trạng thái MQTT thành “Connected”, sau đó tiến hành subscribe vào các topic.
- Nếu trạng thái MQTT bằng “Connected” thì định kì gửi bản tin ping, định kì resubcribe lại các topic cần thiết với chu kì đã biết trước.
– Các hàm callback của MQTT
- Hàm phân giải DNS dns_gethostbyname với 2 tham số quan trọng truyền vào là broker name và hàm xử lí callback “mqtt_dns_found”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
err_t err = dns_gethostbyname(m_cfg.broker_addr, &m_mqtt_server_address, mqtt_dns_found, NULL); if (err == ERR_INPROGRESS) { /* DNS request sent, wait for sntp_dns_found being called */ DEBUG_INFO("sntp_request: %d - Waiting for server %s address to be resolved\r\n", err, m_cfg.broker_addr); } else if (err == ERR_OK) { DEBUG_INFO("DNS resolved aready, host %s, mqtt_ipaddr = %s\r\n", m_cfg.broker_addr, ipaddr_ntoa(&m_mqtt_server_address)); m_is_dns_resolved = 1; } |
– Hàm callback phân giải DNS (thành công hoặc thất bại), với kết quả nằm tại con trỏ “ipaddr”. Trong trường hợp con trỏ “ipaddr” khác null thì thiết bị đã phân giải DNS thành công.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/** * @brief DNS found callback when using DNS names as server address. */ static void mqtt_dns_found(const char *hostname, const ip_addr_t *ipaddr, void *arg) { DEBUG_INFO("mqtt_dns_found: %s\r\n", hostname); LWIP_UNUSED_ARG(hostname); LWIP_UNUSED_ARG(arg); if (ipaddr != NULL) { /* Address resolved, send request */ m_mqtt_server_address.addr = ipaddr->addr; DEBUG_INFO("Server address resolved = %s\r\n", ipaddr_ntoa(&m_mqtt_server_address)); m_is_dns_resolved = 1; m_mqtt_process_now = true; } else { /* DNS resolving failed -> try another server */ DEBUG_INFO("mqtt_dns_found: Failed to resolve server address resolved, trying next server\r\n"); m_is_dns_resolved = 0; } } |
– Hàm subcribe 1 topic, với các tham số chính truyền vào là topic name, QoS và callback
1 2 3 4 5 6 |
/* Subscribe to a topic named "qrm/imei/st_data" with QoS level 1, call mqtt_sub_request_cb with result */ err_t err = mqtt_subscribe(&m_mqtt_static_client, m_mqtt_subscribe_topic_name, SUB_QoS, mqtt_sub_request_cb, NULL); DEBUG_INFO("%s: topic %s\r\n", __FUNCTION__, m_mqtt_subscribe_topic_name); |
– Hàm publish data lên MQTT broker và callback
1 2 3 4 5 6 7 8 |
err_t err = mqtt_publish(&m_mqtt_static_client, m_mqtt_publish_topic_name, m_mqtt_tx_buffer, strlen(m_mqtt_tx_buffer), PUB_QoS, PUB_RETAIN, mqtt_pub_request_cb, NULL); |
– Hàm nhận dữ liệu từ MQTT broker
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
static void mqtt_incoming_data_cb(void *arg, const u8_t *data, u16_t len, u8_t flags) { DEBUG_INFO("MQTT data cb, length %d, flags %u\r\n", len, (unsigned int)flags); if (flags & MQTT_DATA_FLAG_LAST) { /* Last fragment of payload received (or whole part if payload fits receive buffer See MQTT_VAR_HEADER_BUFFER_LEN) */ DEBUG_INFO("Payload data: %s\r\n", (const char *)data); if (m_is_valid_sub_topic == 1) { m_is_valid_sub_topic = 0; /* Update firmware message */ if (strstr((char *)data, "UDFW,")) { DEBUG_INFO("Update firmware\r\n"); } else if (strstr((char *)data, "PLEASE RESET")) { NVIC_SystemReset(); } } // Clear received buffer of client -> du lieu nhan lan sau khong bi thua cua lan truoc, // neu lan truoc gui length > MQTT_VAR_HEADER_BUFFER_LEN memset(m_mqtt_static_client.rx_buffer, 0, MQTT_VAR_HEADER_BUFFER_LEN); } else { /* Handle fragmented payload, store in buffer, write to file or whatever */ } } |
3. Source code
Ở demo này, mình sử dụng 2 thiết bị là 1 kit STM32L083 và 1 kit STM32F4. Các bạn có thể tham khảo ở repository sau: https://github.com/huybk213/lwip_porting
—
Vậy là kết thúc chuỗi bài viết giới thiệu và hướng dẫn thực hành về LWIP. Tổng kết lại toàn bộ nội dung, mình đã giới thiệu đến các bạn:
- Tổng quan về LWIP
- Hướng dẫn khởi tạo Project và thêm thư viện LWIP
- Thiết kế chương trình hàm main, các hàm chức năng và thực hiện Demo
- Kết nối HTTP và MQTT trên nền tảng LWIP
- Chia sẻ Source code mà mình đã từng thực hiện
Chúc các bạn thành công!
Cố vấn tại Cộng đồng Kỹ thuật TAPIT
Trần Văn Huy