距离实现一个完整的物联网小应用只差最后一步了,今天聊聊怎么样在手机上对ESP32芯片发送指令和接收数据,并借助ESP官方的接口——rainmaker,来实现远程控制和通信。我们也借由此进入智能家居时代1.0(部分物联网概念可以看看【序】在23年谈物联网)
目录
level 1:通过socket广播收发实现本地控制
建立TCP SCOKET CLIENT通信
建立TCP SCOKET SEVER通信
小结
level 2:更广泛的传输--UDP通信 & 通过远程控制实现点灯
总结
虽然在上一篇中我们已经学习到了如何让ESP32-C3通过WiFi连接互联网,以及如何通过UDP广播的方式通过手机上的esp touch为ESP32轻松配置网络(链接指路→ESP32 从scan到smart config 讲透WIFI配置)但我们仍然需要更进一步,如果把互联网比作是不同端口之间的路线的话,处理器如何判断哪些数据是需要的,哪些数据是不需要的呢?这是我们今天所想要解决的问题。
所以这里我们就需要更进一步引入协议的概念。一般情况下基于WiFi和以太网的设备都会原生运行我们比较熟知的互联网TCP/IP协议栈,通过它,我们可以大大降低数据本身协议的适配和开发难度(but 虽然降低了,对于入门来说仍然有很多东西要学)
TCP即传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议,分为服务器和客户端。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
level 1:通过socket广播收发实现本地控制
开始广播收发之前,我们需要下载一个网络调试工具(NetAssist)的软件。可以帮助我们调节端口的位置地址以及相关的参数的,我们可以通过它来进行内容的收发。下载链接指路(链接源于网络):NetAssist网络调试助手.exe
安装完成之后打开会看到如下的界面:
其实乍一看SOCKET概念的时候,我还是有一点懵的,因为之前没有做过网络通信相关的实验,花了好一阵子才开始理清整体的结构和概念,上面的图一定要结合代码多看几遍(当然,我下面的注释也会按照层级来详细解释)
首先要解释一下这个client是什么,在上面TCP通信的框图里面,我们看到了左半部分作为客户端,右半部分作为服务端;connect之后,客户端可以直接提交内容和接受信息,但服务端在收发信息之前,需要bind listen accept确立状态。
所以我们先从左半边相对简单的的客户端去分析~
建立TCP SCOKET CLIENT通信
首先我们从整体上对整个代码的概念进行认识,因为我们esp是client模式,所以要先去看电脑端的 网络调试助手选择的ip和端口,并且填入到最上面这里#define,不然则会无法配对。关于函数部分的详细原理会在后面的小结部分梳理,先从整体上对各个功能模块认知,建立宏观的基础概念更有利于理解(个人看法)。
源代码如下:
#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; //调用了另一个文件中的 #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);
}
因为调用了其他文件夹的文件,所以需要修改一下顶层的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部分有没看懂的部分也没关系,可能学完sever部分会有新的观念,还是老规矩,我们先从整体的角度出发:
源代码如下:
#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; // 空中止接收到任何内容都视为字符串
ESP_LOGI(TAG, "Socket(%d) Received %d bytes: %s", sock, len, rx_buffer);
// send() 可以返回定义长度更短的字符,所以有一点裕度更好(个人翻译的,不一定准确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);
}
小结
整体梳理一下所用到的函数部分,其实不难看出几乎每一个步骤都是围绕着socket操作的。
socket创建
socket绑定
socket监听
socket连接
发送数据
接受数据
socket关闭
socket释放
level 2:更广泛的传输--UDP通信 & 通过远程控制实现点灯
UDP 是 User Datagram Protocol 的简称, 中文名是用户数据报协议, 是 OSI (Open System Interconnection, 开放式系统互联)参考模型中一种无连接的传输层协议,在网络中它与 TCP 协议一样用于处理数据包,是一种无连接的协议。
在 OSI 模型中,UDP和TCP一样,在传输层(第四层),处于 IP 协议的上一层。UDP 协议的主要作用是将网络数据流量压缩成数据包的形式。一个典型的数据包就是一个二进制数据的传输单位。每一个数据包的前 8 个字节用来包含报头信息,剩余字节则用来包含具体的传输数据。UDP 有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
简单来说UDP就是没有确TCP协议。TCP每发出一个数据包都要求确认,如果有一个数据包丢失,就收不到确认,发送方就必须重发这个数据包。为了保证传输的可靠性,TCP协议在UDP基础之上建立了三次对话的确认机制,即在正式收发数据前,必须和对方建立可靠的连接。TCP数据包和UDP一样,都是由首部和数据两部分组成,唯一不同的是,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
UDP 用来支持那些需要在计算机之间传输数据的网络应用。 包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用 UDP 协议。 UDP 协议从问世至今已经被使用了很多年, 虽然其最初的光彩已经被一些类似协议所掩盖,但是即使是在今天 UDP 仍然不失为一项非常实用和可行的网络传输层协议。
下图是UDP的传输过程,可以看到相比于TCP协议,UDP简化了一些步骤:
这一部分原理和TCP部分大致相同,因为网上找到的例程有点小bug还没有解决,所以暂时先简单分享一下UDP方面的思路,后续理解了会更新完整代码,关键实现代码如下(led灯在另一个文件中已经定义,此处也可以删掉):
参考代码如下:
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);//目标端口
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);
}
总结
对于第一次接触本地控制来说,tcp/ip真的可以算是一个难啃的骨头,会有很多新的概念,会综合很多部分的使用;最重要的是,不同例程的思路也迥然不同,官方的例程不够全面也没有注解,而第三方的例程有时候的定义和用法又需要重新理解,还有一些嵌套的思路有时候就像解一团绳结一样,如果不是对整体特别熟练,理解起来也会非常头大。
从全局的角度一点点入手,分解、拆开之后,会清晰很多;有些小块不懂的地方其实也不用死磕,可以先记录下来,之后再一点点看,慢慢会有一些思路,很多问题在不知不觉中就明白了。文章来源:https://www.toymoban.com/news/detail-603263.html
这章主要还是聊的局域网内的本地控制,现在我们在同一个wifi下已经能通过电脑远程控制板子了,再加上之前的smart config,可以做出一些不错的尝试。下一章更近一步,通过MQTT协议和HTTP协议的学习,获得联网信息,再到接入esp rainmaker,达到远程控制。
文章来源地址https://www.toymoban.com/news/detail-603263.html
到了这里,关于4·ESP32-C3入门教程——从本地控制走向云端控制(TCP/IP UDP篇)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!