【Arduino实验室】中关于智能硬件的实验在网上绝对是没有的(有也是我发的),都由作者单独设计。敬请期待后期的【鸿蒙实验室】系列文章和视频课程。

 

这个案例是将Python、PyQT6与Arduino结合。通过Arduino开发板控制3个LED(分别为红黄绿3个颜色)来模拟交通信号灯。可以通过单击PC端的三个灯控制Arduino开发板和3个LED。也可以点击“自动”按钮让信号灯自动变换。本系统完全模拟真实的信号灯的自动切换过程。一开始红灯亮6秒(为了减少一个完整变化周期的时间,并没有让信号灯亮过长时间),然后立刻切换到绿灯,继续亮6秒。接下来绿灯闪烁6次(每1秒闪烁一次,一亮一灭),然后切换到黄灯,亮3秒,最后到红灯(亮6秒),完成一个信号周期。如果点击“停止”按钮,信号灯会停止自动切换,但会在完成一个信号周期后停止。视频演示见附件。

 

1. 需要准备哪些实验设备和器材


本实验需要准备的设备如下:
(1)PC一台,系统可以是Windows、macOS或Linux,需要安装Python环境和PyQt6、以及Arduino IDE;
(2)Arduino开发板一块,推荐使用UNO;
(3)3个LED,建议红、黄、绿各一个;
(4)ESP8266 Wi-Fi模块一个,用于联网;
(5)10K电阻一个;
(6)面包板一个,主要用于解决Arduino开发板接口不足的情况;
(7)杜邦线若干,可以多准备一些(最好有多种颜色),反正很便宜。需要两类杜邦线:公对公、公对母;

 

2. ESP8266 Wi-Fi模块与Arduino开发板连接


玩物联网,其实涉及到硬件和软件两部分。硬件主要涉及到选择和连接,一般并不涉及到硬件的设计和制作。本实验的核心模块是ESP8266,这是一个Wi-Fi模块,价格非常便宜,国内价格在15到20元之间,某宝就有卖。


要做的第一步就是将ESP8266与Arduino开发板用杜邦线连接,程序是上传到Arduino开发板上的,然后通过AT命令与ESP8266模块交互。


ESP8266的样子如图1所示。这个模块相对其他大多数模块(如超声波模块、LED、按钮等)要复杂一些,一共有8个管脚,也就是8个针。

【Arduino实验室】NB的玩法,远程控制交通信号灯-ESP8266联网

ESP8288一般与Arduino开发板直接相连。Arduino开发板的样子如图2所示。管脚都是眼,所以需要若干公对母的杜邦线。

【Arduino实验室】NB的玩法,远程控制交通信号灯-ESP8266联网

现在回到图1的8个管脚,其实在开发阶段,只需要连接其中的5个即可,在刷固件时,一般需要连接6个管脚(关于刷ESP8266固件的问题,我后面会写文章介绍)。需要连接的5个管脚如下:


(1)3.3v:接到Arduino开发板的3.3v插孔上,记住,一定是3.3v,不要接在5v上,否则你还需要再买一块ESP8266,切记、切记、切记;
(2)EN:同样需要接到3.3v上,但Arduino开发板只有一个3.3v插孔,所以需要借助面包板;
(3)GND:接在Arduino开发板的GND插孔(一般有3个,插如任意一个GND插孔即可);
(4)TX:通常接在一个软串口,本例接到8上;
(5)RX:通常接在一个软串口,本例接到9上;

 

3. 模拟连接ESP8266与Arduino开发板


在正式连接之前,最好先用Fritzing模拟连接下,相当于画个草图,以免接错。连接完成的效果如图3所示。
【Arduino实验室】NB的玩法,远程控制交通信号灯-ESP8266联网这里要说明的是,EN与3.3v都需要接到Arduino开发板的3.3v管脚上,但Arduino开发板只有一个3.3v管脚,所以首先将Arduino开发板的3.3v管脚通过杜邦线(图3中红色的线)连接到面包板,然后EN与3.3v再通过杜邦线插入面包板中(两根橙色的线),要记住,其中一根橙色的线要与红色线在同一列,另外一根橙色的线在另一列,并让这两列用给一个10K的电阻相连,为了不让ESP8266的EN和3.3v两个管脚由于直接连通而短路。注意,一定要用电阻相连,推荐是10K欧的电阻。

4. 连接3个LED

 

LED的连接就简单的多,LED如图4所示。

【Arduino实验室】NB的玩法,远程控制交通信号灯-ESP8266联网


短一些(左侧)的管脚接地,长一些(右侧)的管脚接某个数字管脚,本例绿、黄、红分别接在了7、6、5管脚。由于Arduino开发板只有3个GND管脚,而ESP8266已经占用了一个,所以仍然需要借助面包板扩展GND管脚。基本原理是将GND通过杜邦线与面包板连接(本例中的黑线),然后将3个LED的GND端都通过杜邦线插入与黑色杜邦线在面包板位置的同一列的其他插孔。最终的效果如图5所示。
【Arduino实验室】NB的玩法,远程控制交通信号灯-ESP8266联网
5. 在Arduino开发板上创建TCP服务器


ESP8266与Arduino交互通常有如下2种方式:
(1)ESP8266作为服务器;
(2)ESP8266作为客户端;


本实验采用了第1种方式,第2种方式后面我会写文章介绍。


不管采用哪一种方式,首先要让ESP8266上网。通常是连接家中的无线路由器,或用手机做的热点。设置ESP8266需要通过AT命令,其实就是一组解释执行的命令,与DOS命令类似。


ESP8266在出厂时的波特率是115200,所以执行AT命令,必须在这个波特率下。在setup函数中使用下面的代码设置波特率,以及执行AT命令连接路由器。

#include <SoftwareSerial.h>
SoftwareSerial wifi(WIFI_RX, WIFI_TX);   //RX, TX
void setup() {
  Serial.begin(9600);
  wifi.begin(115200);
  Serial.println("system is ready!");
  wifi.println("AT+CWMODE=3\r\n");                             // 设置ESP8266的模式,3表示既可以作为路由器模式(AP),为其他设备提供用于上网的Wi-Fi,也可以作为普通的设备建立TCP连接
  delay(500);
  wifi.println("AT+CIPMUX=1\r\n");
  delay(500);
wifi.println("AT+CWJAP=\"路由器名\",\"路由器密码\"\r\n");       //连接路由器,请更换自己的路由器明和路由器密码
 delay(500);
 wifi.println("AT+CIPSERVER=1,5000\r\n");                      // 启动TCP服务,端口号是5000
 delay(500);
}

这里的wifi负责与软串口通信(通常硬串口主要用于刷固件),wifi.println函数用于执行AT命令。要注意,每执行一条AT命令,要等待一定的时间,这里是500毫秒。


当第一遍执行完,除非Arduino开发板重启或重新上传程序,否则setup函数只会执行一次。以后可以将连接路由器的代码去掉了,因为ESP8266是有记忆功能的,这种配置性质的AT命令,执行完,会将执行结果记录在案。所以下一次重启ESP8266模块时,不管执行不执行这条AT命令,ESP8266都会自动连接路由器。


不过其他代码应该保留,因为这些代码的执行结果是不会记录在案的,当重启ESP8266模块时,需要重新执行这些AT命令来建立TCP服务。
经过测试,ESP8266在115200波特率的情况下,通过Wi-Fi传输数据容易出现乱码,所以需要使用下面的代码将ESP8266模块的波特率强行改成9600,这样数据传输非常稳定。

wifi.println("AT+CIOBAUD=9600\r\n");

在Arduino开发板上建立TCP服务器的完整代码如下:

#include <SoftwareSerial.h>

#define WIFI_TX       9
#define WIFI_RX       8
#define LED_RED       7
#define LED_YELLOW    6
#define LED_GREEN     5
SoftwareSerial wifi(WIFI_RX, WIFI_TX);   //RX, TX

String command = "";             //  接收客户端发过来的数据

void setup() {
  //5、6、7三个管脚设置为输出,以便输出高电平来点亮LED
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_YELLOW, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  // 先将5、6、7三个管脚设置为低电平,默认LED是灭的状态
  digitalWrite(LED_RED, LOW);
  digitalWrite(LED_YELLOW, LOW);
  digitalWrite(LED_GREEN, LOW);

  Serial.begin(9600);
  wifi.begin(9600);     // 已经改成9600了,所以这里通过9600波特率与客户端通过Wi-FI传输维护局
  Serial.println("system is ready!");

  wifi.println("AT+CWMODE=3\r\n");
  delay(500);
  wifi.println("AT+CIPMUX=1\r\n");
  delay(500);
  wifi.println("AT+CIPSERVER=1,4999\r\n");
  delay(500);
}
// 该函数会不断循环调用
void loop() {
  // 从客户端(PC端)读取发过来的数据  
  while (wifi.available() > 0) {
    command += char(wifi.read());   
    delay(4);
  }
  // 如果数据不为空,继续处理
  if (command != "") {
    // 将接收到的命令输出到串口监视器  
    Serial.println(command);
    // 命令会自动加上一个前缀+IPD,如果包含这个前缀,才是传过来的命令
    if (command.indexOf("+IPD") > -1)
    {
      if (command.indexOf("close_red") > -1)
      {
        digitalWrite(LED_RED, LOW); //0 灯灭
        Serial.println("close_red");
      }
      else if (command.indexOf("close_yellow") > -1)
      {
        digitalWrite(LED_YELLOW, LOW); //0 灯灭
        Serial.println("close_yellow");     
      }
      else if (command.indexOf("close_green") > -1)
      {
        digitalWrite(LED_GREEN, LOW); //0 灯灭
        Serial.println("close_green");
      }
      else if (command.indexOf("open_red") > -1)
      {
        digitalWrite(LED_RED, HIGH);   //1 灯亮
        Serial.println("open_red");    
      }
      else if (command.indexOf("open_yellow") > -1)
      {
        digitalWrite(LED_YELLOW, HIGH);   //1 灯亮
        Serial.println("open_yellow");
      }
      else if (command.indexOf("open_green") > -1)
      {
        digitalWrite(LED_GREEN, HIGH);   //1 灯亮
        Serial.println("open");   
      }
    }
    command = "";
  }
}

 

阅读这段代码要注意,这里提供了6个命令:close_red(红色LED灭灯)、open_red(红色LED亮灯)、close_yellow(黄色LED灭灯)、open_yellow(黄色LED亮灯)、close_green(绿色LED灭灯)、open_green(绿色LED亮灯)。只要command中包含着6个命令字符串中的一个,就确定客户端发出了该命令。


然后使用Arduino IDE上传程序即可(别忘了选择开发板和端口)


PS:如果要改变端口号,可以直接修改5000,然后需要重启Arduino开发板(当然,ESP8266也会重启),这样就会再次执行setup函数来重新启动TCP服务。

 

6. 编写Python程序


这里编写客户端程序,使用PyQt6编写UI、使用Python编写全部业务逻辑,由于代码比较多,所以只给出了核心代码,基本原理就是Python通过TCP Socket API连接ESP8255中的TCP Server,然后不断发送上一节给出的6个命令。

import TrafficLight1
import sys
import socket
from PyQt6.QtWidgets import QApplication,QMainWindow,QMessageBox
from PyQt6 import  QtGui
from PyQt6.QtCore import QThread

"""
讲师:李宁(蒙娜丽宁)
微信:unitymarvel
微信公众号:极客起源
B站:https://space.bilibili.com/477001733
"""
# 线程类,用于自动切换信号灯
class WorkThread(QThread):

    def __init__(self, events):
        super(WorkThread, self).__init__()
        self.events = events
        self.running = False

    def run(self):
        # 先关闭所有的信号灯
        self.events.close_all_light()
        # 开始自动切换信号灯
        while self.running:
            # 红色信号灯打开6秒
            self.events.open_light("red")
            QThread.msleep(6000)
            self.events.close_light("red")
            QThread.msleep(200)
            # 绿色信号灯打开5秒
            self.events.open_light("green")
            QThread.msleep(6000)
            # 绿色信号灯闪烁5次
            count = 0
            while count < 5:
                self.events.close_light("green")
                QThread.msleep(500)
                self.events.open_light("green")
                QThread.msleep(500)
                count += 1
            self.events.close_light("green")
            QThread.msleep(200)
            # 黄色信号灯显示2秒
            self.events.open_light("yellow")
            QThread.msleep(3000)
            self.events.close_light("yellow")
            QThread.msleep(200)
        self.events.close_all_light()
        print("退出自动运行状态")


# 包含UI事件的代码
class Events:
    def __init__(self, ui):
        self.ui = ui
        self.connected = False   # 是否已经与Arduino建立了连接
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 声明协议类型,不写类型使用默认的
        self.workThread = WorkThread(self)
    def close_all_light(self):
        self.close_light("red")
        QThread.msleep(200)
        self.close_light("yellow")
        QThread.msleep(200)
        self.close_light("green")
        QThread.msleep(200)
    # 打开LED(color参数用于指定打开哪一个颜色的信号灯)    
    def open_light(self, color):
        if self.connected:
            self.client.sendall(("open_" + color).encode())
            if color == 'red':
                self.ui.labelRedLight.setPixmap(QtGui.QPixmap(""))
                self.ui.labelRedLight.state = "open"
            elif color == 'yellow':
                self.ui.labelYellowLight.setPixmap(QtGui.QPixmap(""))
                self.ui.labelYellowLight.state = "open"
            elif color == 'green':
                self.ui.labelGreenLight.setPixmap(QtGui.QPixmap(""))
                self.ui.labelGreenLight.state = "open"
            return True
        else:
            QMessageBox.warning(self.ui.centralwidget, "警告", "请先连接Arduino,再打开灯")
            return False
    # 关闭LED(color参数用于指定关闭哪一个颜色的信号灯)        
    def close_light(self, color):
        if self.connected:
            self.client.sendall(("close_" + color).encode())
            if color == 'red':
                self.ui.labelRedLight.setPixmap(QtGui.QPixmap("close_light.png"))
                self.ui.labelRedLight.state = "close"
            elif color == 'yellow':
                self.ui.labelYellowLight.setPixmap(QtGui.QPixmap("close_light.png"))
                self.ui.labelYellowLight.state = "close"
            elif color == 'green':
                self.ui.labelGreenLight.setPixmap(QtGui.QPixmap("close_light.png"))
                self.ui.labelGreenLight.state = "close"
            return True
        else:
            QMessageBox.warning(self.ui.centralwidget, "警告", "请先连接Arduino,再关闭灯")
            return False
    # 点击红灯时触发        
    def red_light_mouse_press_event(self, event):
        if ui.labelRedLight.state == "open":
            self.close_light("red")
        elif ui.labelRedLight.state == "close":
            self.open_light("red")
    # 点击黄灯时触发        
    def yellow_light_mouse_press_event(self, event):
        if ui.labelYellowLight.state == "open":
            self.close_light("yellow")
        elif ui.labelYellowLight.state == "close":
            self.open_light("yellow")
    # 点击绿灯时触发
    def green_light_mouse_press_event(self, event):
        if ui.labelGreenLight.state == "open":
            self.close_light("green")
        elif ui.labelGreenLight.state == "close":
            self.open_light("green")
    # 点击“连接”按钮时触发,用于链接TCP Server
    def pushButton_connect_mouse_press_event(self, event):
        self.client.connect(('192.168.31.164', 4999))
        self.connected = True
        self.ui.pushButtonConnect.setEnabled(False)

        QMessageBox.information(self.ui.centralwidget, "消息", "成功连接Arduino")
    # 点击“自动”按钮触发,用于开启信号灯自动切换模式
    def pushButton_auto_mouse_press_event(self, event):
        self.workThread.running = True
        self.workThread.start()
        self.ui.pushButtonAuto.setEnabled(False)
        self.ui.pushButtonStop.setEnabled(True)
    # 点击“停止”按钮触发,用于关闭信号灯自动切换模式
    def pushButton_stop_mouse_press_event(self, event):
        self.workThread.running = False
        self.ui.pushButtonAuto.setEnabled(True)
        self.ui.pushButtonStop.setEnabled(False)
# 用于初始化代码
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ui = TrafficLight1.Ui_MainWindow()
    mainWindow = QMainWindow()
    ui.setupUi(mainWindow)
    events = Events(ui)
    ui.labelRedLight.state = "close"
    ui.labelYellowLight.state = "close"
    ui.labelGreenLight.state = "close"
    ui.labelRedLight.mousePressEvent = events.red_light_mouse_press_event
    ui.labelYellowLight.mousePressEvent = events.yellow_light_mouse_press_event
    ui.labelGreenLight.mousePressEvent = events.green_light_mouse_press_event
    ui.pushButtonConnect.mousePressEvent = events.pushButton_connect_mouse_press_event
    ui.pushButtonAuto.mousePressEvent = events.pushButton_auto_mouse_press_event
    ui.pushButtonStop.mousePressEvent = events.pushButton_stop_mouse_press_event
    ui.pushButtonStop.setEnabled(False)
    # 将所有组件的文本尺寸都设置为30px
    mainWindow.setStyleSheet("QWidget{font-size:30px}");
    mainWindow.show()
    app.exec()


现在整个系统都完事了,好好地享受我们的成果吧!


文章相关附件可以点击下面的原文链接前往学习

原文链接:https://harmonyos.51cto.com/posts/3480#bkwz


想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方战略合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#bkwz