本篇文章的项目是课程作业,由于以前没接触过此类东西,故记录一下开发过程

一、准备

首先是freeRTOS,freeRTOS简单的来说,就是一个很小的可以实现多任务的操作系统,多任务的完成是靠中断实现的,所以也可以把freeRTOS当成一个能完成中断小系统(纯属个人理解,不喜勿喷)。然后是esp32,我所使用的是老师发的esp32,其中一个串口已经被改过了,如图

pc端我就使用的笔记本电脑了。

二、思路+做法

首先我要实现的是,在pc端通过一个页面去控制esp32上的小灯闪烁情况。

那么问题就是:1、页面如何写;2、怎么去控制;3、如何优化

对于问题1,我选择python,用pyqt简单写一个页面就行,毕竟主要问题就是如何控制和优化。

对于问题二,无线通讯主要是有蓝牙和WiFi,这里采用wifi通讯,WiFi通讯里分TCP\UDP,具体又分三种方式,分别是AP  STA  AP+STA。

TCP(Transmission Control Protocol,传输控制协议)这种是更安全的一种连接方式,就是传输端(服务端)和接收端(客户端)必须建立可靠的联系后才能收发信息。

UDP(User Data Protocol,用户数据报协议)相比较就没有那么安全,但是更方便,不需要建立稳固的链接,传输端把要传输的信息扔上去,那个接收端想要,自己接收就行。

AP模式是自身作为热点,“散发出”wifi热点;STA是连接外部的WiFi型号;AP+STA就很好理解了,两者混合。

我这儿就用的UDP+AP了。

对于问题三,如何优化其实就是为什么要用freeRTOS的原因了。因为在我的要求中,esp32灯的闪烁情况是会经常变的,且变化的时间、节点也是不固定的,且如何实现改变灯闪烁和接收pc端发出的信息并做出相应的变化,这都是需要使用到多任务的,因此选择采用freeRTOS.

三、具体实现

首先,是编一个小页面,要求就是通过点击不同的模式,使esp32变化不同的闪烁方式,然后能输出一些错误提示信息。方便起见就用pyqt简单的写了一个,这是页面和代码。

​
import sys
import socket
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QLabel
from esp32_udp import Ui_Form

class ESP32Controller(QWidget, Ui_Form):
    def __init__(self, esp32_ip, esp32_port):
        super().__init__()
        self.esp32_ip = esp32_ip
        self.esp32_port = esp32_port

        self.setupUi(self)
        self.modle1.clicked.connect(self.modle_1)
        self.modle2.clicked.connect(self.modle_2)
        self.modle3.clicked.connect(self.modle_3)
        self.connect_off.clicked.connect(self.colse_connect)

    client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    def send_command(self, command):
        self.client_socket.settimeout(10)  # 设置超时时间10秒
        try:
            self.client_socket.sendto(command.encode('utf-8'), (self.esp32_ip, self.esp32_port))
            response, _ = self.client_socket.recvfrom(1024)
            response_text = response.decode('utf-8')
            print("Received:", response_text)
            self.textshow_lable.setText(f'Status: {response_text}')
        except socket.timeout:
            print("超时")
            self.textshow_lable.setText('超时')
        finally:
            self.client_socket.close()

    def modle_1(self):
        self.send_command('modle1')
        self.textshow_lable.setText("成功设置为模式1")
    def modle_2(self):
        self.send_command('modle2')
        self.textshow_lable.setText("成功设置为模式2")

    def modle_3(self):
        self.send_command('modle3')
        self.textshow_lable.setText("成功设置为模式3")

    def colse_connect(self):
        self.client_socket.close()
        self.textshow_lable.setText("关闭连接")

if __name__ == '__main__':
    esp32_ip = "192.168.9.1"  # 替换为你的ESP32 IP
    esp32_port = 1234  # 替换为你的ESP32 UDP端口
    app = QApplication(sys.argv)
    ex = ESP32Controller(esp32_ip, esp32_port)
    ex.show()
    sys.exit(app.exec_())

​

代码中需要注意的一个点就是

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM),这句代码的意思是创建一个套接字,用于收发信息。

套接字简单来说就是一个节点,网络世界里,每个计算机或者用户都有大量的信息需要收发,那如何在网络世界做到你发我收呢,就是通过套接字,就类似于你家窗台,有快递员来了,你只会让他把快递放在窗台,而不是直接进家里。而套接字就是你的操作系统提供的一个“窗台”,其他用户或者计算机内的其他程序想交流数据,都可以通过这个“窗台”。其中的参数就不过多阐述了,可以自行查阅。

其中的流程大概分为以下几步:

1、客户端创建套接字

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

2、设定指定的ip和端口,将信息发送到该ip和端口

server_address = ('localhost', 12345)

message = b'Hello, UDP server!'

client_socket.sendto(message, server_address)

3、服务器创建相应的套接字

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

4、绑定到指定端口

server_socket.bind(('localhost', 12345)) 

5、接收信息

data, server = client_socket.recvfrom(4096)   #4096是创建一个该大小的空间用于储存接收的信息

以上代码都是基于python语言,c也可以,只不过库不太一样。

以上是pc端的,接下来就是esp32 ,开发esp32我选择的vscode上的platfromIO,当然也可以用Arduino,但是Arduino在代码可读性上真的难受,所以还是建议用platformIO。

freeRTOS的移植也很简单,在项目根目录下新建一个freertos文件夹,将source文件夹下的源码复制过来,如果内存紧张的话,可以在portable文件下只留以下三个文件夹,如下图。

但是我亲身实验直接在代码里引用TaskHandle_t、vTaskResume等函数的也是可以的,不用专门去移植freeRTOS,就很奇怪,难道是platformIO里有内置?anyway,接下来就进入代码。以下是整个的完整代码。

#include <WiFi.h>
#include <WiFiUdp.h>
#include <Arduino.h>
#include <AsyncUDP.h>

const char* ssid = "esp32_udp";
const char* password = "123456789";
IPAddress IP_local(192, 168, 9, 1);     // 本机IP
IPAddress IP_gateway(192, 168, 9, 1);   // 网关
IPAddress IP_mask(255, 255, 255, 0);    // 掩码
IPAddress IP_start(192,168,9,10);
const unsigned int localUdpPort = 1234; // 本地UDP端口

WiFiUDP udp;
TaskHandle_t receive_task;
TaskHandle_t Task1;
TaskHandle_t Task2;
TaskHandle_t Task3;
char text[255];  // 缓存接收的数据

void receive(void* pvpoint){
    udp.begin(localUdpPort);
    Serial.printf("UDP server started at port %d\n", localUdpPort);
    while (!WiFi.isConnected()) {
    int packetSize = udp.parsePacket();
    if (packetSize) {
        int len = udp.read(text, 255);
        if (len > 0) {
            text[len] = 0;
            }
            Serial.printf("Received packet of size %d from %s:%d\n", packetSize,
                          udp.remoteIP().toString().c_str(), udp.remotePort());
            Serial.printf("Packet content: %s\n", text);
        }
    }
}

void modle1(void* pvpoint){
    if (strcmp(text, "模式1") == 0) {
        vTaskResume(Task1);
        vTaskSuspend(Task2);
        vTaskSuspend(Task3);
        digitalWrite(22, HIGH);  // turn the LED on (HIGH is the voltage level)
        delay(1000);             // wait for a second
        digitalWrite(22, LOW);   // turn the LED off by making the voltage LOW
        delay(1000);   
        udp.beginPacket(udp.remoteIP(), udp.remotePort());
        udp.print("模式1");
        udp.endPacket();
        }
}

void modle2(void* pvpoint){
    if (strcmp(text, "模式2") == 0) {
        vTaskResume(Task2);
        vTaskSuspend(Task1);
        vTaskSuspend(Task3);
        digitalWrite(22, HIGH);  // turn the LED on (HIGH is the voltage level)
        delay(100);             // wait for a second
        digitalWrite(22, LOW);   // turn the LED off by making the voltage LOW
        delay(100);     // 关闭LED
        udp.beginPacket(udp.remoteIP(), udp.remotePort());
        udp.print("模式2");
        udp.endPacket();
        }
}

void modle3(void* pvpoint){
    if (strcmp(text, "模式3") == 0) {
        vTaskResume(Task3);
        vTaskSuspend(Task1);
        vTaskSuspend(Task2);
        digitalWrite(22, LOW);  // 关闭LED
        udp.beginPacket(udp.remoteIP(), udp.remotePort());
        udp.print("模式3");
        udp.endPacket();
        }
}

void setup() {
    Serial.begin(115200);
    WiFi.mode(WIFI_AP);
    WiFi.softAPConfig(IP_local, IP_gateway, IP_mask);
    WiFi.softAP(ssid, password);
    pinMode(22, OUTPUT);
    digitalWrite(22, HIGH);  // 初始化LED状态为开启
    Serial.println("WiFi AP Started");
    Serial.print("IP Address: ");
    Serial.println(WiFi.softAPIP());
// 创建任务
    xTaskCreate(receive,"receive",4096,NULL,1,&receive_task);
    xTaskCreate(modle1,"modle1",4096,NULL,2,&Task1);
    xTaskCreate(modle2,"modle2",4096,NULL,2,&Task2);
    xTaskCreate(modle3,"modle3",4096,NULL,2,&Task3);
}

void loop() {
    // 主循环不需要做任何事情,任务在FreeRTOS中处理
}

其中我是用了消息队列,流程大概就是

1、创建一个句柄:TaskHandle_t Task1;//Task1是任务一对应的句柄

2、写任务一函数;

3、创建任务一:xTaskCreate(modle1,"modle1",4096,NULL,2,&Task1); //这儿的创建任务其实是在内存中创建一个空白窗口,其中modle1是具体的函数(任务一)的函数名,“modle”是想指定的名字(一般都是和函数名一样便于检查),4096是分配多少内存大小给这个任务,2代表任务优先级,&Task1是将这个空白窗口和你的句柄进行连接。

注:其实我在最开始很不理解为什么要用句柄,直接用指针不好吗。后面了解到,句柄是内存中的一部分内容的唯一指定,更像是一种“把柄”,只要抓住了这部分内容的把柄,不关内容怎么变,你都能找到它,并且不像指针指代内容那么“露骨”,更间接。所以在操作系统层面,句柄的使用能更安全,毕竟用指针的话,使用不当很容易造成系统崩溃。

这就是大概的流程,代码中还使用了vTaskResume、vTaskSuspend,这两个函数很简单,就是挂起和恢复某个任务的工作。

vTaskResume(Task3);

vTaskSuspend(Task1);

vTaskSuspend(Task2);

就代表着task3工作,2、3任务挂起。

在整段代码中,我将接收客户端发出的信息单独作为了一个任务,并将任务优先级设为最优先,然后将接收的信息与modle1、modle2、modle3分别进行了比较,然后决定进行那个任务。这样很麻烦,按道理来说写一个比较循环函数就行,但是博主没成功。。。。。。

四、总结

总的来说这个东西做的不咋行,特别是platformIO上的代码,c代码真的没python好写:),并且运行速度很慢啊,pc端的页面点击次数多了会卡死(难不成pc端的页面也需要多线程吗,但是也没几个任务啊),但是能基本完成最初的设想,也通过这个过程学习了很多,代码能力和学习能力得到的进一步的锻炼。如果有时间的话,后续还会继续改进。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐