? ? ? ? 距離實現(xiàn)一個完整的物聯(lián)網(wǎng)小應(yīng)用只差最后一步了,今天聊聊怎么樣在手機上對ESP32芯片發(fā)送指令和接收數(shù)據(jù),并借助ESP官方的接口——rainmaker,來實現(xiàn)遠程控制和通信。我們也借由此進入智能家居時代1.0(部分物聯(lián)網(wǎng)概念可以看看【序】在23年談物聯(lián)網(wǎng))
? ? ? ?
目錄
level 1:通過socket廣播收發(fā)實現(xiàn)本地控制
建立TCP SCOKET CLIENT通信
建立TCP SCOKET SEVER通信
小結(jié)
level 2:更廣泛的傳輸--UDP通信 & 通過遠程控制實現(xiàn)點燈
總結(jié)
????????雖然在上一篇中我們已經(jīng)學(xué)習(xí)到了如何讓ESP32-C3通過WiFi連接互聯(lián)網(wǎng),以及如何通過UDP廣播的方式通過手機上的esp touch為ESP32輕松配置網(wǎng)絡(luò)(鏈接指路→ESP32 從scan到smart config 講透WIFI配置)但我們?nèi)匀恍枰M一步,如果把互聯(lián)網(wǎng)比作是不同端口之間的路線的話,處理器如何判斷哪些數(shù)據(jù)是需要的,哪些數(shù)據(jù)是不需要的呢?這是我們今天所想要解決的問題。
? ? ? ? 所以這里我們就需要更進一步引入?yún)f(xié)議的概念。一般情況下基于WiFi和以太網(wǎng)的設(shè)備都會原生運行我們比較熟知的互聯(lián)網(wǎng)TCP/IP協(xié)議棧,通過它,我們可以大大降低數(shù)據(jù)本身協(xié)議的適配和開發(fā)難度(but 雖然降低了,對于入門來說仍然有很多東西要學(xué))
????????TCP即傳輸控制協(xié)議,是一種面向連接的、可靠的、基于字節(jié)流的通信協(xié)議,分為服務(wù)器和客戶端。服務(wù)器端先初始化Socket,然后與端口綁定(bind),對端口進行監(jiān)聽(listen),調(diào)用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然后連接服務(wù)器(connect),如果連接成功,這時客戶端與服務(wù)器端的連接就建立了。客戶端發(fā)送數(shù)據(jù)請求,服務(wù)器端接收請求并處理請求,然后把回應(yīng)數(shù)據(jù)發(fā)送給客戶端,客戶端讀取數(shù)據(jù),最后關(guān)閉連接,一次交互結(jié)束。

level 1:通過socket廣播收發(fā)實現(xiàn)本地控制
? ? ? ? 開始廣播收發(fā)之前,我們需要下載一個網(wǎng)絡(luò)調(diào)試工具(NetAssist)的軟件。可以幫助我們調(diào)節(jié)端口的位置地址以及相關(guān)的參數(shù)的,我們可以通過它來進行內(nèi)容的收發(fā)。下載鏈接指路(鏈接源于網(wǎng)絡(luò)):NetAssist網(wǎng)絡(luò)調(diào)試助手.exe
? ? ? ? ?安裝完成之后打開會看到如下的界面:
????????
????????其實乍一看SOCKET概念的時候,我還是有一點懵的,因為之前沒有做過網(wǎng)絡(luò)通信相關(guān)的實驗,花了好一陣子才開始理清整體的結(jié)構(gòu)和概念,上面的圖一定要結(jié)合代碼多看幾遍(當(dāng)然,我下面的注釋也會按照層級來詳細解釋)
????????首先要解釋一下這個client是什么,在上面TCP通信的框圖里面,我們看到了左半部分作為客戶端,右半部分作為服務(wù)端;connect之后,客戶端可以直接提交內(nèi)容和接受信息,但服務(wù)端在收發(fā)信息之前,需要bind listen accept確立狀態(tài)。
? ? ? ? 所以我們先從左半邊相對簡單的的客戶端去分析~
建立TCP SCOKET CLIENT通信
? ? ? ?首先我們從整體上對整個代碼的概念進行認識,因為我們esp是client模式,所以要先去看電腦端的 網(wǎng)絡(luò)調(diào)試助手選擇的ip和端口,并且填入到最上面這里#define,不然則會無法配對。關(guān)于函數(shù)部分的詳細原理會在后面的小結(jié)部分梳理,先從整體上對各個功能模塊認知,建立宏觀的基礎(chǔ)概念更有利于理解(個人看法)。
源代碼如下:
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.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"
#define HOST_IP_ADDR "192.168.0.133"
#define PORT 3333
static const char *TAG = "example";
static const char *payload = "message from computer";
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;
while (1) //大while(1)
{
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = inet_addr(host_ip);
dest_addr.sin_family = AF_INET; //調(diào)用了另一個文件中的 #define AF_INET 2
dest_addr.sin_port = htons(PORT); //port 上面咱們定義過啦
addr_family = AF_INET; //(同上)
ip_protocol = IPPROTO_IP; //#define IPPROTO_IP 0;
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);
int 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");
while (1) //大while(1)里面的while(1)
{
int err = send(sock, payload, strlen(payload), 0);
if (err < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
// Error occurred during receiving
if (len < 0) {
ESP_LOGE(TAG, "recv failed: errno %d", errno);
break;
}
// Data received
else {
rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip);
ESP_LOGI(TAG, "%s", rx_buffer);
}
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(tcp_client_task, "tcp_client", 4096, NULL, 5, NULL);
}
? ? ? ? 因為調(diào)用了其他文件夾的文件,所以需要修改一下頂層的cmakelist(注意,不是main文件夾里面的),這樣可以鏈接到通信所需要用到的兩個頭文件。
cmake_minimum_required(VERSION 3.5)
set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(mqtt_tcp)
建立TCP SCOKET SEVER通信
? ? ? ? 作為SERVER端,其實代碼中需要考慮的東西是更多的,不能僅僅看作只增加了bind()、listen()和accept();不過如果client部分有沒看懂的部分也沒關(guān)系,可能學(xué)完sever部分會有新的觀念,還是老規(guī)矩,我們先從整體的角度出發(fā):
源代碼如下:
#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 3333
#define KEEPALIVE_IDLE 5
#define KEEPALIVE_INTERVAL 5
#define KEEPALIVE_COUNT 3
static const char *TAG = "tcp server";
void wifi_get_ip(void)
{
tcpip_adapter_ip_info_t ipInfo;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ipInfo);
ESP_LOGI(TAG, "wifi_get_ip ip=%s", ip4addr_ntoa(&(ipInfo.ip.addr)));
}
static void do_retransmit(void *pvParameters)
{
int sock = (int)pvParameters;
int len;
char rx_buffer[128];
ESP_LOGI(TAG, "do_retransmit(%d)", sock);
while(true)
{
len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
if (len < 0) {
ESP_LOGE(TAG, "Socket(%d) Error occurred during receiving: errno %d", sock, errno);
shutdown(sock, 0);
close(sock);
vTaskDelete(NULL);
}
else if (len == 0) {
ESP_LOGW(TAG, "Socket(%d) Connection closed", sock);
}
else {
rx_buffer[len] = 0; // 空中止接收到任何內(nèi)容都視為字符串
ESP_LOGI(TAG, "Socket(%d) Received %d bytes: %s", sock, len, rx_buffer);
// send() 可以返回定義長度更短的字符,所以有一點裕度更好(個人翻譯的,不一定準(zhǔn)確qwq)
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, "Socket(%d) Error occurred during sending: errno %d", sock, errno);
}
to_write -= written;
}
}
}
}
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;
wifi_get_ip();
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;
}
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));
ESP_LOGI(TAG, "Socket created");
int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
ESP_LOGE(TAG, "IPPROTO: %d", addr_family);
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; // Large enough for both IPv4 or IPv6
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;
}
// Set tcp keepalive option
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));
// Convert ip address to string
if (source_addr.ss_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
}
ESP_LOGI(TAG, "Socket accepted ip address: %s | %d", addr_str, sock);
// do_retransmit(sock);
xTaskCreate(do_retransmit, "do_retransmit", 4096, (void*)sock, 6, NULL);
}
CLEAN_UP:
close(listen_sock);
vTaskDelete(NULL);
}
void app_main(void)
{
nvs_flash_init();
esp_netif_init();
esp_event_loop_create_default();
example_connect();
xTaskCreate(tcp_server_task, "tcp_server", 4096, (void*)AF_INET, 5, NULL);
}
小結(jié)
整體梳理一下所用到的函數(shù)部分,其實不難看出幾乎每一個步驟都是圍繞著socket操作的。
socket創(chuàng)建
socket綁定
socket監(jiān)聽
socket連接
發(fā)送數(shù)據(jù)
接受數(shù)據(jù)
socket關(guān)閉
socket釋放
level 2:更廣泛的傳輸--UDP通信 & 通過遠程控制實現(xiàn)點燈
????????UDP?是?User?Datagram?Protocol?的簡稱,?中文名是用戶數(shù)據(jù)報協(xié)議,?是?OSI?(Open?System?Interconnection,?開放式系統(tǒng)互聯(lián))參考模型中一種無連接的傳輸層協(xié)議,在網(wǎng)絡(luò)中它與?TCP?協(xié)議一樣用于處理數(shù)據(jù)包,是一種無連接的協(xié)議。
????????在?OSI?模型中,UDP和TCP一樣,在傳輸層(第四層),處于?IP?協(xié)議的上一層。UDP?協(xié)議的主要作用是將網(wǎng)絡(luò)數(shù)據(jù)流量壓縮成數(shù)據(jù)包的形式。一個典型的數(shù)據(jù)包就是一個二進制數(shù)據(jù)的傳輸單位。每一個數(shù)據(jù)包的前?8?個字節(jié)用來包含報頭信息,剩余字節(jié)則用來包含具體的傳輸數(shù)據(jù)。UDP?有不提供數(shù)據(jù)包分組、組裝和不能對數(shù)據(jù)包進行排序的缺點,也就是說,當(dāng)報文發(fā)送之后,是無法得知其是否安全完整到達的。
????????簡單來說UDP就是沒有確TCP協(xié)議。TCP每發(fā)出一個數(shù)據(jù)包都要求確認,如果有一個數(shù)據(jù)包丟失,就收不到確認,發(fā)送方就必須重發(fā)這個數(shù)據(jù)包。為了保證傳輸?shù)目煽啃?,TCP協(xié)議在UDP基礎(chǔ)之上建立了三次對話的確認機制,即在正式收發(fā)數(shù)據(jù)前,必須和對方建立可靠的連接。TCP數(shù)據(jù)包和UDP一樣,都是由首部和數(shù)據(jù)兩部分組成,唯一不同的是,TCP數(shù)據(jù)包沒有長度限制,理論上可以無限長,但是為了保證網(wǎng)絡(luò)的效率,通常TCP數(shù)據(jù)包的長度不會超過IP數(shù)據(jù)包的長度,以確保單個TCP數(shù)據(jù)包不必再分割。
????????UDP?用來支持那些需要在計算機之間傳輸數(shù)據(jù)的網(wǎng)絡(luò)應(yīng)用。?包括網(wǎng)絡(luò)視頻會議系統(tǒng)在內(nèi)的眾多的客戶/服務(wù)器模式的網(wǎng)絡(luò)應(yīng)用都需要使用?UDP?協(xié)議。?UDP?協(xié)議從問世至今已經(jīng)被使用了很多年,?雖然其最初的光彩已經(jīng)被一些類似協(xié)議所掩蓋,但是即使是在今天?UDP?仍然不失為一項非常實用和可行的網(wǎng)絡(luò)傳輸層協(xié)議。
? ? ? ? 下圖是UDP的傳輸過程,可以看到相比于TCP協(xié)議,UDP簡化了一些步驟:

????????這一部分原理和TCP部分大致相同,因為網(wǎng)上找到的例程有點小bug還沒有解決,所以暫時先簡單分享一下UDP方面的思路,后續(xù)理解了會更新完整代碼,關(guān)鍵實現(xiàn)代碼如下(led燈在另一個文件中已經(jīng)定義,此處也可以刪掉):
參考代碼如下:
static struct sockaddr_in dest_addr; //遠端地址
socklen_t dest_addr_socklen = sizeof(dest_addr);
static int udp_socket = 0; //連接socket
TaskHandle_t xUDPRecvTask = NULL;
void udp_send_data(char* data, int len)
{
if(udp_socket>0){
int err = sendto(udp_socket, data, len, 0, (struct sockaddr *)&dest_addr, dest_addr_socklen);
if (err < 0) printf( "Error occurred during sending: errno %d", errno);
}
}
void udp_recv_data(void *pvParameters){
socklen_t socklen = sizeof(dest_addr);
uint8_t rx_buffer[1024] = {0};
printf("create udp recv\n");
while (1)
{
int len = recvfrom(udp_socket, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&dest_addr, &dest_addr_socklen);
if(len > 0){
if(len == 2 && rx_buffer[0]=='o' && rx_buffer[1]=='n') led_red(LED_ON);
else if(len == 3 && rx_buffer[0]=='o' && rx_buffer[1]=='f' && rx_buffer[2]=='f') led_red(LED_OFF);
else
{
rx_buffer[len] = 0; //未尾增加"\0,確保長度"
printf("Received %d bytes: %s.\n", len, rx_buffer);
udp_send_data((char*)rx_buffer, len);
}
}
}
}
void udp_ini_client(void *pvParameters){
if(udp_socket>0){
close(udp_socket);
udp_socket=0;
}
udp_socket = socket(AF_INET,SOCK_DGRAM,0);
printf("connect_socket:%d\n",udp_socket);
if(udp_socket < 0){
printf( "Unable to create socket: errno %d", errno);
return;
}
dest_addr.sin_addr.s_addr = inet_addr("255.255.255.255");
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(UDP_PORT);//目標(biāo)端口
printf("Socket created, sending to 255.255.255.255:%d", UDP_PORT);
struct sockaddr_in Loacl_addr;
Loacl_addr.sin_addr.s_addr = htonl(INADDR_ANY);
Loacl_addr.sin_family = AF_INET;
Loacl_addr.sin_port = htons(UDP_PORT);
uint8_t res = 0;
res = bind(udp_socket,(struct sockaddr *)&Loacl_addr,sizeof(Loacl_addr));
if(res != 0){
printf("bind error\n");
}
if(xUDPRecvTask != NULL){
vTaskDelete(xUDPRecvTask);
xUDPRecvTask = NULL;
}
xTaskCreate(&udp_recv_data,"udp_recv_data",2048*2,NULL,10,&xUDPRecvTask);
vTaskDelete(NULL);
}
void create_udp()
{
xTaskCreate(&udp_ini_client, "udp_ini_client", 4096, NULL, 5, NULL);
}
總結(jié)
? ? ? ? 對于第一次接觸本地控制來說,tcp/ip真的可以算是一個難啃的骨頭,會有很多新的概念,會綜合很多部分的使用;最重要的是,不同例程的思路也迥然不同,官方的例程不夠全面也沒有注解,而第三方的例程有時候的定義和用法又需要重新理解,還有一些嵌套的思路有時候就像解一團繩結(jié)一樣,如果不是對整體特別熟練,理解起來也會非常頭大。
????????從全局的角度一點點入手,分解、拆開之后,會清晰很多;有些小塊不懂的地方其實也不用死磕,可以先記錄下來,之后再一點點看,慢慢會有一些思路,很多問題在不知不覺中就明白了。文章來源:http://www.zghlxwxcb.cn/news/detail-603263.html
? ? ? ? 這章主要還是聊的局域網(wǎng)內(nèi)的本地控制,現(xiàn)在我們在同一個wifi下已經(jīng)能通過電腦遠程控制板子了,再加上之前的smart config,可以做出一些不錯的嘗試。下一章更近一步,通過MQTT協(xié)議和HTTP協(xié)議的學(xué)習(xí),獲得聯(lián)網(wǎng)信息,再到接入esp rainmaker,達到遠程控制。
????????文章來源地址http://www.zghlxwxcb.cn/news/detail-603263.html
到了這里,關(guān)于4·ESP32-C3入門教程——從本地控制走向云端控制(TCP/IP UDP篇)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!