100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > matlab制作以太网数据接收上位机_Python制作串口通讯上位机

matlab制作以太网数据接收上位机_Python制作串口通讯上位机

时间:2018-11-05 18:25:16

相关推荐

matlab制作以太网数据接收上位机_Python制作串口通讯上位机

串口通讯具有简单易用的特点广泛应用于测试设备的通讯和数据传递、单片机与计算机的通讯等,本案例基于Python语言制作一个用于接收燃油质量流量计的串口通讯上位机,实现数据的读取和保存。

1. 相关知识点:

1.1 Python GUI库

GUI开发是开发具有用户图形界面的程序,在打包成可执行文件.exe之后,具有用户界面的程序具有更好地交互性和易用性,Python中常用的GUI库如下:

Tkinter:是Python内置的GUI库,小巧简单,著名的Python IDLE就是用tkinter实现的,在Windows, MacOS和Linux平台均可使用,适合用于开发界面简单的程序。

PyQt:功能强大的GUI开发库,具有方便的周边工具支持,如QtDesigner, Eric等;但由于其功能强大,因此安装较为繁琐,运行也较为庞大,此外还需掌握一定的C++知识,PyQt同样可应用于Windows,Mac OS和Linux平台;该GUI库适合开发界面复杂、功能强大的程序。

wxPython:wxPython则是tkinter和PyQt的一个折中选择,功能也介于两者之间,也具有与PyQt类似的可视化开发工具。

对于GUI库的选择,需要根据自己的需求而定,本例中由于制作的串口通讯上位机界面和功能都较为简单,因此选择最易上手的tkinter库制作。

1.2 类和对象

面向对象的程序设计往往是GUI程序设计的基础,让程序具有更好地封装性。类(class)是对象的一种抽象,描述了对象的特征,包括数据和操作;对象(object)是类的一个具体化,是由数据及能对其实施的操作所构成的封装体。也就是说,类不占用内存,而对象占用内存。例如:“狗”这个概念即可看作一个类,而名叫“小黄”的这条狗则是“狗”这个类的具体化对象,它具有类的特征,占用“内存“。

图片来源:中国大学Mooc—用Python玩转数据

1.3 串口通讯

串行接口简称串口(COM口),是采用串行通信方式的扩展接口。串行接口(Serial Interface)是指数据一位一位地顺序传送。其特点是通信线路简单,只要一对传输线就可以实现双向通信,从而大大降低了成本,特别适用于远距离通信,但传送速度较慢。目前常用的串口标准有RS-232、RS-422和RS-485,在功能上主要差别体现在抗干扰能力、最大传输速度和最大传输距离上。在目前计算机上使用串口通讯需要配备一根USB转串口线(如下图),在正确安装通讯线的驱动后,可在计算机的设备管理器中看到相应的COM口。

在Python编程语言中,pyserial库封装了串口通讯模块,可以像文件读写一样操作串口,如用read,write等函数,极大地简化了串口的操作。使用pyserial之前需要对这个库进行安装,方法非常简单,打开cmd命令提示符界面,输入pip install pyserial等待片刻即可自动安装好pyserial库。

1.4 线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。(上述内容摘抄自百度百科)

一个线程中可以对另一个线程进行操作,如创建、停止等,同一进程中的多个线程之间可以并发执行。Threading模块是python众多线程模块中功能强大,易于使用的线程管理模块,对线程的支持较为完善,绝大多数情况下,只需要使用 threading 这个高级模块就够了。

本例制作用于接收串口信号的GUI程序至少需要两个线程,一个线程用于响应用于对于GUI程序的界面操作,如点击按钮,另一个线程用于不停地接收并显示串口数据。

1.5 通讯协议

通讯协议指的是数据通讯的方式,即通讯双方约定好的数据解读方法,若通讯协议不正确会导致接收到的数据没有被正确地“翻译”而无法获取所需的信息。正确的通讯协议包括了正确地数据线接线、正确的波特率和正确的数据格式等。本例中采用RS422的5线制接线方式,波特率为2400,1位起始位,8位数据位,1位校验位(奇校验),1位停止位。具体来说就是在起始位和停止位之间的8位数据位用于传输一个字符信息,典型地为ASCII码字符,而一组完整的数据往往由多个ASCII码字符组成,也就是需要进行多次传输才能完整地传输出一组完整信息。在本例中,数据格式如下图所示,每位数据位之间间隔20 ms,每一组数据以回车结束;一组数据发送完后间隔50 ms:

由于串口通讯的特性,数据只能一位一位地传输,因此我们读取串口数据也需要从缓存区一位一位地读取,可以根据每位数据传输的时间间隔来设定缓存区的读取间隔,如在本例中可采用10 ms读取一次的方式,读取到有数据之后将数据传送到程序中,并清空缓存区,等待下一次读取。

2 串口通讯上位机制作流程

2.1 最终结果

首先看一下本例中制作的串口通讯上位机的最终结果,如下图所示。该上位机程序包含四个功能模块:左上角的串口通讯,左下角的原始数据显示,右上角的实时数据显示,右下角的文件保存。

具体的使用流程和功能如下图:启动软件后,首先需要选择或者输入串口号,这里利用下拉菜单动态识别可用串口;点击开始采集之后,开始采集以及串口配置区域变为不可用,停止采集按钮变为可用,同时左下角的数据接收框开始显示接收到的原始数据,右上角的数据显示框显示转换之后的数据信息;在想要保存采集数据时,设置好右下角区域的路径、文件名、数据名和采集数量之后,点击记录数据即可将当前数据写入到文件。在程序运行过程中,原始数据区域会不断地接收信号,为了避免长时间的数据堆积导致运行速度变慢,在数据满300行或者点击清空内容按钮时,该数据框实现一次清空操作;当然,也可手动点击清空按钮。

在上面这张操作演示图中,由于手边没有硬件设备,这里用了虚拟串口(左下软件)和串口调试助手(左上软件)来模拟实际的仪器所发送的串口数据;右上角软件为本文开发的采集软件,其用到的串口也事先由虚拟串口配置好了。右下角是保存数据所在的文件夹。

2.2 堆代码的枯燥过程

2.2.1 类的定义

对于GUI编程,通常将界面的控件、变量和函数封装成一个类,在使用时实例化一个该类的具体对象,进行相关的操作。通常而言,一个类的定义需要包含一个初始化函数和若干变量以及函数,在类的对象实例化时自动执行初始化函数中的内容;为了保证无论在类中哪个函数下定义的变量都具有整个类的作用域,往往需要在定义的变量前加self进行声明。如self.x这个变量的作用域是整个类,而直接定义的变量x作用域只有当前函数中。在本例中类进行如下的定义。更具体地,在后面将分模块、分功能对代码进行解读。

class myGUI: def__init__(self): #定义窗口界面、控件、变量、执行的操作 #Self.varname defReadUART(self): #串口读取操作 defSavetofile(self): #保存文件的操作GUI = myGUI() #实例化对象

2.2.2 窗口和控件的定义

在Tkinter中,界面的生成需要依靠代码实现,一个控件大约需要2-4行代码即可,一般界面和控件的生成需要在初始化函数中完成,界面生成和部分控件的生成代码如下:

def__init__(self): self.window = tk.Tk() self.window.title("油耗采集_byJianxiong Hua") self.APPlabel = ttk.Label(self.window, text = "FCM油耗采集软件",font = ('黑体', 20)) self.APPlabel.grid(row = 1, column = 1, rowspan = 2, columnspan = 4,sticky = tk.N) self.frame_COMinf = tk.Frame(self.window) self.frame_COMinf.grid(row=3, column=1) self.RunInf = tk.StringVar(value = '请选择串口号!') # TK变量,储存提示信息 self.labelInf = ttk.Label(self.frame_COMinf, textvariable = self.RunInf) self.labelInf.grid(row = 3, column = 2, padx = 5, pady = 3)

在上述代码中,首先用self.window=tk.Tk()生成一个窗口,用于承载所有的控件,然后设置其title特性;然后以self.window为载体定义了一个叫做self.APPlabel的标签(Label)控件,该控件仅用于显示信息,可对其text内容和字体等进行设置,然后用grid函数将其固定(若不固定则不会显示在window上);同理,接下来定义了一个名叫self.frame_COMinf的Frame框,然后以这个Frame又定义了一个用于显示运行状态信息的label。这里涉及到控件的变量传递:在Tkinter中控件之间传递变量需要用到TK变量,这里的tk.StringVar就是一个字符类型的TK变量,它与self.labelInf中的textvariable特征关联起来了,在程序运行中若想要更改self.labelInf显示的信息只需要修改对应的TK变量,这里是名叫self.RunInf的这个StringVar变量。更多的变量传递用法在后面有更详细地介绍。

除了label控件,还需要用到输入框(Entry), 按钮(Button), 下拉框(Combobox)等。值得一提的是,本例中的Entry, Button, Label和Combobox控件用的是ttk而非tk,二者在用法上几乎相同,而ttk的界面显示更加美观,符合win7和win10的系统风格。下面以Combobox为例,再介绍一下控件的定义:

self.labelBaudrate=ttk.Label(self.frame_COMinf,text='波特率:') self.Baudrate = tk.IntVar(value = 2400) #定义TK中的整数变量,存储波特率 boBaudrate = bobox(self.frame_COMinf, width = 12,textvariable = self.Baudrate) boBaudrate["values"] = (100, 300, 600, 1200, 2400,4800, 9600, 14400, 19200, 38400, 56000) self.labelBaudrate.grid(row = 5, column = 1, padx = 5, pady = 3) boBaudrate.grid(row = 5, column =2, padx = 5, pady = 3)

上述代码中combobox默认的值为2400,若点击下拉箭头将出现数组中定义的100,300,……,56000等数值。

2.2.3 自动查询可用串口号

自动查询可用串口号即点击串口的下拉框时动态刷新可用串口,要想实现该功能需要在初始化函数中定义combobox时将对应的函数与下拉框动作相绑定,然后将动态刷新串口功能在对应的函数中实现,具体如下:

def__init__(self): …… = tk.StringVar(value = '') #定义TK中的字符变量,存储一个串口号 boCOM = bobox(self.frame_COMinf, width = 12, textvariable= , postcommand = self.Port_List)#单机下拉时触发self.Port_List方法 boCOM.grid(row = 4, column = 2, padx = 5, pady = 3) …… def Port_List(self): port_list = list(serial.ports()) port_serial = [] #*******以下提取COM口的端口号******* if len(port_list) <= 0: self.RunInf.set("未找到端口!")else: for i in range(len(port_list)):port_serial.append(list(port_list[i])[0]) boCOM["values"] = port_serial

上述代码中涉及到两个函数,即__initial__()和Port_List(),前者为类的初始化函数,后者为动态获取可用串口号并将其赋值给相应的下拉框的功能函数。该函数通过postcommand=self.Port_List语句与下拉框的下拉操作相绑定。

2.2.3 按钮的执行函数绑定和串口打开

与下拉框绑定执行函数的方法类似,对于按钮控件则必须绑定相应的函数,以执行点击按钮时的操作。这里以开始采集和停止采集按钮为例进行说明,具体代码如下:

def __init__(self): …… #开始和停止按钮 self.buttonStart = ttk.Button(self.window, text = "开始采集", command = self.Start) self.buttonStart.grid(row = 8, column = 1, padx = 5, pady = 3, sticky =tk.E) self.buttonStop = ttk.Button(self.window, text = "停止采集", command = self.Stop) self.buttonStop.grid(row = 8, column = 3, padx = 5, pady = 3) self.buttonStop.configure(state = 'disabled') self.ser = serial.Serial() #串口变量 …… def Start(self): self.ser.port = .get() #端口号 self.ser.baudrate = self.Baudrate.get() #波特率 self.ser.timeout = 1#超时设置,1s未读取到数据则返回结果 strParity = self.Parity.get() #校验形式 if (strParity == "NONE"): self.ser.parity = serial.PARITY_NONE elif (strParity=="ODD"): self.ser.parity = serial.PARITY_ODD elif(strParity=="EVEN"): self.ser.parity = serial.PARITY_EVEN elif(strParity=="MARK"): self.ser.parity = serial.PARITY_MARK elif(strParity=="SPACE"): self.ser.parity = serial.PARITY_SPACE strStopbits = self.Stopbits.get() #停止位 if (strStopbits == "1"): self.ser.stopbits = serial.STOPBITS_ONE elif (strStopbits == "1.5"): self.ser.stopbits =serial.STOPBITS_ONE_POINT_FIVE elif (strStopbits == "2"): self.ser.stopbits = serial.STOPBITS_TWO self.RunInf.set("串口打开失败!") self.ser.open() #打开串口 if (self.ser.isOpen()): #判断是否成功打开 self.buttonStart.configure(state = 'disabled') self.buttonStop.configure(state = 'normal') boCOM.configure(state = 'disabled') boBaudrate.configure(state = 'disabled') boParity.configure(state ='disabled') boStopbits.configure(state = 'disabled') self.RunInf.set("已成功打开串口") self.uartState = True self.ReadUART() #调用读取串口的程序 def Stop(self): #关闭串口 self.t.cancel() #停止定时器 if (self.ser.isOpen()): self.ser.close() self.buttonStop.configure(state = 'disabled') self.buttonStart.configure(state = 'normal') boCOM.configure(state = 'normal') boBaudrate.configure(state = 'normal') boParity.configure(state = 'normal') boStopbits.configure(state = 'normal') self.RunInf.set("已关闭串口!") self.uartState = False

这段代码列出了两个按钮(开始采集和停止采集)的功能函数,具体通过定义Button控件时的command=self.Start/Stop属性将Start/Stop函数与对应的按钮进行了绑定,这样在点击按钮时即可执行对应的函数。在initial函数中还需要声明一个串口变量ser,然后再Start函数中对ser变量中的端口号port、波特率baudrate、校验parity、停止位stopbits、超时时间等进行赋值,然后通过ser.open()打开串口,在判断串口成功打开后,执行串口读取函数self.ReadUART()。该函数涉及到线程的使用,在2.2.4节进行介绍。在Stop函数中需要执行的操作就简单得多,即停止掉线程的定时器以结束串口读取线程,然后关闭串口,更改按钮等控件的状态。Normal为可用状态,disabled为不可更改的状态(灰色)。

2.2.4 多线程

前面已经提到,线程是在一个主程序下并行执行的一些操作,在执行时不受主线程的影响,尤其是在主程序需要执行循环语句的情况下,往往需要启动新的线程去执行其他任务。这里简单介绍两种基于python的多线程方法。

方法一:

import threadingimport timedef test_thread (): print(“testing thread”) time.sleep(0.5)thread1 = threading.Thread(target=test_thread)thread1.start()

方法一首先需要定义一个需要新线程执行的函数,这里是test_thread()函数,然后利用threading.Thread语句声明一个线程,目标属性为test_thread函数,再用start方法启动该线程即可。线程启动后即反复不停地执行test_thread()函数,在主程序停止时线程也停止。可以用相应方法

方法二:

import threadingdef test_thread (): print(“testing thread”) globaltimer1 timer1= threading.Timer(0.5, test_thread) timer1.start()timer1 = threading.Timer(0.5, test_thread)timer.start()

在方法二中,并没有像方法一中那样声明线程对象,而是利用构建线程定时器的方法实现新线程的构建。在主程序中首先第一次构建线程定时器并启动后,在执行函数中也需要构建线程定时器,这样每次函数执行完毕后都会有一个线程定时器对象对函数自身进行调用,这样也实现了线程的功能。这种方式较为灵活,用于简单的线程操作,还可以用timer1.cancel()方法终止该线程。在本例中即采用的这种方式构建串口线程。

本例中的串口线程关键代码如下:

def Start(self):…… #打开串口的操作 ……self.ReadUART()#调用读取串口的程序def ReadUART(self): …… #读取串口数据的操作 …… self.t=threading.Timer(0.005,self.ReadUART) self.t.start() #启动定时器

同理,检测数据接收框长度并自动清零的线程构建如下:

def __init__(self):……#控件和变量的定义……self.MonitorText()#启动文本框监视器线程,到达一定数量后自动清零self.window.mainloop() def MonitorText(self): if len(self.OutputText.get('1.0',tk.END)) > 500: #超过一定长度后自动清零 self.OutputText.delete(1.0,tk.END) self.monitor=threading.Timer(0.1,self.MonitorText) self.monitor.start()

2.2.5 原始数据接收框

程序的左下角的文本框即为原始数据接收框,用于接收并显示原始信号,这是一个简单的控件、按钮、线程的结合使用。点击清空内容按钮可清空接收框的内容,或者在接收框接收到的数据长度超过500时自动清空内容。具体代码如下:

def__init__(self): …… #其他控件和变量的定义操作 …… #**********************数据接收框************ self.frame_Recv = ttk.Frame(self.window) self.frame_Recv.grid(row = 9, column = 1) self.labelRecvName = ttk.Label(self.frame_Recv, text = '接收到的原始数据:') self.labelRecvName.grid(row = 1, column = 1, padx = 5, pady = 1, sticky= tk.W) self.frameTransSon = tk.Frame(self.frame_Recv) #同一个frame用了grid不能用pack,因此建立一个子frame self.frameTransSon.grid(row = 10, column =1, rowspan = 6, columnspan =2, padx = 5, pady = 1) self.scrollbarTrans = tk.Scrollbar(self.frameTransSon) self.scrollbarTrans.pack(side = tk.RIGHT, fill = tk.Y) self.OutputText = tk.Text(self.frameTransSon, wrap = tk.WORD, width =30, height = 8, yscrollcommand = self.scrollbarTrans.set, font =('TimesNewRoman', 8)) self.OutputText.pack() self.buttonClearText = ttk.Button(self.frame_Recv, text = "清空内容", command = self.ClearText) self.buttonClearText.grid(row = 17, column = 1, columnspan = 2, padx =5, pady = 1, sticky = tk.N) ……self.MonitorText()#启动文本框监视器线程,到达一定数量后自动清零…… def ClearText(self): self.OutputText.delete(1.0,tk.END) def MonitorText(self): if len(self.OutputText.get('1.0',tk.END)) > 500: #文本超过一定长度后自动清零 self.OutputText.delete(1.0,tk.END) self.monitor=threading.Timer(0.1,self.MonitorText) self.monitor.start() #启动定时器monitor

2.2.6 数据传递和计算

在本例的串口程序中,数据传递主要包括两类,一类是控件间的数据传递,即将数据传递给控件用于显示或者获取控件上显示的数据;另一类是读取串口缓存区的数据,并做相应的类型转换后进行相关的计算操作。

控件间的数据传递主要依靠的是一类叫做TK变量的特殊数据类型,其定义方式为a=tk.StringVar(value=’astring’),然后将该TK变量与相应控件的textvariable属性相绑定,如label1=ttk.Label(window, textvariable=a)。如此一来,在程序中无论何时何处只要对变量a (TK变量)进行了修改,那么在label1上显示的内容也会随之更改。除了StringVar类型外,TK变量还有tk.DoubleVar,tk.IntVar等类型,在使用时应该注意给这些变量赋值时需要数据类型正确,否则会出错。

对于TK变量值的修改和读取与传统的变量有所不同,需要用到set和get方法。例如将刚才定义的a变量进行值的修改,应写为:a.set(‘this is a string’),要获取a的数据则应写为:b = a.get()。另外,除了TK变量外,对于包含内容的控件来说,也可以直接用get和set方法来获取和修改对应控件的值。

对于第二类串口缓存区的变量,则需要用到python中的串口操作,用.read()函数读取缓存区的变量,一般而言串口以SACII码方式发送数据,因此需要将数据解码,然后进行后续的操作。本例中的串口读取部分代码如下:

def__init__(self): …… self.ser = serial.Serial() self.data = [] …… def ReadUART(self): if (self.uartState):try:ch = self.ser.read()self.RunInf.set("正在采集数据")ch =ch.decode('ASCII') #油耗仪中数据传输为ASCII,需要解码 self.OutputText.insert(tk.END,ch)if ch == 'F': self.ch_flag = 1 #串口数据标志位为1,表示是需要的数据if self.ch_flag == 0: self.data.clear()if self.ch_flag == 1: #如果是需要的数据则执行以下操作 if ch != 'T': #读取到T为止 self.data.append(ch) if ch == 'T': #所需要的数据是T之前的5位 self.ch_flag =0 #把标志位置0except:self.RunInf.set("发生错误!") self.t=threading.Timer(0.005,self.ReadUART) self.t.start() #启动定时器

在这部分代码中,在初始化函数中声明了一个串口操作变量ser,利用该变量可对串口进行操作(2.2.3中介绍了如何打开串口)。在ReadUART函数中用ser.read()方法进行串口读取,一次读取一个字符,然后用decode(‘ASCII’)函数进行解码(内置的函数),将其插入到数据接收框中。接下来判断读取到的数据是否在后续计算中会用到,如果会用到则将其添加到data列表变量中(data.append(ch))。这样一次读取一个字符,然后该线程一直重复该过程即完成了串口数据的传递和计算。

2.2.7 文件保存

在本例中文件保存的逻辑如下:点击保存按钮后,在与之绑定的函数中将保存标志位置为1;由于串口线程独立于主线程并且无限循环,因此在串口线程中检测保存标志位是否为1,否则不保存,是1则保存当前的数据。具体地,保存数据时需要获取路径、文件名、写入的内容、写入的数量等参数,然后每保存一次,采集计数+1,直到保存的数量与设定的数量相等时,将保存标志位置0。具体代码如下:

def ReadUART(self): if (self.uartState):try:ch = self.ser.read()self.RunInf.set("正在采集数据")…… if self.issave== 1: #判断保存标志位self.Savefile() #执行Savefile函数self.testCount.set(int(self.testCount.get())+ 1)ifint(self.testCount.get()) == int(self.testNum.get()): self.testCount.set('0')self.issave = 0 self.buttonSavetofile.config(state = 'normal') self.data.clear() #接收完数据后清空except:self.RunInf.set("发生错误!") self.t=threading.Timer(0.005,self.ReadUART) self.t.start() #启动定时器 def Savefile(self): filepath = self.filePath.get() filename = self.fileName.get() wholename = os.path.join(filepath, filename) #完整的路径+文件名(无后缀) condition = self.conditionName.get() if os.path.exists(filepath) == False: os.mkdir(filepath) with open(wholename+'.txt', 'a') as f1: ls = [condition + '\t' + str(self.AvgFuel.get()) + '\n'] f1.writelines(ls)#===========保存按钮执行的函数============= def StartSave(self): self.issave = 1 self.buttonSavetofile.config(state = 'disabled')

2.2.8 生成可执行程序

生成exe需要用到pyinstaller,因此需要首先利用pip install pyinstaller安装该库,具体生成exe步骤:

1. 启动命令提示符;

2. 在命令提示符中进入到包含.py文件和.co文件的文件夹;(直接输入E:可进入磁盘,输入cd 文件夹路径即可进入文件夹)

3. 输入pyinstaller -D-i 图标名称.ico 文件名称.py --noconsole (--noconsole作用是生成的exe文件在执行时不出现黑色的dos界面)

4. 在dist文件夹中即可找到生成的可执行文件,注意-D是生成一个包含多个文件的文件夹,若将-D替换为-F即可生成只有一个exe的可执行文件,但其启动速度比-D的文件夹形式更慢.

在后台回复“FCM”或“串口GUI”或“串口程序”可获取本例的完整代码、可执行程序、虚拟串口软件和串口调试助手的下载链接。

留言区

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。