项目源码全开源。
本次项目python版本环境: python3.10.9
所需要的库: PySide2、pymodbus、serial、threading、time
提示:项目文件要在同一个目录下。本案例只有modbus-rtu模式,就是通过串行口进行通讯的,如果需要使用modbus-tcp模式的,只需要改部分代码就可以了,大家可以自行探索一下。
环境配置
1.命令行输入(下载慢的话可使用国内镜像源下载,之前的文章我讲过)
- pip install PySide2 -i https://pypi.tuna.tsinghua.edu.cn/simple/
- pip install pymodbus
- pip install pyserial
2.安装modbus仿真工具,modbus Poll和modbus slave。
两个软件都只有几兆,去网上随便一搜就有很多,所以这两个软件还请大家自行下载。
3.安装Configure Virtual Serial Port Driver,用来创建虚拟串口。
下文讲解完代码再讲述如何使用这几个软件。
运行效果
首先运行程序后会自动检测电脑上的串行端口,包括虚拟端口,然后在复选框选中某个串行口的时候,后面会显示出相应串行口的描述。
接下来填写的是波特率,波特率是必须要填写的,一般填9600就可以,因为那两个仿真软件是默认就是9600的波特率,波特率是必须一样才能正常连接以及通信的。然后选择模式,模式有两种,一种是模拟modbus poll,一种是模拟modbus slave。modbus poll是用来和modbus slave通信的,modbus slave是用来和modbus poll通信的。仿真的时候要选择正确。
连接之后可以读取modbus从站设备保持型寄存器中的值,实时显示在下方表格中。
在表格里也可以实时进行更改从站设备中保持型寄存器的值。
这里只是一个案例用来参考学习,所以没有进行多个从设备的仿真。有需要可以自己参考这个做一下,或者有问题的话可以评论告诉我。
代码修改:
麻烦大家把这行注释掉,然后加上一个print打印的。后来使用的时候发现在线程中不可以使用QMessageBox,这里应该要使用Qt中的QThread或者QProcess。后面有时间再给大家改吧。
代码解释
class MODBUS:
def __init__(self, app1):
self.app1: QApplication = app1
file = QFile('./modbus.ui')
file.open(QFile.ReadOnly)
file.close()
self.window = QUiLoader().load(file)
self.port_dict = self.initial()
self.window.port_box.setEditable(True)
self.window.port_box.addItems(list(self.port_dict.keys()))
self.window.port_box.currentIndexChanged.connect(self.box_change)
self.window.description.setText(self.port_dict[list(self.port_dict.keys())[0]])
self.window.data_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.table_init()
self.window.add_button.clicked.connect(self.add_data)
self.window.drop_button.clicked.connect(self.drop_data)
self.window.conn_button.clicked.connect(self.connect_modbus)
self.window.close_button.clicked.connect(self.close_modbus)
self.window.data_table.cellChanged.connect(self.write_registers)
self.client = None
self.store = ModbusSlaveContext(di=ModbusSequentialDataBlock.create(), co=ModbusSequentialDataBlock.create(),
hr=ModbusSequentialDataBlock.create(), ir=ModbusSequentialDataBlock.create())
self.context = ModbusServerContext(slaves=self.store, single=True)
这段代码定义了一个MODBUS类对象,通过__init__函数初始化,初始化函数中首先加载了QT的ui文件,然后调用类里面的initial()函数获取电脑的串行端口,并且将电脑的所有串行端口显示在界面的COMBOX控件中,下面让这个串行端口复选框绑定了一个函数,在复选框选项改变的时候会执行这个函数,函数作用是在另一个控件LineEdit中显示这个串行端口的详细描述。下面是一系列给按钮绑定点击事件函数的代码。最后面三行代码,倒数第三行定义了一个客户端对象,倒数第二行定义了一个Modbusslave的存储器对象,用来存储数据,最后一行是使用上面的存储器对象来生成一个上下文对象。
def box_change(self):
port = self.window.port_box.currentText()
self.window.description.setText(self.port_dict[port])
def initial(self):
# 获取所有串行端口
ports = serial.tools.list_ports.comports()
# 打印所有串行端口信息
port_dict = {}
for port, desc, hwid in sorted(ports):
# print(f"Port: {port} | Description: {desc} | Hardware ID: {hwid}")
port_dict[port] = desc
return port_dict
这两个函数就是上面用到的两个函数,一个用来获取电脑串行端口号,一个用来更新lineedit控件的文本。
def table_init(self):
for row in range(10):
item = QTableWidgetItem()
item.setText(f'{row}')
self.window.data_table.setItem(row, 0, item)
def add_data(self):
rowcount = self.window.data_table.rowCount()
# print(rowcount)
self.window.data_table.insertRow(rowcount)
item = QTableWidgetItem()
item.setText(f'{rowcount}')
self.window.data_table.setItem(rowcount, 0, item)
def drop_data(self):
rowcount = self.window.data_table.rowCount()
# print(rowcount)
self.window.data_table.removeRow(rowcount - 1)
这三个函数,第一个是用来给表格初始化,让表格的第一列显示出0到9的数字,代表保持型寄存器的地址。第二个函数用来给表格控件增加一行,第三个则用来给表格控件删除一行。
def run_modbus_server(self, port):
try:
server = StartSerialServer(context=self.context, port=port, framer=ModbusRtuFramer)
server.serve_forever()
except Exception as e:
print(e)
def close_modbus(self):
self.client.close()
这两个函数,第一个函数是用来运行一个modbus-rtu模式的server服务器。这个在多线程运行比较好,所以单独定义了一个函数。第二个函数很明显是用来关闭客户端的。
def connect_modbus(self):
if self.window.method_type.currentText() == "client/poll":
self.window.status.setText("连接中...")
baud = self.window.baud.text()
port = self.window.port_box.currentText()
self.client = ModbusClient(method='rtu', port=port, baudrate=int(baud))
if self.client.connect():
self.window.status.setText("连接成功!")
table_change = threading.Thread(target=self.read_registers, args=tuple())
table_change.start()
elif self.window.method_type.currentText() == "server/slave":
self.window.status.setText("连接中...")
port = self.window.port_box.currentText()
modbus_thread = threading.Thread(target=self.run_modbus_server, args=(port,))
modbus_thread.start()
self.window.status.setText("连接成功!")
table_change = threading.Thread(target=self.read_registers, args=tuple())
table_change.start()
这个函数用来连接modbus的服务器或者客户端。如果使用的是服务器模式的话,这个函数的主要作用是打开一个modbus-rtu服务器,并且开一个线程用来持续读取数据。如果用的是客户端模式的话,这个函数用来连接服务器,并且也会打开一个多线程用来读取数据。
def read_registers(self):
if self.window.method_type.currentText() == "client/poll":
while True:
row_num = self.window.data_table.rowCount()
response = self.client.read_holding_registers(address=0, count=row_num, slave=1)
if not response.isError():
for i in range(len(response.registers)):
try:
if str(response.registers[i]) != self.window.data_table.item(i, 2).text():
item = QTableWidgetItem()
item.setText(f'{response.registers[i]}')
self.window.data_table.setItem(i, 2, item)
else:
pass
except:
item = QTableWidgetItem()
item.setText(f'{response.registers[i]}')
self.window.data_table.setItem(i, 2, item)
else:
# QMessageBox.information(self.window, "提示", "寄存器数量小于正在读取的数据数量。")
print("寄存器数量小于正在读取的数据数量。")
break
time.sleep(2)
elif self.window.method_type.currentText() == "server/slave":
while True:
try:
row_num = self.window.data_table.rowCount()
self.store.setValues(3, row_num - 1, [0])
response = self.context[0].getValues(3, 0, count=row_num)
# print(response)
for i in range(len(response)):
try:
if str(response[i]) != self.window.data_table.item(i, 2).text():
item = QTableWidgetItem()
item.setText(f'{response[i]}')
self.window.data_table.setItem(i, 2, item)
except:
item = QTableWidgetItem()
item.setText(f'{response[i]}')
self.window.data_table.setItem(i, 2, item)
except Exception as e:
print(e)
time.sleep(2)
这个代码用来读取保持型寄存器的数据,同样分为服务器模式和客户端模式,读取之后会把有改变的数据更新在表格中,读取频率的话我设置的是2s读取一次。
def write_registers(self, row, column):
if self.window.method_type.currentText() == "client/poll":
data = self.window.data_table.item(row, column).text()
value = int(data)
self.client.write_register(address=row, value=value, slave=1)
elif self.window.method_type.currentText() == "server/slave":
data = self.window.data_table.item(row, column).text()
value = int(data)
self.store.setValues(3, row, [value])
这个函数用来向保持型寄存器写入数据,通过在表格中修改值来改变modbus保持型寄存器的数据。
项目源代码
链接:https://pan.baidu.com/s/1BPPb_qvIZTRQYKfUpwAuAA?pwd=wwww
提取码:wwww