【M5Stack物联网开发】第六章 使用互联网

1 HTTP协议

让我们从几个小故事开始理解HTTP协议是什么。

邮寄信件的故事

在一个小镇上,有个叫小明的人。他有一个远方的朋友小红。一天,小明(客户端)想给小红(服务器)写封信(请求)。他在信上写满了想说的话,然后把信装进信封,贴上邮票,写上小红的地址(服务器地址),并把信投入邮筒(通过网络发送请求)

邮递员叔叔(HTTP协议)每天都来收信,把信件送到各个地方。小红收到了小明的信,打开信封,读了信上的内容(服务器处理请求)。她很高兴,决定回信。她写了一封回信,装进信封,贴上邮票,写上小明的地址(客户端地址),并寄了出去。

几天后,小明收到了小红的回信,读了信上的内容(响应)。每次他们通信,邮递员叔叔都不会记得之前送过什么信(无状态),每封信都是独立的。

点餐的故事

在一个热闹的城市,有一家受欢迎的餐厅。小李(客户端)走进餐厅,坐在了一个靠窗的位置(客户端地址)。服务员小张(HTTP协议)走过来,微笑着递上菜单。小李看了看菜单,决定点一份披萨(请求)。小张记下了他的订单,然后把订单送到厨房(通过网络发送请求)

厨房的厨师们忙碌地准备着小李的披萨(服务器处理请求)。不一会儿,披萨做好了。小张把热腾腾的披萨端到了小李的桌上(响应)。每次小李来餐厅点餐,小张都不会记得他上次点了什么(无状态),每次点餐都是独立的。

图书馆借书的故事

在一个安静的图书馆里,小芳(客户端)经常来这里借书。一天,小芳找到了一本她感兴趣的书。她拿着书,走到图书馆管理员阿姨(服务器)那里,填写了一张借书单(请求),递给了阿姨。

阿姨检查了一下书的可借状态(服务器处理请求),然后在系统中登记,并把书借给了小芳(响应)。每次小芳来借书,阿姨都不会记得她之前借了哪些书(无状态),每次借书都是独立的。

问路的故事

在一个繁华的街头,小丽(客户端)迷路了。她看到一个好心的路人叔叔(服务器),便上前询问:“请问去博物馆怎么走?”(请求)。路人叔叔耐心地告诉她详细的路线(响应)

每次小丽问路,路人叔叔都不会记得她之前问过什么(无状态),每次问路都是独立的。

电话咨询的故事

在一个温馨的家中,小强(客户端)遇到了一个产品使用上的问题。他拿起电话,拨打了客服热线。电话接通后,客服小姐姐(服务器)接听了电话。小强描述了他的问题(请求),客服小姐姐仔细听完后,给出了详细的解答(响应)

每次小强打电话咨询,客服小姐姐都不会记得他之前问过什么(无状态),每次咨询都是独立的。

网上购物的故事

在一个舒适的家里,小华正在用电脑(客户端)浏览一个网上商城(服务器)。他看中了一件漂亮的外套,决定购买。小华点击了“加入购物车”按钮,然后点击“下单”(请求)

网上商城(服务器)接收到了小华的订单,确认了库存和支付信息(服务器处理请求)。一切顺利后,商城系统给小华发了一封确认邮件,并安排发货(响应)。每次小华在网上购物,商城系统都不会记得他之前买了什么(无状态),每次购物都是独立的。

上面的故事由三个主要角色构成:客户端、服务器端和HTTP协议;三个基本行为:请求、处理和响应;以及一个重要特性:无状态。

基本的流程是,客户端发起请求,并通过HTTP协议将请求发送到服务器端。服务器端处理请求后,通过HTTP协议将结果返回给客户端,这个过程称为响应。而且,每次请求和响应之间都是独立的,没有关联,这就是无状态的特性。

HTTP数据协议

HTTP(HyperText Transfer Protocol,超文本传输协议)是一种用于在客户端和服务器之间传输数据的协议。它的基本结构包括请求和响应两个部分。

基本概念

  • 请求-响应模型:HTTP是一种请求-响应协议。客户端发送请求到服务器,服务器处理请求并返回响应。
  • 注意:如果请求是从你的计算机发起,那么你的计算机就是客户端,比如打开一个网页,但如果你的计算机同时还接受别人的请求,比如你允许别人远程控制你计算机的桌面,那么此时你的计算机就是服务器端。
  • 无状态协议:HTTP是无状态的,每个请求都是独立的,与之前或之后的请求没有直接的关系。

HTTP 请求(Request)

HTTP请求由以下几个部分组成:

  1. 请求行(Request Line):

    • 方法(Method):表示要如何发送请求,常见的方法有GET、POST、PUT、DELETE等。
    • 请求URI(Request URI):表示要访问的资源的路径,比如/index.html、/pages/res/cat.png。
    • HTTP版本(HTTP Version):表示使用的HTTP协议版本,比如HTTP/1.1。

    示例:

    GET /index.html HTTP/1.1
  2. 请求头(Request Headers):

    • 包含键值对形式的数据,用于提供客户端信息、请求参数等。
    • 常见的请求头有Host、User-Agent、Accept、Content-Type等。
    • 分别表示服务器地址、客户端标识、客户端可以接受的响应数据类型、第四部分请求体的数据类型

    示例:

    Host: www.example.com User-Agent: Mozilla/5.0 Accept: text/html
    由Mozilla/5.0浏览器向www.example.com发送请求,可以接受文本/网页数据
  3. 空行(CRLF):

    • 用于分隔请求头和请求体。
  4. 请求体(Request Body)(可选):

    • 包含需要发送到服务器的数据,通常在POST或PUT请求中使用。

    示例:

    name=John&age=30

HTTP 响应(Response)

HTTP响应由以下几个部分组成:

  1. 状态行(Status Line):

    • HTTP版本(HTTP Version):表示使用的HTTP协议版本,如HTTP/1.1。
    • 状态码(Status Code):表示响应的状态,如200(OK)、404(Not Found)等。
    • 状态描述(Reason Phrase):对状态码的简短描述。

    示例:

    HTTP/1.1 200 OK
  2. 响应头(Response Headers):

    • 包含键值对形式的元数据,用于提供服务器信息、响应参数等。
    • 常见的响应头有Content-Type、Content-Length、Server、Set-Cookie等。

    示例:

    Content-Type: text/html Content-Length: 1234 Server: Apache/2.4.1
  3. 空行(CRLF):

    • 用于分隔响应头和响应体。
  4. 响应体(Response Body):

    • 包含服务器返回的数据内容,如HTML文档、JSON数据等。

    示例:

    html:
    <html> <body> <h1>Hello, World!</h1> </body> </html>
    json:
    {"text": "hello world"}

HTTP 请求示例

在浏览器的地址栏中输入www.example.com/index.html就是完成了一个简单的GET请求

GET /index.html HTTP/1.1 Host: www.example.com User-Agent: Mozilla/5.0 Accept: text/html

HTTP 响应示例

服务器端成功处理请求(200 OK),并返回了一个网页的HTML代码

HTTP/1.1 200 OK Content-Type: text/html Content-Length: 1234 Server: Apache/2.4.1 <html> <body> <h1>Hello, World!</h1> </body> </html>

2 HTTP方法

客户端在向HTTP服务器发送请求时,HTTP协议定义了一些方法来表示对服务器上资源的操作,常见的方法有:

  • GET:客户端向服务器请求指定数据。只获取数据,不对服务器上的数据进行修改。
  • POST:客户端向服务器提交新的数据,通常会导致服务器上的资源发生变化。
  • PUT:客户端向服务器提交指定数据的最新内容。
  • DELETE:客户端向服务器请求删除指定的数据。
  • HEAD:与GET方法类似,但只返回响应头,不返回响应体。
  • OPTIONS:返回服务器支持的HTTP方法。
  • PATCH:客户端向服务器请求对数据进行部分修改。

在实际操作时,并不需要严格按照上述方法向服务器发送请求,只要客户端和服务器之间约定好,也可是只是用GET方法,完成对资源的增删改查工作。

使用GET方式时,有两种方式可以将查询的参数发送给服务器,但需要注意这两种方式都是将数据直接嵌入至URL中的明文传输数据,所以不太安全,如果不希望使用明文传输数据,请使用POST方法。

  1. 查询字符串(Query String):

    • 数据附加在URL的末尾,使用问号?分隔URL和查询字符串。
    • 每个参数键值对使用等号=连接,多个参数使用与号&分隔。
    • 例如:http://example.com/search?keyword=hello&page=2
  2. 路径参数(Path Parameters):

    • 数据直接嵌入在URL路径中,通常用于RESTful API。
    • 例如:http://example.com/users/12345(这里的12345是用户ID)

在HTTP协议中,POST方法用于向服务器发送数据。与GET方法不同,POST方法会将数据包含在请求体中,而不是通过URL传递。以下是使用POST方法传输数据的一些基本步骤和示例:

  1. 设置URL:指定要发送请求的服务器地址。
  2. 设置请求头:包括内容类型(例如application/json)和其他必要的头信息。
  3. 设置请求体:包含要发送的数据,通常是JSON格式。

例如:

Method:POST
URL:https://example.com/api/data
Header:Content-Type: application/json
JSON Data:{"key1":"value1","key2":"value2"}

3 JSON数据

早期HTTP数据协议传输的数据都是普通的TXT文本,但是很多时候我们传输的都是非常结构化的数据,比如C++中的结构体,这时候人们发明了结构化文本数据也就是XML,但是随着网络应用快速发展XML文本的结构又过于复杂,于是人们开始化简XML文件架构,最终产生了JSON数据。

JSON由Douglas Crockford在2000年左右提出,目的是提供一种简单、易用的格式来替代XML,用于在服务器和网页之间传输数据。由于其简洁性和易用性,JSON迅速成为Web开发中最流行的数据交换格式之一。

JSON的结构非常简单,就是由一对大括号括起来的键-值对。

规则

  1. 键(Key):键必须是字符串,用双引号 "" 包围,通常是标识符。

  2. 值(Value):JSON值可以是字符串、数字、对象、数组、布尔值或 null

    • 字符串:用双引号 "" 包围。
    • 数字:可以是整数或浮点数,不需要引号。
    • 布尔值:true 或 false
    • null:表示空值。
  3. JSON对象(JSON Object):对象是一个无序的键值对集合,用花括号 {} 包围。每个键值对由一个字符串键和一个值组成,键和值之间用冒号 : 分隔,键值对之间用逗号 , 分隔。

    { "name": "Alice", "age": 30, "isStudent": false }
    三个键值分别是:name、age、isStudent
    三个值分别是:字符串“Alice”、数字30、布尔值false
  4. 数组(Array):数组是一个有序的值的集合,用方括号 [] 包围,值之间用逗号 , 分隔。

复制代码
[
    "apple",
    "banana",
    "cherry",
    "date",
    "elderberry"
]

或者

[
    {
        "name": "John Doe",
        "age": 30,
        "isStudent": false
    },
    {
        "name": "Jane Smith",
        "age": 25,
        "isStudent": true
    },
    {
        "name": "Emily Johnson",
        "age": 22,
        "isStudent": true
    }
]
复制代码

一个完整的JSON数据例子:

复制代码
{
    "company": {
        "name": "Tech Solutions",
        "location": "Silicon Valley",
        "employees": [
            {
                "name": "John Doe",
                "age": 30,
                "position": "Software Engineer",
                "skills": ["JavaScript", "React", "Node.js"],
                "isFullTime": true,
                "contact": {
                    "email": "john.doe@techsolutions.com",
                    "phone": "123-456-7890"
                }
            },
            {
                "name": "Jane Smith",
                "age": 25,
                "position": "Product Manager",
                "skills": ["Project Management", "Agile", "Scrum"],
                "isFullTime": true,
                "contact": {
                    "email": "jane.smith@techsolutions.com",
                    "phone": "098-765-4321"
                }
            },
            {
                "name": "Emily Johnson",
                "age": 22,
                "position": "Intern",
                "skills": ["Python", "Data Analysis"],
                "isFullTime": false,
                "contact": {
                    "email": "emily.johnson@techsolutions.com",
                    "phone": "555-555-5555"
                }
            }
        ],
        "projects": [
            {
                "projectName": "Project Alpha",
                "deadline": "2023-12-31",
                "budget": 100000,
                "teamMembers": ["John Doe", "Jane Smith"]
            },
            {
                "projectName": "Project Beta",
                "deadline": "2024-06-30",
                "budget": 150000,
                "teamMembers": ["Emily Johnson", "John Doe"]
            }
        ],
        "isHiring": true,
        "openPositions": null
    }
}
复制代码

4 函数指针

4.1 函数指针

函数在和变量对于程序而言,都是一段代码,所以也可以用指针表示其所在的内存位置。

C++中的函数指针是一种特殊类型的指针,它指向的是函数而不是变量。函数指针可以用来调用函数,传递函数作为参数,或者返回函数。函数指针的使用可以提高代码的灵活性和可重用性。以下是一些关于函数指针的基本概念和用法。

定义函数指针

假设有一个函数如下:

int add(int a, int b) { return a + b; }

定义一个指向该函数的指针:

 
// 这个指针的返回值与参数类型必须与函数add相同
int (*funcPtr)(int, int);

初始化函数指针

将函数的地址赋给函数指针:

// 初始化函数指针的时候,不需要取地址符号“&”
funcPtr = add;

使用函数指针调用函数

通过函数指针调用函数:

int result = funcPtr(2, 3); 
// 等价于调用 
// int result = add(2, 3);
// 以上两行代码功能完全一致

函数指针作为参数

函数指针可以作为参数传递给另一个函数。例如:

void compute(int (*operation)(int, int), int x, int y)
{ 
    int result = operation(x, y);
}

compute(add, 5, 3);

函数指针作为返回值

函数指针也可以作为函数的返回值。例如:

复制代码
typedef int (*operationFunc)(int, int); 
operationFunc getOperation(char op) 
{ 
    switch(op) 
    { 
        case '+': 
            return add; 
        // 可以添加更多操作符和相应的函数 
        default: 
            return nullptr; 
    } 
}
 
operationFunc op = getOperation('+'); 
if (op != nullptr) 
{ 
    op(5, 3); 
}
复制代码

4.2 回调函数

使用生活中的例子来解释回调函数可以更容易理解。让我们用一个餐厅点餐和通知顾客取餐的例子来说明回调函数的概念。

餐厅点餐和取餐

场景描述

你去了一家餐厅点餐,点完餐后,你不会一直站在柜台前等待食物,而是找个座位坐下,做其他事情,比如玩手机或聊天。餐厅会在你的餐点准备好后,通知你来取餐。这种通知机制就类似于编程中的回调函数。

对应编程中的回调函数

  1. 顾客点餐(主函数调用):

    • 你去柜台点餐,相当于主函数调用了一个需要回调的操作(如异步操作或事件处理)。
  2. 餐厅记录顾客信息(传递回调函数):

    • 餐厅记录你的信息(比如给你一个取餐号或者你的电话号码),相当于主函数传递了一个回调函数(你的联系方式或取餐号)给餐厅。
  3. 顾客等待,餐厅继续记录另一个顾客的信息(主函数继续执行):

    • 你找到座位坐下,继续做其他事情,相当于主函数继续执行其他代码,不会被阻塞。
  4. 餐厅准备餐点(异步操作进行中):

    • 餐厅在后台准备你的餐点,这个过程是异步的,不会影响你做其他事情。
  5. 餐点准备好,通知顾客(回调函数被调用):

    • 餐点准备好后,餐厅通过取餐号或者电话通知你来取餐,相当于回调函数被调用,通知主函数相应的操作已经完成。
  6. 顾客取餐(回调函数的具体实现):

    • 你收到通知后去取餐,这就是回调函数的具体实现,完成了回调操作。
复制代码
// 回调函数定义、餐点准备后,通知客户
void notifyCustomer(int orderNumber) {
    dishTo(orderNumber);
}

// 后厨开始备餐、并在完成后,调用回调函数callback通知客户
void prepareOrder(int orderNumber, void (*callback)(int)) {
    waitCooking();
    callback(orderNumber); // 通知顾客
}

// 主函数
void order() {
    int orderNumber = 1;
    while(waitCustomer())
    {
        // 主函数会将菜单发送给后厨
        // 后厨会自己独立执行做菜并通知客户操作
        // 这种行为称作异步调用    
        prepareOrder(orderNumber, notifyCustomer);
        orderNumber++;
    }
}
复制代码
  • 同步执行:任务按顺序执行,当前任务未完成前,后续任务不会开始,程序会等待任务完成。
  • 异步执行:任务不会阻塞程序的执行,程序会立即继续执行后续代码,常用于提高程序响应性。
  • 回调函数:用于在特定条件下调用传递的函数,通常用于事件处理和异步操作。

回调函数(Callback Function)是指通过函数指针传递给另一个函数,并在适当的时候由后者调用的函数。回调函数是一种常见的编程模式,尤其在事件驱动编程、异步编程和处理复杂的逻辑时非常有用。例如,假设有一个函数 doSomething,它接受一个函数指针作为参数,并在某个条件下调用这个函数:

复制代码
// 定义一个回调函数
void myCallback(int value) {
    M5.Display.print(value);
}

// 定义一个接受回调函数的函数
// 也称为异步函数
void doSomething(void (*callback)(int)) {
    // doing...
    // something...

    // to do ....

    callback(42);
}

// 主函数
void loop() {
    // 调用doSomething并传递回调函数
    doSomething(myCallback);

    // doing...
    // something...

    // to do ....
}
复制代码

5 字符串操作

在Arduino编程中,String类提供了一系列方便的方法来操作字符串。以下是一些常见的String操作及其用法:

复制代码
// 创建和初始化字符串
String str1 = "Hello, World!";
String str2("Arduino");
String str3 = String(1234); // 从数字创建字符串

// 连接字符串
String str1 = "Hello, ";
String str2 = "World!";
String str3 = str1 + str2; // str3 = "Hello, World!"
str1 += str2; // str1 = "Hello, World!"

// 获取字符串长度
String str = "Arduino";
int length = str.length(); // length = 7

// 访问字符串中的字符
String str = "Arduino";
char ch = str.charAt(0); // ch = 'A'
str.setCharAt(0, 'a'); // str = "arduino"

// 子字符串
String str = "Hello, World!";
String subStr = str.substring(7); // subStr = "World!"
String subStr2 = str.substring(7, 12); // subStr2 = "World"

// 查找字符或子字符串
String str = "Hello, World!";
int index = str.indexOf('W'); // index = 7
int index2 = str.indexOf("World"); // index2 = 7
int lastIndex = str.lastIndexOf('o'); // lastIndex = 8

// 替换子字符串
String str = "Hello, World!";
str.replace("World", "Arduino"); // str = "Hello, Arduino!"

// 去除空白字符
String str = "  Hello, World!  ";
str.trim(); // str = "Hello, World!"

// 转换大小写
String str = "Hello, World!";
str.toLowerCase(); // str = "hello, world!"
str.toUpperCase(); // str = "HELLO, WORLD!"

// 比较字符串
String str1 = "Arduino";
String str2 = "Arduino";
if (str1.equals(str2)) {
    // 两个字符串相等
}

// 忽略大小写比较
if (str1.equalsIgnoreCase("arduino")) {}

// 转换为基本数据类型
String str = "1234";
int num = str.toInt(); // num = 1234
String str2 = "12.34";
float fnum = str2.toFloat(); // fnum = 12.34

// 格式化字符串
String str = String("Value: ");
str += String(1234, DEC); // 十进制格式
str += String(0xFF, HEX); // 十六进制格式
str += String(7, BIN); // 二进制格式
复制代码

6 使用HTTP协议

6.1 连接WiFi

Wi-Fi 模式

ESP32 支持多种 Wi-Fi 工作模式,以适应不同的网络需求:

  1. 站点模式(Station Mode, STA):将ESP32变成连接WiFi的客户端

    • ESP32 作为一个 Wi-Fi 客户端,连接到现有的 Wi-Fi 网络(如家庭路由器)。
    • 适用于需要访问互联网或局域网资源的应用。
  2. 接入点模式(Access Point Mode, AP):将ESP32变成无线发射器

    • ESP32 作为一个 Wi-Fi 热点,允许其他设备连接到它。
    • 适用于创建本地网络供其他设备连接,例如 IoT 设备的本地控制。
  3. 混合模式(Station + AP Mode, STA+AP):

    • ESP32 同时作为客户端和热点,既可以连接到现有网络,又能提供热点供其他设备连接。
    • 适用于需要中继网络或提供本地访问的应用。

IP地址

IP地址(互联网协议地址,Internet Protocol Address)是分配给每个连接到互联网或局域网的设备的唯一标识符。IP地址用于在网络上定位和识别设备,确保数据能够正确传输到目标设备。

只要是接入网络的设备,都必须拥有一个IP地址,不同网络之间的IP地址,理论上无法直接访问。

IP地址与地图中的地址类似,比如北京市,海淀区,西土城路,4号。这个地址不会发生发生变化,但是随着时间的推移,这个位置的建筑物(客户端)却在事实发生变化。

 

主要类型:

  • IPv4地址:由四个十进制数(每个数在0到255之间)组成,格式如192.168.1.1。
  • IPv6地址:由八组十六进制数(每组四个字符)组成,格式如2001:0db8:85a3:0000:0000:8a2e:0370:7334。

IP地址的分配方式:

  • 静态IP地址:手动配置,固定不变。
  • 动态IP地址:通过DHCP(动态主机配置协议)自动分配,可能会变化。

WiFi与IP地址的关系

  • 当设备通过WiFi连接到网络时,通常会通过DHCP从路由器或无线接入点获取一个动态IP地址。
  • 路由器或无线接入点会管理局域网内的IP地址分配,确保每个设备都有一个唯一的IP地址。
  • 设备可以通过这个IP地址在局域网内通信,或者通过路由器访问互联网。
复制代码
void wifiConnection()
{
    M5.Display.println("Connecting to WiFi");
// 断开所有ESP32连接的wifi WiFi.disconnect();
// 断开所有连接ESP32 wifi的客户端 WiFi.softAPdisconnect(
true);
// 将ESP32设定为STA模式,也就是ESP32去连接别的wifi WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) { M5.Display.print("."); delay(100); } M5.Display.println("\n\nconnected\n");
// 打印ESP32的IP地址 M5.Display.print(WiFi.localIP()); M5.Display.print(
"\n\n"); }
复制代码

6.2 使用HTTP协议

HTTP客户端例子:

复制代码
#define WIFI_SSID "ABC"
#define WIFI_PASS "123456"

// 这个头文件,必须放在M5Unified之前
#include <HTTPClient.h>
#include <M5Unified.h>

// 将本机,ESP32,设定为HTTP客户端
// HTTP客户端,相当于一个网页浏览器
HTTPClient http;

void setup()
{
  auto cfg = M5.config();
  M5.begin(cfg);

  M5.Display.println("Connecting to WiFi");
  WiFi.disconnect();
  WiFi.softAPdisconnect(true);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  while (WiFi.status() != WL_CONNECTED)
  {
    M5.Display.print(".");
    delay(100);
  }

  M5.Display.println("\n\nconnected\n");
  M5.Display.print(WiFi.localIP());
  M5.Display.print("\n\n");

  // 指定URL
  http.begin("https://www.baidu.com/");

  // 发起GET请求
  int httpResponseCode = http.GET();

  // 如果请求成功
  if (httpResponseCode > 0)
  {
    String payload = http.getString();
    M5.Display.println("HTTP Response code: " + String(httpResponseCode));
    M5.Display.println("Response payload: " + payload);
  }
  else
  {
    M5.Display.println("Error on HTTP request");
  }

  // 结束HTTP请求
  http.end();
}

void loop()
{
  delay(100);
}
复制代码

HTTP服务器端例子:

复制代码
#define WIFI_SSID "ABC"
#define WIFI_PASS "123456"

// 必须放在M5Unified之前
#include <ESPAsyncWebServer.h>
#include <M5Unified.h>

// 创建一个HTTP服务器,并监听本机80端口
AsyncWebServer server(80);

// 一个网页
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP32 Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1>ESP32 Web Server</h1>
  <p>GET request received!</p>
</body>
</html>)rawliteral";

void setup()
{
  auto cfg = M5.config();
  M5.begin(cfg);

  M5.Display.println("Connecting to WiFi");
  WiFi.disconnect();
  WiFi.softAPdisconnect(true);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  while (WiFi.status() != WL_CONNECTED)
  {
    M5.Display.print(".");
    delay(100);
  }

  M5.Display.println("\n\nconnected\n");
  M5.Display.print(WiFi.localIP());
  M5.Display.print("\n\n");

  // 如果客户端使用GET请求,请求本机ROOT时,返回一个网页
  // 在浏览器中输入:http://ESP32的ip
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
            { request->send_P(200, "text/html", index_html); });

  // 如果客户端使用POST请求,请求本机/post时,返回一段JSON数据
  // 浏览器无法直接发送POST请求,需要使用一些插件比如POST MAN
  server.on("/post", HTTP_GET, [](AsyncWebServerRequest *request)
            {
    String response = "{\"message\":\"POST request received\"}";
    request->send(200, "application/json", response); });

  // 以异步的方式,启动HTTP服务器
  server.begin();
}

void loop()
{
  delay(100);
}
复制代码

7 小程序5:To-do List

7.1 功能分析

数据结构

  • 内容:表示待办事项的具体内容,如“Go Shopping”。
  • 日期:表示待办事项的截止日期或创建日期,如“6.15”。

数据存储

  • 存储格式:以TXT文本形式保存在SD卡上。
  • 数据分隔符:使用特定的分隔符来分隔内容和日期。例如使用&表示文件结尾,使用$表示日分隔,使用^表示内容和日期。
  • Hiking^8.14$Dinner with PXQ^7.12$Go Shopping^8.14$&

ESP32 服务器功能

  • 首次启动:

    • 从SD卡上读取存储的待办事项数据。
    • 将数据加载到内存中以便快速访问和修改。
  • 增、删、改、查操作:

    • 增加:通过浏览器发送GET请求,包含新增的待办事项内容和日期。服务器处理请求后,将新数据写入SD卡。
    • 删除:通过浏览器发送GET请求,包含要删除的待办事项的标识(如索引或内容)。服务器处理请求后,从内存和SD卡中删除相应数据。
    • 修改:通过浏览器发送GET请求,包含要修改的待办事项标识及新的内容或日期。服务器处理请求后,更新内存和SD卡中的数据。
    • 查询:通过浏览器发送GET请求,包含要查询的关键字,也可以没有。服务器处理请求后,返回JSON数据。
  • 数据同步:

    • 每次增、删、改操作后,服务器都会将最新的数据写回SD卡,确保数据持久性。

浏览器与服务器交互

  • 请求方法:使用GET方法向ESP32服务器发送请求。

  • 请求类型:

    • 查询(查):请求当前所有待办事项数据。
    • 增加(增):请求增加新的待办事项。
    • 删除(删):请求删除指定的待办事项。
    • 修改(改):请求修改指定的待办事项。
  • 数据返回:

    • 服务器将处理后的待办事项数据以JSON格式返回给浏览器。
    • JSON数据包含所有当前的待办事项,每个待办事项包含内容和日期两个字段。

浏览器后期处理

  • 数据展示:将服务器返回的JSON数据解析并展示给用户。
  • 用户操作:
    • 用户可以通过浏览器界面添加新的待办事项、删除已有事项或修改事项内容。
    • 每次用户操作都通过GET请求与服务器交互,服务器处理后返回最新的数据。

总结

该系统通过ESP32服务器和浏览器之间的GET请求,实现了一个简单的基于SD卡存储的待办事项管理功能。数据以特定分隔符的TXT格式存储在SD卡上,服务器负责数据的读取和写入操作,并通过JSON格式与浏览器交互,从而实现增、删、改、查等功能。

7.2 伪代码

  • 开机后:
    • 从SD卡读取数据文件
    • 连接WiFi
    • 启动HTTP服务器
  • HTTP服务器
    • GET /Create 为增加数据操作,创建对应的回调函数,并启动异步监听
    • GET /Delete 为删除数据操作,创建对应的回调函数,并启动异步监听
    • GET /Update 为修改数据操作,创建对应的回调函数,并启动异步监听
    • GET /Search 为查找数据操作,创建对应的回调函数,并启动异步监听
  • SD卡服务
    • 从SD卡读取数据文件,保存至todoList动态数组中:vector<TodoList>
    • 从SD卡将todoList动态数组的数据保存至数据文件中
    • 其他字符串辅助函数

7.3 功能实现

复制代码
// htmlhelper.hpp
#include <HTTPClient.h>
#include <ESPAsyncWebServer.h>
#include <M5Unified.h>

#define WIFI_SSID "PangXingQing"
#define WIFI_PASS "302503302503"

AsyncWebServer server(80);

void wifiConnection()
{
    M5.Display.println("Connecting to WiFi");
    WiFi.disconnect();
    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASS);

    while (WiFi.status() != WL_CONNECTED)
    {
        M5.Display.print(".");
        delay(100);
    }

    M5.Display.println("\n\nconnected\n");
    M5.Display.print(WiFi.localIP());
    M5.Display.print("\n\n");
}

// CDUR
// Create、Delete、Update、Search
void requestCreateTodoList(AsyncWebServerRequest *request)
{
    // Create
    auto newItem = todoList();
    newItem.id = generateUUID();
    newItem.item = request->arg("item");
    newItem.time = request->arg("time");
    todoLists.push_back(newItem);
    writeListToFile();

    String response = "";
    for (int i = 0; i < todoLists.size(); i++)
    {
        auto todoListItem = todoLists.at(i);
        response +=
            "{\"id\": \"" + todoListItem.id + "\"," +
            "\"item\": \"" + todoListItem.item + "\"," +
            "\"time\": \"" + todoListItem.time + "\"},";
    }

    response.remove(response.length() - 1);
    response = "{\"todolist\":[" + response + "]}";
    request->send(200, "application/json", response);
}

void requestDeleteTodoList(AsyncWebServerRequest *request)
{
    auto deleteID = request->arg("id");
    auto deleteIndex = -1;

    String response = "";
    for (int i = 0; i < todoLists.size(); i++)
    {
        if (todoLists[i].id.equals(deleteID))
        {
            deleteIndex = i;
        }
        else
        {
            response +=
                "{\"id\": \"" + todoLists[i].id + "\"," +
                "\"item\": \"" + todoLists[i].item + "\"," +
                "\"time\": \"" + todoLists[i].time + "\"},";
        }
    }

    // Delete
    if (deleteIndex != -1)
    {
        todoLists.erase(todoLists.begin() + deleteIndex);
        writeListToFile();
    }

    response.remove(response.length() - 1);
    response = "{\"todolist\":[" + response + "]}";
    request->send(200, "application/json", response);
}

void requestUpdateTodoList(AsyncWebServerRequest *request)
{
    auto id = request->arg("id");
    auto item = request->arg("item");
    auto time = request->arg("time");

    String response = "";
    for (int i = 0; i < todoLists.size(); i++)
    {
        if (todoLists[i].id.equals(id))
        {
            todoLists[i].item = item;
            todoLists[i].time = time;
        }

        response +=
            "{\"id\": \"" + todoLists[i].id + "\"," +
            "\"item\": \"" + todoLists[i].item + "\"," +
            "\"time\": \"" + todoLists[i].time + "\"},";
    }

    // Update anyway
    writeListToFile();

    response.remove(response.length() - 1);
    response = "{\"todolist\":[" + response + "]}";
    request->send(200, "application/json", response);
}

void requestSearchTodoList(AsyncWebServerRequest *request)
{
    auto keyword = request->arg("kw");

    String response = "";
    for (int i = 0; i < todoLists.size(); i++)
    {
        auto todoListItem = todoLists.at(i);
        if (todoListItem.item.indexOf(keyword) != -1)
            response +=
                "{\"id\": \"" + todoListItem.id + "\"," +
                "\"item\": \"" + todoListItem.item + "\"," +
                "\"time\": \"" + todoListItem.time + "\"},";
    }

    response.remove(response.length() - 1);
    response = "{\"todolist\":[" + response + "]}";
    request->send(200, "application/json", response);
}

void startWebServices()
{
    // CDUR
    // Create、Delete、Update、Search
    server.on("/Create", HTTP_GET, requestCreateTodoList);
    server.on("/Delete", HTTP_GET, requestDeleteTodoList);
    server.on("/Update", HTTP_GET, requestUpdateTodoList);
    server.on("/Search", HTTP_GET, requestSearchTodoList);

    // Start server
    server.begin();
}
复制代码
复制代码
// sdhelper.hpp
#include <SD.h>
#include <vector>
#include <M5Unified.h>

#define ToDoListFilePath "/todolist.txt"

struct todoList
{
    String id;
    String item;
    String time;
};

std::vector<todoList> todoLists;

// 生成UUID的函数
String generateUUID()
{
    String uuid = "";
    const char *hexChars = "0123456789ABCDEF";

    for (int i = 0; i < 32; i++)
    {
        // 生成随机数并转换为十六进制字符
        uuid += hexChars[random(0, 16)];

        // 添加分隔符
        if (i == 7 || i == 11 || i == 15 || i == 19)
        {
            uuid += '-';
        }
    }

    return uuid;
}

void splitString(String data, char delimiter)
{
    int start = 0;
    int end = data.indexOf(delimiter);

    while (end != -1)
    {
        String part = data.substring(start, end);

        int timeStart = part.indexOf('^');

        todoList newItem = todoList();
        newItem.id = generateUUID();
        newItem.item = part.substring(0, timeStart);
        newItem.time = part.substring(timeStart + 1);

        todoLists.push_back(newItem);

        start = end + 1;
        end = data.indexOf(delimiter, start);
    }
}

void readLisFromFile()
{
    File file = SD.open(ToDoListFilePath);
    if (!file)
    {
        M5.Display.println("Failed to open file!");
        return;
    }
    auto allText = file.readStringUntil('&');
    file.close();

    if (allText.length() != 0)
    {
        splitString(allText, '$');
    }
    else
    {
        todoLists.clear();
    }
}

void writeListToFile()
{
    File file = SD.open(ToDoListFilePath, FILE_WRITE);
    if (!file)
    {
        M5.Display.println("Failed to open file!");
        return;
    }

    String text = "";
    for (int i = 0; i < todoLists.size(); i++)
    {
        auto item = todoLists.at(i);
        text += item.item + "^" + item.time + "$";
    }
    text = text + "&";
    file.println(text);
    file.close();
}
复制代码
复制代码
// main.cpp
#include "sdhelper.hpp"
#include "htmlhelper.hpp"
#include <M5Unified.h>

void setup()
{
    auto cfg = M5.config();
    M5.begin(cfg);

    SD.begin(GPIO_NUM_4, SPI, 25000000);

    readLisFromFile();
    wifiConnection();

    startWebServices();
}

void loop()
{
}
复制代码

 

7.4 小练习:完成修改功能

8

posted @   庞兴庆  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示