本章节讲述基于Pycharm环境,使用Python开发XMODEM软件升级工具的过程。阅读本章节需要具备一定的Python基础,故本章节不在讲述Pycharm安装和Python基础语法,而Python的库如何安装,使用百度搜索就可以了。开发的Python版本是V3.8,Windows是Win7。希望阅读完本文章的博友能大概了解如何在Python环境适应PYQT5库设计Windows窗口界面,当然能动手操作效果最佳。
一: 基于PYQT5使用Qt Designer设计界面
PYQT5是一款用于开发图形界面的库,可以方便的开发各种复杂的界面,但XMODEM升级工具的界面比较简单,因此本小节能介绍到的PYQT5的功能不多,希望对初学者起到抛砖引玉的作用。
1.1: 通过Python调出Qt Designer
1.2: 创建新的窗口
Qt Designer出来后,选择Widget,然后点击创建即可。
说明下: Main Window用于创建带有菜单的主窗口,而Widget创建无菜单的主窗口。
创建完成后,把鼠标放在新窗口的边上,会出现箭头,可以通过箭头调整窗口的大小。
把完新窗口保存到文件夹,并命名为Layout.ui。
1.3: 把Layout.ui转换成代码
转成代码后,可以看到,Layout.py和相关的代码
1.4: 添加显示窗口的代码
点击右键运行代码,未能看懂窗口,那是因为还未添加显示代码。
添加显示窗口的代码,代码中绿色部分。
from PyQt5 import QtCore, QtGui, QtWidgets
class UiMainWindow(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(673, 548)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MyMainWindow = QtWidgets.QMainWindow()
MyUi = UiMainWindow()
MyUi.setupUi(MyMainWindow)
MyMainWindow.show()
sys.exit(app.exec_()) # app.exec_()是类中的一个循环监听方法
点击运行代码,出现空白的窗口,如下图。
主窗口的左上角的“Form”,可以在QtDesigner修改,修改方法如下:
鼠标随便电解QtDesigner界面中主窗口,在窗口右边,找到windowTitle,修改名称。
1.5: 显示窗口代码与Layout布局代码分离
在设计主窗口界面时,会出现添加新功能,然后同步至Python,再转换成Python代码;如果Layout的代码与显示主窗口的未分离,每次转换成Python代码时,都是把显示主窗口代码弄丢,很时烦人,所以这两部分代码必须要分离。分离方法如下。
1: 新建一个Main.py的文件。
后续所有逻辑功能的代码都在Main.py里实现,当然也可以把各种不同的逻辑分到多个文件。因为XMODEM升级工具比较简单,所以没必要这么做。
1.6: 显示窗口的代码放到Main.py
在Main.py里创建一个新的类,用于实现各种逻辑,在类的初始化里实现窗口显示。
from Layout import Ui_MainWindow from PyQt5 import QtWidgets,QtGui,QtCore class MyMainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # 从Layout文件中,继承Ui_MainWindow类 def __init__(self, parent=None): super(MyMainWindow, self).__init__(parent) # 使用父类初始化 self.setupUi(self) # 初始化QDialog界面 #self.myWindowInit() # 所有窗口初始化 return # ************************************************************************************* # 逻辑代码区 # ************************************************************************************* if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) myUiWindow = MyMainWindow() myUiWindow.show() sys.exit(app.exec_()) # app.exec_()是类中的一个循环监听方法
同时,需要把Layout.py里的显示代码注释掉,否则会报错。
刚才修改了主窗口名称为“XMODEM”,保存后,在Pychar重新转换成python代码后,上图注释部分已消失,运行“Main.py”,出现同样的窗口。
1.7: 设计自己喜欢的窗口界面
Qt Designer设计窗口界面非常友好,需要什么样的功能,直接把鼠标按住左边的功能框,拖到右边的窗口即可。在把功能框拖到右边时,Qt Designer直接建立一个类,注意在Python中,所有的信息都是以类的形式存在,小到一个字节,大到主窗口。
XMODEM需要以下功能框:
1: 标签Label,标识各种信息,对应Qt Designer左边窗口的Display Widgets中的Label。如果有多的标签,将按序号先后顺序出现,下图。
2: 选择框Combo Box,对应Qt Designer左边窗口的Input Widgets中的Combo Box。如果有多的选择框,将按序号先后顺序出现,下图。
3: 按钮Button,对应Qt Designer左边窗口的Buttons中的Push Button。如果有多的按钮,将按序号先后顺序出现,下图。
4: 进度显示条Progress Bar,对应Qt Designer左边窗口的Display Widgets中的Progress Bar。如果有多的进度显示条,将按序号先后顺序出现,下图。
5: 单行输入框Line Edit,对应Qt Designer左边窗口的Input Widgets中的Line Edit。如果有多的单行输入框,将按序号先后顺序出现,下图。
6: 多行文本显示框Text Browser,对应Qt Designer左边窗口的Input Widgets中的Text Browser。如果有多的单行输入框,将按序号先后顺序出现,下图
各个功能框可以行对齐或者列对齐等方式对齐,还可以把各个功能框合并,合并后的功能框再对齐。
所有的功能框都可以设定默认值,Python代码运行后,功能框的数值都是以设定的默认值出现。
至此,XMODEM所有的功能框添加和设计完成。
7: 保存窗口,设计好的界面转换成python代码,Layout.py出现新的代码,运行Main.py,结果如下。
1.8: 显示窗口的代码放到Main.py
Layout.py源代码:
from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(896, 579) self.layoutWidget_2 = QtWidgets.QWidget(MainWindow) self.layoutWidget_2.setGeometry(QtCore.QRect(430, 10, 411, 25)) self.layoutWidget_2.setObjectName("layoutWidget_2") self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.layoutWidget_2) self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_4.setObjectName("horizontalLayout_4") self.pushButton_3 = QtWidgets.QPushButton(self.layoutWidget_2) font = QtGui.QFont() font.setPointSize(11) self.pushButton_3.setFont(font) self.pushButton_3.setFocusPolicy(QtCore.Qt.NoFocus) self.pushButton_3.setContextMenuPolicy(QtCore.Qt.NoContextMenu) self.pushButton_3.setObjectName("pushButton_3") self.horizontalLayout_4.addWidget(self.pushButton_3) self.pushButton_4 = QtWidgets.QPushButton(self.layoutWidget_2) font = QtGui.QFont() font.setPointSize(11) self.pushButton_4.setFont(font) self.pushButton_4.setFocusPolicy(QtCore.Qt.NoFocus) self.pushButton_4.setContextMenuPolicy(QtCore.Qt.NoContextMenu) self.pushButton_4.setObjectName("pushButton_4") self.horizontalLayout_4.addWidget(self.pushButton_4) self.progressBar = QtWidgets.QProgressBar(self.layoutWidget_2) self.progressBar.setProperty("value", 24) self.progressBar.setObjectName("progressBar") self.horizontalLayout_4.addWidget(self.progressBar) self.layoutWidget_3 = QtWidgets.QWidget(MainWindow) self.layoutWidget_3.setGeometry(QtCore.QRect(10, 50, 831, 25)) self.layoutWidget_3.setObjectName("layoutWidget_3") self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.layoutWidget_3) self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_6.setObjectName("horizontalLayout_6") self.lineEdit = QtWidgets.QLineEdit(self.layoutWidget_3) self.lineEdit.setObjectName("lineEdit") self.horizontalLayout_6.addWidget(self.lineEdit) self.pushButton_2 = QtWidgets.QPushButton(self.layoutWidget_3) font = QtGui.QFont() font.setPointSize(11) self.pushButton_2.setFont(font) self.pushButton_2.setObjectName("pushButton_2") self.horizontalLayout_6.addWidget(self.pushButton_2) self.pushButton_5 = QtWidgets.QPushButton(self.layoutWidget_3) font = QtGui.QFont() font.setPointSize(11) self.pushButton_5.setFont(font) self.pushButton_5.setFocusPolicy(QtCore.Qt.NoFocus) self.pushButton_5.setContextMenuPolicy(QtCore.Qt.NoContextMenu) self.pushButton_5.setObjectName("pushButton_5") self.horizontalLayout_6.addWidget(self.pushButton_5) self.textBrowser = QtWidgets.QTextBrowser(MainWindow) self.textBrowser.setGeometry(QtCore.QRect(10, 80, 831, 461)) self.textBrowser.setObjectName("textBrowser") self.layoutWidget = QtWidgets.QWidget(MainWindow) self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 301, 27)) self.layoutWidget.setObjectName("layoutWidget") self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.layoutWidget) self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.label = QtWidgets.QLabel(self.layoutWidget) font = QtGui.QFont() font.setPointSize(12) self.label.setFont(font) self.label.setObjectName("label") self.horizontalLayout_2.addWidget(self.label) self.comboBox = QtWidgets.QComboBox(self.layoutWidget) self.comboBox.setObjectName("comboBox") self.horizontalLayout_2.addWidget(self.comboBox) self.horizontalLayout_3.addLayout(self.horizontalLayout_2) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.pushButton = QtWidgets.QPushButton(self.layoutWidget) font = QtGui.QFont() font.setPointSize(11) self.pushButton.setFont(font) self.pushButton.setFocusPolicy(QtCore.Qt.NoFocus) self.pushButton.setContextMenuPolicy(QtCore.Qt.NoContextMenu) self.pushButton.setObjectName("pushButton") self.horizontalLayout.addWidget(self.pushButton) self.comboBox_2 = QtWidgets.QComboBox(self.layoutWidget) self.comboBox_2.setObjectName("comboBox_2") self.horizontalLayout.addWidget(self.comboBox_2) self.horizontalLayout_3.addLayout(self.horizontalLayout) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "XMODEM")) self.pushButton_3.setText(_translate("MainWindow", "打开文件")) self.pushButton_4.setText(_translate("MainWindow", "升级固件")) self.pushButton_2.setText(_translate("MainWindow", "发送数据(Hex)")) self.pushButton_5.setText(_translate("MainWindow", "清除窗口")) self.label.setText(_translate("MainWindow", "波特率:")) self.pushButton.setText(_translate("MainWindow", "打开串口"))
Main.py源代码(未添加逻辑代码):
from Layout import Ui_MainWindow from PyQt5 import QtWidgets,QtGui,QtCore class MyMainWindow(QtWidgets.QMainWindow, Ui_MainWindow): # 从Layout文件中,继承Ui_MainWindow类 def __init__(self, parent=None): super(MyMainWindow, self).__init__(parent) # 使用父类初始化 self.setupUi(self) # 初始化QDialog界面 #self.myWindowInit() # 所有窗口初始化 return if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) myUiWindow = MyMainWindow() myUiWindow.show() sys.exit(app.exec_()) # app.exec_()是类中的一个循环监听方法
二: 添加XMODEM逻辑代码
2.1: 定义各种参数和变量
from PyQt5 import QtWidgets from PyQt5.QtWidgets import QMessageBox,QFileDialog from Layout import Ui_MainWindow import re import os import time import struct import binascii import threading import serial as MySerial
# ****************************************** 定义常量 ****************************************************************** global BIN_FILE_PACK_SIZE # XMODEM数据包真实数据长度,128字节 BIN_FILE_PACK_SIZE = 128 global UPDATE_FIRMWARE_STATUS_NULL # 固件更新的状态,空状态 UPDATE_FIRMWARE_STATUS_NULL = 0 global UPDATE_FIRMWARE_STATUS_WAIT_C # 等待字符“C”状态 UPDATE_FIRMWARE_STATUS_WAIT_C = 1 global UPDATE_FIRMWARE_STATUS_RECEIVED_C # 接收字符“C”状态 UPDATE_FIRMWARE_STATUS_RECEIVED_C = 2 global UPDATE_FIRMWARE_STATUS_SEND_DATA # 发送数据状态 UPDATE_FIRMWARE_STATUS_SEND_DATA = 3 global UPDATE_FIRMWARE_STATUS_WAIT_ACK # 等待ACK状态 UPDATE_FIRMWARE_STATUS_WAIT_ACK = 4 global UPDATE_FIRMWARE_STATUS_RECEIVED_ACK # 接收到ACK状态 UPDATE_FIRMWARE_STATUS_RECEIVED_ACK = 5 global UPDATE_FIRMWARE_STATUS_RECEIVED_NACK # 接收到NACK状态 UPDATE_FIRMWARE_STATUS_RECEIVED_NACK = 6 global UPDATE_FIRMWARE_STATUS_SEND_EOT # 发送EOT状态 UPDATE_FIRMWARE_STATUS_SEND_EOT = 7 global UPDATE_FIRMWARE_TIME_OUT # 固件更新超时 UPDATE_FIRMWARE_TIME_OUT = 3 global XMODEM_NULL XMODEM_NULL = '0x00' global XMODEM_SOH XMODEM_SOH = '0x01' global XMODEM_EOT XMODEM_EOT = '0x04' global XMODEM_ACK XMODEM_ACK = '0x06' global XMODEM_NACK XMODEM_NACK = '0x15' global XMODEM_ETB XMODEM_ETB = '0x17' global XMODEM_CAN XMODEM_CAN = '0x18' global XMODEM_C XMODEM_C = '0x43' global XMODEM_DATA_1A XMODEM_END_DATA = '0x1A' # ********************************************* 定义变量 *************************************************************** global UpdateFirmwareStatus UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_NULL global ComPortNumber ComPortNumber = 'COM0' # 串口端口号 global ComBaudRate ComBaudRate = 9600 # 波特率全局变量 global IsBinFileReady IsBinFileReady = False global IsComOpenSuccess IsComOpenSuccess = False global ReceiveComCCount ReceiveComCCount = 0 global ReceiveComACKCount ReceiveComACKCount = 0 global ReceiveComNACKCount ReceiveComNACKCount = 0 global SystemTime SystemTime = 0 # ******************************************************************************************************************
2.2: 功能框初始化
def myWindowInit(self): Baudrate = ['9600','19200','38400','57600','15200'] self.comboBox.addItems(Baudrate) self.comboBox.currentIndexChanged.connect(self.GetComBaudRate) ComPortNumber = ['COM0', 'COM1', 'COM2', 'COM3', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9'] self.comboBox_2.addItems(ComPortNumber) self.comboBox_2.currentIndexChanged.connect(self.GetComNumber) # 关联到GetComNumber()函数 self.pushButton.clicked.connect(self.OpenComPortButton) # 点击按钮关联到OpenComPortButton()函数 self.pushButton_2.clicked.connect(self.SendComData) # 点击按钮关联到SendComData()函数 self.pushButton_3.clicked.connect(self.OpenFirmwareFile) # 点击按钮关联到OpenFirmwareFile()函数 self.pushButton_4.clicked.connect(self.UpdateFirmware) # 点击按钮关联到UpdateFirmware()函数 self.pushButton_5.clicked.connect(self.ClearTextBrowser) # 点击按钮关联到ClearTextBrowser()函数 self.progressBar.setProperty("value", 10) # 设置状态进度条初始值为10 self.lineEdit.setText('AA FF 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 BB 92 98') # 单行编辑框默认值 return
2.3: 逻辑核心代码
# *********************************************获取串口波特率函数******************************************************
2.3.1: 获取串口波特率函数
def GetComBaudRate(self): //获取串口波特率 global ComBaudRate ComBaudrateText= self.comboBox.currentText() if ComBaudrateText == '9600': ComBaudRate = 9600 elif ComBaudrateText == '19200': ComBaudRate = 19200 elif ComBaudrateText == '38400': ComBaudRate = 38400 elif ComBaudrateText == '57600': ComBaudRate = 57600 elif ComBaudrateText == '115200': ComBaudRate = 115200 return
# *****************************************获取串口号函数*************************************************
2.3.2: 获取串口号函数
def GetComNumber(self): global ComPortNumber ComPortNumber = self.comboBox_2.currentText() return
2.3.3: 打开串口函数
def OpenComPortButton(self): //打开串口
global ComBaudRate
global ComPortNumber
global IsComOpenSuccess
global XmodemSerial
if (IsComOpenSuccess == True): # 处于打开状,则点击时关闭串口
IsComOpenSuccess = False # 置打开串口标记为False
XmodemSerial.close() # 关闭串口
self.pushButton.setText("打开串口") # 按钮字符改变
print("Close COM success!")
return
try:
XmodemSerial = MySerial.Serial() # winsows系统使用com口连接串行口
XmodemSerial.port = ComPortNumber # COM口号
XmodemSerial.baudrate = ComBaudRate # 波特率
XmodemSerial.bytesize = MySerial.EIGHTBITS # 数据位8位
XmodemSerial.stopbits = MySerial.STOPBITS_ONE # 1位停止位
XmodemSerial.parity = MySerial.PARITY_NONE # 无奇偶校验
XmodemSerial.timeout = 0.001 # 实时监控串口
XmodemSerial.open() # 打开串口
if (XmodemSerial.is_open == True):
print("Open COM success!")
IsComOpenSuccess = True
self.pushButton.setText("关闭串口")
thread = threading.Thread(target=ReadComPortData, args=(self,))
thread.daemon = True # 设置为守护线程,主线程结束时此线程也会结束
thread.start()
except:
QMessageBox.information(self, '提示', '请正确配置端口参数', QMessageBox.Ok)
IsComOpenSuccess = False
return
2.3.4: 串口发送数据函数
def SendComData(self): # 发送数据按钮
global XmodemSerial
global IsComOpenSuccess
if( IsComOpenSuccess == False ):
QMessageBox.information(self, '提示', '请打开串口', QMessageBox.Ok)
return
LineEditData = self.lineEdit.text()
HexData = bytes.fromhex( LineEditData ) # 转换成十六进制数据
XmodemSerial.write( HexData )
XmodemSerial.flushInput()
self.textBrowser.insertPlainText("发→□")
self.textBrowser.insertPlainText( LineEditData )
self.textBrowser.insertPlainText("\n")
return
2.3.5:打开文件函数
def OpenFirmwareFile(self): # 打开文件按钮 from PyQt5.QtWidgets import QFileDialog FilePath = QFileDialog.getOpenFileName(None) # 读取文件路径 if ( FilePath == False ): return FilePath = FilePath[0:1] FilePathTemp = str(FilePath) # tuple类型转成String类型 FilePathTemp = FilePathTemp[2:-3] # 去除头和尾的多余字符 global BinFileSize BinFileSize = os.path.getsize(FilePathTemp) # 获取文件大小 print('File size:', end=' ') # print(BinFileSize) BinFile = open(FilePathTemp, "rb") # 通过路径打开文件 global BinFileBuffer # 声明BinFileBuffer为全局变量 BinFileBuffer = () # 创建空元组Tupple类型,数据是bytes类型,不可修改 BinFileBuffer = BinFile.read() # 读取文件内容到缓存 global IsBinFileReady IsBinFileReady = True i=0 while i < BinFileSize: t = FormatHex(BinFileBuffer[i]) # 转换成十六进制 self.textBrowser.insertPlainText( t +' ') # 在MyText多行文本框尾端插入字符 i = i+1 # 准备输出下个值 if i % 43 == 0: self.textBrowser.insertPlainText('\n') # 在 TextBrowser 多行文本框显示内容 self.textBrowser.insertPlainText("\n") BinFile.close() # 关闭文件 return
2.3.6: 升级固件函数
def UpdateFirmware(self): # 固件升级按钮 global IsBinFileReady # 文件是否打开 global IsComOpenSuccess # 串口是否打开 global XmodemSerial global XMODEM_SOH # XMODEM的包起始字符 global XMODEM_C global XMODEM_ACK global XMODEM_NACK global XMODEM_EOT # XMODEM的接收字符 global XMODEM_END_DATA global BinFileSize # 文件大小 global BinFileBuffer # 文件内容缓存区 global BinFilePackLength # 包数量 global BIN_FILE_PACK_SIZE # 包的数据大小,XMODEM 是128字节 global ReceiveComACKCount # 接收到ACK的数量 global ReceiveComNACKCount # 接收到NACK的数量 global UpdateFirmwareStatus global UPDATE_FIRMWARE_STATUS_NULL # XMODEM空状态 global UPDATE_FIRMWARE_STATUS_WAIT_C # XMODEM等待接收连接字符“C”状态 global UPDATE_FIRMWARE_STATUS_RECEIVED_C # XMODEM接收到字符“C”状态 global UPDATE_FIRMWARE_STATUS_SEND_DATA # XMODEM发送数据状态 global UPDATE_FIRMWARE_STATUS_WAIT_ACK # XMODEM等待下位机ACK状态 global UPDATE_FIRMWARE_STATUS_RECEIVED_ACK # XMODEM接收到ACK状态 global UPDATE_FIRMWARE_STATUS_RECEIVED_NACK # XMODM接收到NACK状态 global UPDATE_FIRMWARE_STATUS_SEND_EOT # XMODEM发送结束符状态 global UPDATE_FIRMWARE_TIME_OUT if( IsBinFileReady == False ): QMessageBox.information(self, '提示', '请打开文件', QMessageBox.Ok) return if (IsComOpenSuccess == False): # 串口未打开,不能升级 QMessageBox.information(self, '提示', '请打开串口', QMessageBox.Ok) return if (BinFileSize % BIN_FILE_PACK_SIZE == 0): # 文件刚好是整数包 BinFilePackLength = BinFileSize // BIN_FILE_PACK_SIZE # 整数包大小 else: BinFilePackLength = BinFileSize // BIN_FILE_PACK_SIZE + 1 # 整数包大小+1 self.progressBar.setProperty("value", 0) # 设置升级进度条 ReceiveComACKCount = 0 ReceiveComNACKCount = 0 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_WAIT_C # print( BinFilePackLength ) SystemTime = time.time() while( True ): try: ComData = XmodemSerial.readall() # 读取串口数据 if ( len(ComData) ): ComDataHex = binascii.hexlify(ComData).decode('utf-8') # 解码成Hex格式数据 XmodemSerial.flushInput() # 清空串口输入缓存 if ( (ComDataHex == XMODEM_C[2:] ) and ( UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_WAIT_C) ): UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_RECEIVED_C # 收到字符‘C’,进入收到C状态 elif ( ComDataHex == XMODEM_ACK[2:] ): UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_RECEIVED_ACK # 收到'ACK'响应,进入收到ACK状态 elif ( ComDataHex == XMODEM_NACK[2:] ): UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_RECEIVED_NACK # 收到'NACK'响应,进入收到NACK状态 else: # 收到其它无效字符,进入NULL状态 pass except: pass if (UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_NULL): # XMODEM空状态 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_NULL # 继续空状态 elif( UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_WAIT_C ): # 等待接收'C'字符状态 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_WAIT_C # 继续等待 if( (time.time() - SystemTime) > UPDATE_FIRMWARE_TIME_OUT): # 升级固件超时 QMessageBox.information(self, '提示', '固件升级超时', QMessageBox.Ok) break elif( UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_RECEIVED_C): # 接收到'C'字符状态 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_SEND_DATA # 转入发送数据状态 elif (UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_SEND_DATA): # 发送XMODEM数据 Position = ReceiveComACKCount * BIN_FILE_PACK_SIZE # 数据包数据的起始位置 if( ReceiveComACKCount < (BinFilePackLength-1) ): DataLength = BIN_FILE_PACK_SIZE # 完整一个数据包的数据长度128字节 else: if( BinFileSize % BIN_FILE_PACK_SIZE == 0 ): # 文件刚好是128字节的整数倍 DataLength = BIN_FILE_PACK_SIZE # 完整一个数据包的数据长度128字节 else: DataLength = BinFileSize%BIN_FILE_PACK_SIZE # 求余得出尾数据包的数据长度 PackSOH = struct.pack('>i', 0x01) # 包起始符 PackOrder = struct.pack('>i', (ReceiveComACKCount + 1)) # 包序号,真正地数据包从1开始 PackOrderInverse = struct.pack('>i', ~(ReceiveComACKCount+1)) # 包序号反码 if( DataLength < BIN_FILE_PACK_SIZE ): DataTupple1 = BinFileBuffer[Position:(Position+DataLength)] DataTupple2 = bytes.fromhex('1A')*(BIN_FILE_PACK_SIZE-DataLength) # 不足128字节,补0x1A数据 DataTemp = DataTupple1 + DataTupple2 # 两个元组合并 else: DataTemp = BinFileBuffer[Position:(Position + DataLength)] # 取出数据 DataCRCResult = DataCRC(DataTemp, BIN_FILE_PACK_SIZE) # 数据作CRC校验 DataCRCBytes = struct.pack('>i', DataCRCResult) # 转换Python数据格式为C语言数据格式 NewData = PackSOH[3:] + PackOrder[3:] + PackOrderInverse[3:] + DataTemp + DataCRCBytes[2:] XmodemSerial.write(NewData) # 发送数据 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_WAIT_ACK # 转入等待ACK状态 elif (UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_WAIT_ACK): # 等待接收ACK状态 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_WAIT_ACK # 转入等待ACK状态 elif (UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_RECEIVED_ACK): # 接收到ACK响应 ReceiveComACKCount = ReceiveComACKCount + 1 # 准备发送下一个数据包 UpdateStatusTemp = (int)((ReceiveComACKCount/BinFilePackLength)*100) # 进度转换为百分比 self.progressBar.setProperty("value", UpdateStatusTemp) # 设置升级进度条 if (ReceiveComACKCount == BinFilePackLength): # 数据包发送完成 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_SEND_EOT # 数据发送完成,转入结束状态 else: UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_SEND_DATA # 转入发送数据状态 elif (UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_RECEIVED_NACK): # 接收到NACK响应 ReceiveComNACKCount = ReceiveComNACKCount + 1 # 统计接收到NACK的数量 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_SEND_DATA # 准备重发数据 elif (UpdateFirmwareStatus == UPDATE_FIRMWARE_STATUS_SEND_EOT): # 发送EOT结束符 XmodemSerial.write(bytes.fromhex(XMODEM_EOT[2:])) XmodemSerial.flushOutput() # 清空串口输出缓存 UpdateFirmwareStatus = UPDATE_FIRMWARE_STATUS_NULL self.textBrowser.insertPlainText("Update Firmware Success!") self.textBrowser.insertPlainText("\n") QMessageBox.information(self, '提示', '固件升级成功', QMessageBox.Ok) break else: QMessageBox.information(self, '提示', '固件升级失败,请重新升级', QMessageBox.Ok) pass return
2.3.7: 清除文本显示框内容函数
def ClearTextBrowser(self): # 清除多行文本显示按钮 self.textBrowser.clear() return
2.3.8: 读取串口数据函数
def ReadComPortData( self ): global XmodemSerial if (XmodemSerial.is_open == False): return while(True): if( XmodemSerial.in_waiting > 0): ComData = XmodemSerial.readall() XmodemSerial.flushInput() str_data = binascii.hexlify(ComData).decode('utf-8') str_data = str_data.upper() DataTemp = re.findall(".{2}",str_data) NewData = " ".join(DataTemp) self.textBrowser.insertPlainText( "收←■" ) self.textBrowser.insertPlainText( NewData ) self.textBrowser.insertPlainText("\n") return
2.3.9: 常量转换成十六进制函数
def FormatHex(HexNumber): hex_str = hex(HexNumber).upper()[2:] # 输出十六进制大写字母 if len(hex_str) < 2: return '0{}'.format(hex_str) else: return hex_str return
2.3.10: 数据作CRC运算函数
def DataCRC( NewData,Length ): # CRC校验 i = 0 CRCData = 0x0000 while(True): CRCData = (CRCData^((NewData[i]<<8 )&0xFFFF)&0xFFFF) # 此处的0xFFFF是必要的的,因为python无2字节数据 j = 8 while(True): if( CRCData & 0x8000 ): CRCData = ((CRCData<<1)^0x1021)&0xFFFF else: CRCData = ((CRCData)<<1)&0xFFFF j -= 1 if( j==0 ): break i += 1 if( i==Length ): break return CRCData
2.3.11: 窗口显示代码
if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) myUiWindow = MyMainWindow() myUiWindow.show() sys.exit(app.exec_()) # app.exec_()是类中的一个循环监听方法
至此,XMODEM的Python代码完成,以上代码可以直接运行,运行的结果如下:
关于XMODEM升级工具的测试,请查看这篇文章第三篇:STM32+Python用XMODEM协议升级固件之STM32固件开发-CSDN博客