基于UDP协议的虚拟路灯

Python
310
0
0
2023-04-20
标签   网络协议

基于UDP实现的虚拟路灯

项目目标

​ 使用UDP通信协议,创建虚拟路灯。具备多个虚拟路灯的终端,一个UDP Server服务器,通过UDP通信协议将设备相连,并实现虚拟路灯上的数据向服务端的传输、以及服务端可对虚拟路灯终端设备进行远程控制灯的开关。

设计与实现

使用语言:Python

界面设计:Pyqt5、Pyqt5 Designer、Pyuic

客户端设计思路:

  • 使用Pyqt5 Designer工具先进行界面设计,这是一款图形化的工具,可以方便的得到我们想要的界面效果,并支持通过Ctrl+R进行实时预览,完成后有会生成一个UI文件,使用Pyuic工具即可以将UI文件转换成py文件
  • 定义函数生成随机数据,可以再客户端的界面进行展示
  • 在Pyqt5的界面代码中给按钮添加信号,点击则开始相关的功能函数
  • 定义工作函数,将生成的数据通过UDP的方式发送到Server
  • 由于在Pyqt5所运行的线程中无法使用,否则界面会卡死,需要引入多线程编程,将UDP接收函数在一个单独的线程上运行

服务端设计思路:

  • 使用Pyqt5 Designer工具先进行界面设计,这是一款图形化的工具,可以方便的得到我们想要的界面效果,并支持通过Ctrl+R进行实时预览,完成后有会生成一个UI文件,使用Pyuic工具即可以将UI文件转换成py文件
  • 定义Pyqt5对界面的展示信号,以及对按钮的信号设置
  • 发包函数,以用于对客户端的开关灯信号的发送
  • 由于在Pyqt5所运行的线程中无法使用,否则界面会卡死,需要引入多线程编程,将UDP接收函数在一个单独的线程上运行

测试与结果

客户端与三个服务端展示

img

点击服务端开始接收,客户端开始工作;再点击客户端工作按钮,客户端开始工作

img

分别点击服务端的停止按钮,服务端会停止工作

img

此时点击服务端的开机按钮,服务端会向所有的终端发出开灯指令

img

开灯指令在服务端接收后会返回一个数据包给服务端,只有在服务端收到这个客户端返回的数据包,证明传输成功,这样来实现可靠传输。

点击关灯按钮,所有设备关闭,但保留了接收远程信号的功能

img

总结与展望

​ 这是第一次对程序制作操作界面,从刚开始的磕磕绊绊,到逐渐理解,到完成程序,中途遇见了很多的坑,也学到了很多。开始明白了界面实际是将某种固件在特定的位置点上进行展示,以及按钮的一些使用方法;也明白了界面也是一个程序,但是不能与循环同时运行的原因。这次大作业锻炼了我的编程能力,让我自己在自我解决问题的方面迈出了一大步,网络上的资源很多,我们要学会充分利用。

相关源代码

客户端UI

img

服务端UI

img

客户端源代码

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'Light_Client.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.
import random
import sys
import threading
import time
import socket
from multiprocessing.connection import Client

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QThread

#Port是本设备的监听地址,服务端默认三个设备是 设备三8887 设备二8888 设备一8889 三个端口,如有需要可以自行修改
#使用不同的Port端口值即可新建一个设备
IP = '127.0.0.1'
Port = '8889'


# 创建套接字类,便于后期的套接字的使用
class Client:
    client_socket = None

    def __init__(self):
        self.initialize_socket()

    def initialize_socket(self):
        # 创建套接字
        self.clientsocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)


# PYQT界面的设计代码
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(597, 489)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")

        # 第一个标签 写了 温度显示
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(130, 70, 71, 31))
        self.label.setObjectName("label")

        # 第二个标签 写了湿度显示
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(290, 70, 81, 31))
        self.label_2.setObjectName("label_2")

        # 第三个标签 写了照度显示
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(450, 70, 71, 31))
        self.label_3.setObjectName("label_3")

        # LCD显示器 显示温度数据
        self.lcdNumber_WenDu = QtWidgets.QLCDNumber(self.centralwidget)
        self.lcdNumber_WenDu.setGeometry(QtCore.QRect(110, 110, 91, 31))
        self.lcdNumber_WenDu.setObjectName("lcdNumber_WenDu")

        # LCD显示器 显示照度数据
        self.lcdNumber_ZhaoDu = QtWidgets.QLCDNumber(self.centralwidget)
        self.lcdNumber_ZhaoDu.setGeometry(QtCore.QRect(430, 110, 91, 31))
        self.lcdNumber_ZhaoDu.setObjectName("lcdNumber_ZhaoDu")

        # LCD显示器 显示湿度数据
        self.lcdNumber_ShiDu = QtWidgets.QLCDNumber(self.centralwidget)
        self.lcdNumber_ShiDu.setGeometry(QtCore.QRect(270, 110, 91, 31))
        self.lcdNumber_ShiDu.setObjectName("lcdNumber_ShiDu")

        # 第四个标签 显示了灯工作状态的解释
        self.label_4 = QtWidgets.QLabel(self.centralwidget)
        self.label_4.setGeometry(QtCore.QRect(180, 270, 301, 41))
        self.label_4.setObjectName("label_4")

        # 文本框 用于展示当前设备的IP地址
        self.IP_Display = QtWidgets.QTextBrowser(self.centralwidget)
        self.IP_Display.setGeometry(QtCore.QRect(110, 320, 161, 31))
        self.IP_Display.setObjectName("IP_Display")
        self.IP_Display.setText(IP + ":" + Port)

        # 第五个标签 展示了IP字样
        self.label_5 = QtWidgets.QLabel(self.centralwidget)
        self.label_5.setGeometry(QtCore.QRect(150, 360, 101, 31))
        self.label_5.setObjectName("label_5")

        # 开始工作的按钮 触发则进入到工作状态
        self.Start = QtWidgets.QPushButton(self.centralwidget)
        self.Start.setGeometry(QtCore.QRect(310, 330, 75, 23))
        self.Start.setObjectName("Start")

        # 停止工作按钮 触发则停止工作
        self.Stop = QtWidgets.QPushButton(self.centralwidget)
        self.Stop.setGeometry(QtCore.QRect(410, 330, 75, 23))
        self.Stop.setObjectName("Stop")

        # 中央灯状态表示区
        self.frame = QtWidgets.QFrame(self.centralwidget)
        self.frame.setGeometry(QtCore.QRect(220, 190, 120, 80))
        self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
        self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
        self.frame.setStyleSheet("background-color:black")
        self.frame.setObjectName("frame")

        # MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 597, 22))
        self.menubar.setObjectName("menubar")
        # MainWindow.setMenuBar(self.menubar)

        self.statusbar = QtWidgets.QStatusBar(MainWindow)

        # ----------------------按钮触发方法的设置-----------------------------------------
        # 开始按钮点击则启动start_work
        self.Start.clicked.connect(self.start_work)
        self.Stop.clicked.connect(self.stop_work)
        # 关闭按钮则启动stop_work
        # ----------------------按钮触发方法的设置-----------------------------------------

        self.statusbar.setObjectName("statusbar")
        # MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    # 设置所有的组件上的文字
    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label.setText(_translate("MainWindow", "温度显示"))
        self.label_2.setText(_translate("MainWindow", "湿度显示"))
        self.label_3.setText(_translate("MainWindow", "照度显示"))
        self.label_4.setText(_translate("MainWindow", "红色状态为正常工作 蓝色状态为关闭"))
        self.label_5.setText(_translate("MainWindow", "设备IP地址"))
        self.Start.setText(_translate("MainWindow", "工作"))
        self.Stop.setText(_translate("MainWindow", "停止"))

    # 开灯函数,在随机生成数据并显示的同时做到了向Server发包并启动了UDP的接收代码
    def start_work(self):
        # 数据处理,随机生成温度湿度照度数据并显示
        self.frame.setStyleSheet("background-color:red")
        WenDu = random.randint(1, 100)
        ShiDu = random.randint(1, 100)
        ZhaoDu = random.randint(1, 100)
        self.lcdNumber_WenDu.display(WenDu)
        self.lcdNumber_ShiDu.display(ShiDu)
        self.lcdNumber_ZhaoDu.display(ZhaoDu)
        # 创建套接字 向8080服务器所在的端口进行UDP数据包的发送
        self.client = Client()
        self.client.initialize_socket()
        # 发送的数据除了三个传感器数据外并带上了自己监控的接收端口
        message = str(WenDu) + ' ' + str(ShiDu) + " " + str(ZhaoDu) + " " + Port
        self.client.clientsocket.sendto(message.encode("utf8"), ('127.0.0.1', 8080))
        # flag用于保证循环接收不会启动第二次,导致端口占用的报错
        # 开启一次则不再进行开启
        global flag
        if flag == 0:
            # 使用一个新的线程进行接收的操作
            get_Thred = threading.Thread(target=getter)
            get_Thred.start()
        flag = 1

    # stop实质停止所有的数据,则将显示的数据置零
    def stop_work(self):
        # while (1):
        self.frame.setStyleSheet("background-color:blue")
        self.lcdNumber_WenDu.display(0)
        self.lcdNumber_ShiDu.display(0)
        self.lcdNumber_ZhaoDu.display(0)


# 接收端函数 占用一个端口进行循环接收UDP数据包
def getter():
    udp_getter = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_getter.bind(('127.0.0.1', int(Port)))
    while 1:
        message, addr = udp_getter.recvfrom(1024)
        sig = message.decode('utf-8')
        # print(sig)
        # 对数据包内部的内容进行判断,判断完成后执行相应的操作
        if sig == '1':
            ui.start_work()
        elif sig == '0':
            ui.stop_work()


if __name__ == "__main__":
    sig = ""
    flag = 0
    app = QtWidgets.QApplication(sys.argv)
    widget = QtWidgets.QWidget()
    ui = Ui_MainWindow()
    ui.setupUi(widget)
    widget.show()
    sys.exit(app.exec_())

服务端源代码

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'Light_Server.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.

#服务端默认三个设备是 设备三8887 设备二8888 设备一8889 三个端口,如有需要可以自行修改

import socket
import sys
import threading
import time

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QThread


# PYQT界面代码部分
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.show_shebei1 = QtWidgets.QTextBrowser(self.centralwidget)
        self.show_shebei1.setGeometry(QtCore.QRect(30, 70, 501, 41))
        self.show_shebei1.setObjectName("show_shebei1")
        self.show_shebei3 = QtWidgets.QTextBrowser(self.centralwidget)
        self.show_shebei3.setGeometry(QtCore.QRect(30, 210, 501, 41))
        self.show_shebei3.setObjectName("show_shebei3")
        self.show_shebei2 = QtWidgets.QTextBrowser(self.centralwidget)
        self.show_shebei2.setGeometry(QtCore.QRect(30, 140, 501, 41))
        self.show_shebei2.setObjectName("show_shebei2")
        self.labal_shebei1 = QtWidgets.QLabel(self.centralwidget)
        self.labal_shebei1.setGeometry(QtCore.QRect(250, 41, 54, 21))
        self.labal_shebei1.setObjectName("labal_shebei1")
        self.label_shebei2 = QtWidgets.QLabel(self.centralwidget)
        self.label_shebei2.setGeometry(QtCore.QRect(250, 112, 54, 31))
        self.label_shebei2.setObjectName("label_shebei2")
        self.label_shebei3 = QtWidgets.QLabel(self.centralwidget)
        self.label_shebei3.setGeometry(QtCore.QRect(250, 180, 54, 31))
        self.label_shebei3.setObjectName("label_shebei3")
        self.textBrowser_shujubao = QtWidgets.QTextBrowser(self.centralwidget)
        self.textBrowser_shujubao.setGeometry(QtCore.QRect(70, 280, 256, 192))
        self.textBrowser_shujubao.setObjectName("textBrowser_shujubao")
        self.start_light = QtWidgets.QPushButton(self.centralwidget)
        self.start_light.setGeometry(QtCore.QRect(600, 110, 75, 23))
        self.start_light.setStyleSheet("background-color:red")
        self.start_light.setObjectName("start_light")
        self.stop_light = QtWidgets.QPushButton(self.centralwidget)
        self.stop_light.setGeometry(QtCore.QRect(600, 160, 75, 23))
        self.stop_light.setStyleSheet("background-color:yellow")
        self.stop_light.setObjectName("stop_light")
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(150, 490, 181, 20))
        self.label.setObjectName("label")
        self.Stop_All = QtWidgets.QPushButton(self.centralwidget)
        self.Stop_All.setGeometry(QtCore.QRect(420, 360, 201, 51))
        self.Stop_All.setStyleSheet("background-color:White")
        self.Stop_All.setObjectName("Stop_All")
        # MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 22))
        self.menubar.setObjectName("menubar")
        # MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        # MainWindow.setStatusBar(self.statusbar)
        # -----------------------------------------------------------------
        # 按钮触发部分
        # Stop_all按钮连接的是开始接收按钮
        self.Stop_All.clicked.connect(self.start)
        # start_light连接的是开灯按钮 启动light_up函数
        self.start_light.clicked.connect(self.light_up)
        # stop_light连接的是关灯按钮 启动lightdown函数
        self.stop_light.clicked.connect(self.light_down)
        # self.Stop_All.clicked.connect(self.stop_all)
        # 按钮触发部分
        # -----------------------------------------------------------------
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    # 对每一个部件上面设置显示的文字
    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.labal_shebei1.setText(_translate("MainWindow", "设备一"))
        self.label_shebei2.setText(_translate("MainWindow", "设备二"))
        self.label_shebei3.setText(_translate("MainWindow", "设备三"))
        self.start_light.setText(_translate("MainWindow", "开灯"))
        self.stop_light.setText(_translate("MainWindow", "关灯"))
        self.label.setText(_translate("MainWindow", "数据包列表"))
        self.Stop_All.setText(_translate("MainWindow", "开始接收"))

    # 开始一个新的线程作为对8080端口的监听
    def start(self):
        get_Thred = threading.Thread(target=getter)
        get_Thred.start()

    # 对设备一的设备相关信息进行显示
    def dayin1(self):
        # print(str(get))
        self.show_shebei1.append(get)
        # print(str(get))

    # 对设备二的设备相关信息进行显示
    def dayin2(self):
        # print(str(get))
        self.show_shebei2.append(get)
        # print(str(get))

    # 对设备三的设备相关信息进行显示
    def dayin3(self):
        # print(str(get))
        self.show_shebei3.append(get)
        # print(str(get))

    # 对数据包的发送与接收进行显示,显示的量是一个全局变量bag,如果需要显示则先修改bag再进行函数的调用
    def dayin4(self):
        # 有一个问题:不知道为什么在跨线程的调用中,似乎只有append方法起作用,原本的setText并没有起作用
        self.textBrowser_shujubao.append(bag)

    # 通过UDP发包向所有设备的地址分别发送开灯的数据包,设备接收到会调用开灯的函数,相当于执行一次开灯
    def light_up(self):
        udp_up = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        mes = str(1)
        udp_up.sendto(mes.encode("utf8"), ('127.0.0.1', 8889))
        udp_up.sendto(mes.encode("utf8"), ('127.0.0.1', 8888))
        udp_up.sendto(mes.encode("utf8"), ('127.0.0.1', 8887))
        global bag
        time.sleep(0.2)
        bag = "向所有设备发出开灯数据包"
        self.dayin4()

    # 通过UDP发包向所有设备的地址分别发送关灯的数据包,设备接收到会调用关灯的函数,相当于执行一次关灯
    def light_down(self):
        udp_down = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        mes = str(0)
        udp_down.sendto(mes.encode("utf8"), ('127.0.0.1', 8889))
        udp_down.sendto(mes.encode("utf8"), ('127.0.0.1', 8888))
        udp_down.sendto(mes.encode("utf8"), ('127.0.0.1', 8887))
        global bag
        time.sleep(0.2)
        bag = "向所有设备发出关灯数据包"
        self.dayin4()

    # 启动新的线程进行 While 循环来确保能够接收到设备发来的UDP包


def getter():
    udp_getter = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_getter.bind(('127.0.0.1', 8080))
    while 1:
        message, addr = udp_getter.recvfrom(1024)
        global get
        global bag
        temp = message.decode('utf-8')
        temp = str(temp)
        new = temp.split()
        # 对发送来的包进行鉴别,判断出数据包来自哪里并读取处理其中的内容,调用相关的方法显示在数据包列表和相关的设备列表
        if new[3] == '8889':
            # 来自8889设备
            # 处理数据显示区域的显示
            get = "设备一:" + "温度为" + new[0] + " 湿度为" + new[1] + " 照度为" + new[2]
            ui.dayin1()
            # 处理数据包显示区的信息
            bag = "来自" + new[3] + "的数据包"
            ui.dayin4()
        elif new[3] == '8888':
            # 来自8888设备
            # 处理数据显示区的数据
            get = "设备二:" + "温度为" + new[0] + " 湿度为" + new[1] + " 照度为" + new[2]
            ui.dayin2()
            # 处理数据包显示区域的数据
            bag = "来自" + new[3] + "的数据包"
            ui.dayin4()
        elif new[3] == '8887':
            # 来自8887设备
            # 处理数据显示区的设备
            get = "设备三:" + "温度为" + new[0] + " 湿度为" + new[1] + " 照度为" + new[2]
            ui.dayin3()
            # 处理数据包显示区的显示数据
            bag = "来自" + new[3] + "的数据包"
            ui.dayin4()
        else:
            bag = "来自" + new[3] + "的未知设备" + new[3]
            ui.dayin4()


if __name__ == "__main__":
    get = ""
    bag = ""
    app = QtWidgets.QApplication(sys.argv)
    widget = QtWidgets.QWidget()
    ui = Ui_MainWindow()
    ui.setupUi(widget)
    widget.show()
    sys.exit(app.exec_())