云计算、AI、云原生、大数据等一站式技术学习平台

网站首页 > 教程文章 正文

牛批!用Python远程控制交通信号灯

jxf315 2024-12-17 14:08:13 教程文章 43 ℃

图片来自 Pexels

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

这个案例是将 Python、PyQT6 与 Arduino 结合。通过 Arduino 开发板控制 3 个 LED(分别为红黄绿 3 个颜色)来模拟交通信号灯。

可以通过单击 PC 端的三个灯控制 Arduino 开发板和 3 个 LED。也可以点击“自动”按钮让信号灯自动变换。

本系统完全模拟真实的信号灯的自动切换过程。一开始红灯亮 6 秒(为了减少一个完整变化周期的时间,并没有让信号灯亮过长时间),然后立刻切换到绿灯,继续亮 6 秒。

接下来绿灯闪烁 6 次(每 1 秒闪烁一次,一亮一灭),然后切换到黄灯,亮 3 秒,最后到红灯(亮 6 秒),完成一个信号周期。如果点击“停止”按钮,信号灯会停止自动切换,但会在完成一个信号周期后停止。视频演示请点击“阅读原文”见附件。

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

本实验需要准备的设备如下:

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

02与 Arduino 开发板连接

玩物联网,其实涉及到硬件和软件两部分。硬件主要涉及到选择和连接,一般并不涉及到硬件的设计和制作。

本实验的核心模块是 ESP8266,这是一个 Wi-Fi 模块,价格非常便宜,国内价格在 15 到 20 元之间,某宝就有卖。

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

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

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

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

需要连接的 5 个管脚如下:

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

03连接 ESP8266 与 Arduino 开发板

在正式连接之前,最好先用 Fritzing 模拟连接下,相当于画个草图,以免接错。

连接完成的效果如图 3 所示:

这里要说明的是,EN 与 3.3v 都需要接到 Arduino 开发板的 3.3v 管脚上。

但 Arduino 开发板只有一个 3.3v 管脚,所以首先将 Arduino 开发板的 3.3v 管脚通过杜邦线(图 3 中红色的线)连接到面包板,然后 EN 与 3.3v 再通过杜邦线插入面包板中(两根橙色的线)。

要记住,其中一根橙色的线要与红色线在同一列,另外一根橙色的线在另一列,并让这两列用给一个 10K 的电阻相连,为了不让 ESP8266 的 EN 和 3.3v 两个管脚由于直接连通而短路。

注意,一定要用电阻相连,推荐是 10K 欧的电阻。

04连接 3 个 LED

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

短一些(左侧)的管脚接地,长一些(右侧)的管脚接某个数字管脚,本例绿、黄、红分别接在了 7、6、5 管脚。

由于 Arduino 开发板只有 3 个 GND 管脚,而 ESP8266 已经占用了一个,所以仍然需要借助面包板扩展 GND 管脚。

基本原理是将 GND 通过杜邦线与面包板连接(本例中的黑线),然后将 3 个 LED 的 GND 端都通过杜邦线插入与黑色杜邦线在面包板位置的同一列的其他插孔。

最终的效果如图 5 所示:

05创建 TCP 服务器

ESP8266 与 Arduino 交互通常有如下 2 种方式:

  • ESP8266 作为服务器
  • 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 服务。

06编写 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


# 线程类,用于自动切换信号灯
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

最近发表
标签列表