基于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接收函数在一个单独的线程上运行
测试与结果
客户端与三个服务端展示
点击服务端开始接收,客户端开始工作;再点击客户端工作按钮,客户端开始工作
分别点击服务端的停止按钮,服务端会停止工作
此时点击服务端的开机按钮,服务端会向所有的终端发出开灯指令
开灯指令在服务端接收后会返回一个数据包给服务端,只有在服务端收到这个客户端返回的数据包,证明传输成功,这样来实现可靠传输。
点击关灯按钮,所有设备关闭,但保留了接收远程信号的功能
总结与展望
这是第一次对程序制作操作界面,从刚开始的磕磕绊绊,到逐渐理解,到完成程序,中途遇见了很多的坑,也学到了很多。开始明白了界面实际是将某种固件在特定的位置点上进行展示,以及按钮的一些使用方法;也明白了界面也是一个程序,但是不能与循环同时运行的原因。这次大作业锻炼了我的编程能力,让我自己在自我解决问题的方面迈出了一大步,网络上的资源很多,我们要学会充分利用。
相关源代码
客户端UI
服务端UI
客户端源代码
# -*- 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_()) |