Modbus Client example#
The example acts as Modbus client sending Modbus request via serial line and TCP respectively. The shown dialog allows the definition of standard requests and displays incoming responses.
The example must be used in conjunction with the Modbus server example or another Modbus device which is either connected via TCP or Serial Port.
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
"""PySide6 port of the examples/serialbus/modbus/client example from Qt v6.x"""
from argparse import ArgumentParser, RawDescriptionHelpFormatter
import sys
from PySide6.QtCore import QCoreApplication, QLoggingCategory
from PySide6.QtWidgets import QApplication
from mainwindow import MainWindow
if __name__ == "__main__":
parser = ArgumentParser(prog="Modbus Client Example",
formatter_class=RawDescriptionHelpFormatter)
parser.add_argument("-v", "--verbose", action="store_true",
help="Generate more output")
options = parser.parse_args()
if options.verbose:
QLoggingCategory.setFilterRules("qt.modbus* = true")
a = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(QCoreApplication.exec())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import functools
from enum import IntEnum
from PySide6.QtCore import QUrl, Slot
from PySide6.QtGui import QStandardItemModel, QStandardItem
from PySide6.QtWidgets import QMainWindow
from PySide6.QtSerialBus import (QModbusDataUnit,
QModbusDevice, QModbusReply,
QModbusRtuSerialClient, QModbusTcpClient)
from ui_mainwindow import Ui_MainWindow
from settingsdialog import SettingsDialog
from writeregistermodel import WriteRegisterModel
class ModbusConnection(IntEnum):
SERIAL = 0
TCP = 1
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self._modbus_device = None
self._settings_dialog = SettingsDialog(self)
self.init_actions()
self._write_model = WriteRegisterModel(self)
self._write_model.set_start_address(self.ui.writeAddress.value())
self._write_model.set_number_of_values(self.ui.writeSize.currentText())
self.ui.writeValueTable.setModel(self._write_model)
self.ui.writeValueTable.hideColumn(2)
vp = self.ui.writeValueTable.viewport()
self._write_model.update_viewport.connect(vp.update)
self.ui.writeTable.addItem("Coils", QModbusDataUnit.Coils)
self.ui.writeTable.addItem("Discrete Inputs", QModbusDataUnit.DiscreteInputs)
self.ui.writeTable.addItem("Input Registers", QModbusDataUnit.InputRegisters)
self.ui.writeTable.addItem("Holding Registers", QModbusDataUnit.HoldingRegisters)
self.ui.connectType.setCurrentIndex(0)
self.onConnectTypeChanged(0)
self._write_size_model = QStandardItemModel(0, 1, self)
for i in range(1, 11):
self._write_size_model.appendRow(QStandardItem(f"{i}"))
self.ui.writeSize.setModel(self._write_size_model)
self.ui.writeSize.setCurrentText("10")
self.ui.writeSize.currentTextChanged.connect(self._write_model.set_number_of_values)
self.ui.writeAddress.valueChanged.connect(self._write_model.set_start_address)
self.ui.writeAddress.valueChanged.connect(self._writeAddress)
@Slot(int)
def _writeAddress(self, i):
last_possible_index = 0
currentIndex = self.ui.writeSize.currentIndex()
for ii in range(0, 10):
if ii < (10 - i):
last_possible_index = ii
self._write_size_model.item(ii).setEnabled(True)
else:
self._write_size_model.item(ii).setEnabled(False)
if currentIndex > last_possible_index:
self.ui.writeSize.setCurrentIndex(last_possible_index)
def _close_device(self):
if self._modbus_device:
self._modbus_device.disconnectDevice()
del self._modbus_device
self._modbus_device = None
def closeEvent(self, event):
self._close_device()
event.accept()
def init_actions(self):
self.ui.actionConnect.setEnabled(True)
self.ui.actionDisconnect.setEnabled(False)
self.ui.actionExit.setEnabled(True)
self.ui.actionOptions.setEnabled(True)
self.ui.connectButton.clicked.connect(self.onConnectButtonClicked)
self.ui.actionConnect.triggered.connect(self.onConnectButtonClicked)
self.ui.actionDisconnect.triggered.connect(self.onConnectButtonClicked)
self.ui.readButton.clicked.connect(self.onReadButtonClicked)
self.ui.writeButton.clicked.connect(self.onWriteButtonClicked)
self.ui.readWriteButton.clicked.connect(self.onReadWriteButtonClicked)
self.ui.connectType.currentIndexChanged.connect(self.onConnectTypeChanged)
self.ui.writeTable.currentIndexChanged.connect(self.onWriteTableChanged)
self.ui.actionExit.triggered.connect(self.close)
self.ui.actionOptions.triggered.connect(self._settings_dialog.show)
@Slot(int)
def onConnectTypeChanged(self, index):
self._close_device()
if index == ModbusConnection.SERIAL:
self._modbus_device = QModbusRtuSerialClient(self)
elif index == ModbusConnection.TCP:
self._modbus_device = QModbusTcpClient(self)
if not self.ui.portEdit.text():
self.ui.portEdit.setText("127.0.0.1:502")
self._modbus_device.errorOccurred.connect(self._show_device_errorstring)
if not self._modbus_device:
self.ui.connectButton.setDisabled(True)
message = "Could not create Modbus client."
self.statusBar().showMessage(message, 5000)
else:
self._modbus_device.stateChanged.connect(self.onModbusStateChanged)
@Slot()
def _show_device_errorstring(self):
self.statusBar().showMessage(self._modbus_device.errorString(), 5000)
@Slot()
def onConnectButtonClicked(self):
if not self._modbus_device:
return
self.statusBar().clearMessage()
md = self._modbus_device
if md.state() != QModbusDevice.ConnectedState:
settings = self._settings_dialog.settings()
if self.ui.connectType.currentIndex() == ModbusConnection.SERIAL:
md.setConnectionParameter(QModbusDevice.SerialPortNameParameter,
self.ui.portEdit.text())
md.setConnectionParameter(QModbusDevice.SerialParityParameter,
settings.parity)
md.setConnectionParameter(QModbusDevice.SerialBaudRateParameter,
settings.baud)
md.setConnectionParameter(QModbusDevice.SerialDataBitsParameter,
settings.data_bits)
md.setConnectionParameter(QModbusDevice.SerialStopBitsParameter,
settings.stop_bits)
else:
url = QUrl.fromUserInput(self.ui.portEdit.text())
md.setConnectionParameter(QModbusDevice.NetworkPortParameter,
url.port())
md.setConnectionParameter(QModbusDevice.NetworkAddressParameter,
url.host())
md.setTimeout(settings.response_time)
md.setNumberOfRetries(settings.number_of_retries)
if not md.connectDevice():
message = "Connect failed: " + md.errorString()
self.statusBar().showMessage(message, 5000)
else:
self.ui.actionConnect.setEnabled(False)
self.ui.actionDisconnect.setEnabled(True)
else:
md.disconnectDevice()
self.ui.actionConnect.setEnabled(True)
self.ui.actionDisconnect.setEnabled(False)
@Slot(int)
def onModbusStateChanged(self, state):
connected = (state != QModbusDevice.UnconnectedState)
self.ui.actionConnect.setEnabled(not connected)
self.ui.actionDisconnect.setEnabled(connected)
if state == QModbusDevice.UnconnectedState:
self.ui.connectButton.setText("Connect")
elif state == QModbusDevice.ConnectedState:
self.ui.connectButton.setText("Disconnect")
@Slot()
def onReadButtonClicked(self):
if not self._modbus_device:
return
self.ui.readValue.clear()
self.statusBar().clearMessage()
reply = self._modbus_device.sendReadRequest(self.read_request(),
self.ui.serverEdit.value())
if reply:
if not reply.isFinished():
reply.finished.connect(functools.partial(self.onReadReady, reply))
else:
del reply # broadcast replies return immediately
else:
message = "Read error: " + self._modbus_device.errorString()
self.statusBar().showMessage(message, 5000)
@Slot()
def onReadReady(self, reply):
if not reply:
return
if reply.error() == QModbusDevice.NoError:
unit = reply.result()
total = unit.valueCount()
for i in range(0, total):
addr = unit.startAddress() + i
value = unit.value(i)
if unit.registerType().value <= QModbusDataUnit.Coils.value:
entry = f"Address: {addr}, Value: {value}"
else:
entry = f"Address: {addr}, Value: {value:x}"
self.ui.readValue.addItem(entry)
elif reply.error() == QModbusDevice.ProtocolError:
e = reply.errorString()
ex = reply.rawResult().exceptionCode()
message = f"Read response error: {e} (Modbus exception: 0x{ex:x})"
self.statusBar().showMessage(message, 5000)
else:
e = reply.errorString()
code = int(reply.error())
message = f"Read response error: {e} (code: 0x{code:x})"
self.statusBar().showMessage(message, 5000)
reply.deleteLater()
@Slot()
def onWriteButtonClicked(self):
if not self._modbus_device:
return
self.statusBar().clearMessage()
write_unit = self.write_request()
total = write_unit.valueCount()
table = write_unit.registerType()
for i in range(0, total):
addr = i + write_unit.startAddress()
if table == QModbusDataUnit.Coils:
write_unit.setValue(i, self._write_model.m_coils[addr])
else:
write_unit.setValue(i, self._write_model.m_holdingRegisters[addr])
reply = self._modbus_device.sendWriteRequest(write_unit,
self.ui.serverEdit.value())
if reply:
if reply.isFinished():
# broadcast replies return immediately
reply.deleteLater()
else:
reply.finished.connect(functools.partial(self._write_finished, reply))
else:
message = "Write error: " + self._modbus_device.errorString()
self.statusBar().showMessage(message, 5000)
@Slot(QModbusReply)
def _write_finished(self, reply):
error = reply.error()
if error == QModbusDevice.ProtocolError:
e = reply.errorString()
ex = reply.rawResult().exceptionCode()
message = f"Write response error: {e} (Modbus exception: 0x{ex:x}"
self.statusBar().showMessage(message, 5000)
elif error != QModbusDevice.NoError:
e = reply.errorString()
message = f"Write response error: {e} (code: 0x{error:x})"
self.statusBar().showMessage(message, 5000)
reply.deleteLater()
@Slot()
def onReadWriteButtonClicked(self):
if not self._modbus_device:
return
self.ui.readValue.clear()
self.statusBar().clearMessage()
write_unit = self.write_request()
table = write_unit.registerType()
total = write_unit.valueCount()
for i in range(0, total):
addr = i + write_unit.startAddress()
if table == QModbusDataUnit.Coils:
write_unit.setValue(i, self._write_model.m_coils[addr])
else:
write_unit.setValue(i, self._write_model.m_holdingRegisters[addr])
reply = self._modbus_device.sendReadWriteRequest(self.read_request(),
write_unit,
self.ui.serverEdit.value())
if reply:
if not reply.isFinished():
reply.finished.connect(functools.partial(self.onReadReady, reply))
else:
del reply # broadcast replies return immediately
else:
message = "Read error: " + self._modbus_device.errorString()
self.statusBar().showMessage(message, 5000)
@Slot(int)
def onWriteTableChanged(self, index):
coils_or_holding = index == 0 or index == 3
if coils_or_holding:
self.ui.writeValueTable.setColumnHidden(1, index != 0)
self.ui.writeValueTable.setColumnHidden(2, index != 3)
self.ui.writeValueTable.resizeColumnToContents(0)
self.ui.readWriteButton.setEnabled(index == 3)
self.ui.writeButton.setEnabled(coils_or_holding)
self.ui.writeGroupBox.setEnabled(coils_or_holding)
def read_request(self):
table = self.ui.writeTable.currentData()
start_address = self.ui.readAddress.value()
assert start_address >= 0 and start_address < 10
# do not go beyond 10 entries
number_of_entries = min(int(self.ui.readSize.currentText()),
10 - start_address)
return QModbusDataUnit(table, start_address, number_of_entries)
def write_request(self):
table = self.ui.writeTable.currentData()
start_address = self.ui.writeAddress.value()
assert start_address >= 0 and start_address < 10
# do not go beyond 10 entries
number_of_entries = min(int(self.ui.writeSize.currentText()),
10 - start_address)
return QModbusDataUnit(table, start_address, number_of_entries)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>516</width>
<height>378</height>
</rect>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>1000</height>
</size>
</property>
<property name="windowTitle">
<string>Modbus Client Example</string>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="5">
<widget class="QLabel" name="label_27">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Server Address:</string>
</property>
</widget>
</item>
<item row="0" column="7">
<widget class="QPushButton" name="connectButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Connect</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="4">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="6">
<widget class="QSpinBox" name="serverEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>247</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="connectType">
<item>
<property name="text">
<string>Serial</string>
</property>
</item>
<item>
<property name="text">
<string>TCP</string>
</property>
</item>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Connection type:</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="portEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="minimumSize">
<size>
<width>250</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Read</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Start address:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="readAddress">
<property name="maximum">
<number>9</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Number of values:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="readSize">
<property name="currentIndex">
<number>9</number>
</property>
<item>
<property name="text">
<string>1</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
<item>
<property name="text">
<string>3</string>
</property>
</item>
<item>
<property name="text">
<string>4</string>
</property>
</item>
<item>
<property name="text">
<string>5</string>
</property>
</item>
<item>
<property name="text">
<string>6</string>
</property>
</item>
<item>
<property name="text">
<string>7</string>
</property>
</item>
<item>
<property name="text">
<string>8</string>
</property>
</item>
<item>
<property name="text">
<string>9</string>
</property>
</item>
<item>
<property name="text">
<string>10</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Result:</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QListWidget" name="readValue">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="writeGroupBox">
<property name="minimumSize">
<size>
<width>225</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Write</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Start address:</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QTreeView" name="writeValueTable">
<property name="showDropIndicator" stdset="0">
<bool>true</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>true</bool>
</attribute>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="writeAddress">
<property name="maximum">
<number>9</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Number of values:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="writeSize">
<property name="currentIndex">
<number>9</number>
</property>
<item>
<property name="text">
<string>1</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
<item>
<property name="text">
<string>3</string>
</property>
</item>
<item>
<property name="text">
<string>4</string>
</property>
</item>
<item>
<property name="text">
<string>5</string>
</property>
</item>
<item>
<property name="text">
<string>6</string>
</property>
</item>
<item>
<property name="text">
<string>7</string>
</property>
</item>
<item>
<property name="text">
<string>8</string>
</property>
</item>
<item>
<property name="text">
<string>9</string>
</property>
</item>
<item>
<property name="text">
<string>10</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Table:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="writeTable"/>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>13</width>
<height>17</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="readButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Read</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="writeButton">
<property name="text">
<string>Write</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="readWriteButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Read-Write</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>516</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuDevice">
<property name="title">
<string>&Device</string>
</property>
<addaction name="actionConnect"/>
<addaction name="actionDisconnect"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuToo_ls">
<property name="title">
<string>Too&ls</string>
</property>
<addaction name="actionOptions"/>
</widget>
<addaction name="menuDevice"/>
<addaction name="menuToo_ls"/>
</widget>
<action name="actionConnect">
<property name="icon">
<iconset resource="modbusclient.qrc">
<normaloff>:/images/connect.png</normaloff>:/images/connect.png</iconset>
</property>
<property name="text">
<string>&Connect</string>
</property>
</action>
<action name="actionDisconnect">
<property name="icon">
<iconset resource="modbusclient.qrc">
<normaloff>:/images/disconnect.png</normaloff>:/images/disconnect.png</iconset>
</property>
<property name="text">
<string>&Disconnect</string>
</property>
</action>
<action name="actionExit">
<property name="icon">
<iconset resource="modbusclient.qrc">
<normaloff>:/images/application-exit.png</normaloff>:/images/application-exit.png</iconset>
</property>
<property name="text">
<string>&Quit</string>
</property>
</action>
<action name="actionOptions">
<property name="icon">
<iconset resource="modbusclient.qrc">
<normaloff>:/images/settings.png</normaloff>:/images/settings.png</iconset>
</property>
<property name="text">
<string>&Options</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<tabstops>
<tabstop>connectType</tabstop>
<tabstop>portEdit</tabstop>
<tabstop>serverEdit</tabstop>
<tabstop>connectButton</tabstop>
<tabstop>readAddress</tabstop>
<tabstop>readSize</tabstop>
<tabstop>readValue</tabstop>
<tabstop>writeAddress</tabstop>
<tabstop>writeSize</tabstop>
<tabstop>writeValueTable</tabstop>
<tabstop>writeTable</tabstop>
<tabstop>readButton</tabstop>
<tabstop>writeButton</tabstop>
<tabstop>readWriteButton</tabstop>
</tabstops>
<resources>
<include location="modbusclient.qrc"/>
</resources>
<connections/>
</ui>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from PySide6.QtCore import Slot
from PySide6.QtWidgets import QDialog
from PySide6.QtSerialPort import QSerialPort
from ui_settingsdialog import Ui_SettingsDialog
class Settings:
def __init__(self):
self.parity = QSerialPort.EvenParity
self.baud = QSerialPort.Baud19200
self.data_bits = QSerialPort.Data8
self.stop_bits = QSerialPort.OneStop
self.response_time = 1000
self.number_of_retries = 3
class SettingsDialog(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.m_settings = Settings()
self.ui = Ui_SettingsDialog()
self.ui.setupUi(self)
self.ui.parityCombo.setCurrentIndex(1)
self.ui.baudCombo.setCurrentText(f"{self.m_settings.baud}")
self.ui.dataBitsCombo.setCurrentText(f"{self.m_settings.data_bits}")
self.ui.stopBitsCombo.setCurrentText(f"{self.m_settings.stop_bits}")
self.ui.timeoutSpinner.setValue(self.m_settings.response_time)
self.ui.retriesSpinner.setValue(self.m_settings.number_of_retries)
self.ui.applyButton.clicked.connect(self._apply)
@Slot()
def _apply(self):
self.m_settings.parity = self.ui.parityCombo.currentIndex()
if self.m_settings.parity > 0:
self.m_settings.parity = self.m_settings.parity + 1
self.m_settings.baud = int(self.ui.baudCombo.currentText())
self.m_settings.data_bits = int(self.ui.dataBitsCombo.currentText())
self.m_settings.stop_bits = int(self.ui.stopBitsCombo.currentText())
self.m_settings.response_time = self.ui.timeoutSpinner.value()
self.m_settings.number_of_retries = self.ui.retriesSpinner.value()
self.hide()
def settings(self):
return self.m_settings
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SettingsDialog</class>
<widget class="QDialog" name="SettingsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>239</width>
<height>256</height>
</rect>
</property>
<property name="windowTitle">
<string>Modbus Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>43</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="timeoutSpinner">
<property name="accelerated">
<bool>true</bool>
</property>
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>-1</number>
</property>
<property name="maximum">
<number>5000</number>
</property>
<property name="singleStep">
<number>20</number>
</property>
<property name="value">
<number>200</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Response Timeout:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="applyButton">
<property name="text">
<string>Apply</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Serial Parameters</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Parity:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="parityCombo">
<item>
<property name="text">
<string>No</string>
</property>
</item>
<item>
<property name="text">
<string>Even</string>
</property>
</item>
<item>
<property name="text">
<string>Odd</string>
</property>
</item>
<item>
<property name="text">
<string>Space</string>
</property>
</item>
<item>
<property name="text">
<string>Mark</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Baud Rate:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="baudCombo">
<item>
<property name="text">
<string>1200</string>
</property>
</item>
<item>
<property name="text">
<string>2400</string>
</property>
</item>
<item>
<property name="text">
<string>4800</string>
</property>
</item>
<item>
<property name="text">
<string>9600</string>
</property>
</item>
<item>
<property name="text">
<string>19200</string>
</property>
</item>
<item>
<property name="text">
<string>38400</string>
</property>
</item>
<item>
<property name="text">
<string>57600</string>
</property>
</item>
<item>
<property name="text">
<string>115200</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Data Bits:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="dataBitsCombo">
<item>
<property name="text">
<string>5</string>
</property>
</item>
<item>
<property name="text">
<string>6</string>
</property>
</item>
<item>
<property name="text">
<string>7</string>
</property>
</item>
<item>
<property name="text">
<string>8</string>
</property>
</item>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Stop Bits:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="stopBitsCombo">
<item>
<property name="text">
<string>1</string>
</property>
</item>
<item>
<property name="text">
<string>3</string>
</property>
</item>
<item>
<property name="text">
<string>2</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Number of retries:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="retriesSpinner">
<property name="value">
<number>3</number>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from enum import IntEnum
from PySide6.QtCore import QAbstractTableModel, QBitArray, Qt, Signal, Slot
class Column(IntEnum):
NUM_COLUMN = 0
COILS_COLUMN = 1
HOLDING_COLUMN = 2
COLUMN_COUNT = 3
ROW_COUNT = 10
class WriteRegisterModel(QAbstractTableModel):
update_viewport = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.m_coils = QBitArray(Column.ROW_COUNT, False)
self.m_number = 0
self.m_address = 0
self.m_holdingRegisters = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
def rowCount(self, parent):
return Column.ROW_COUNT
def columnCount(self, parent):
return Column.COLUMN_COUNT
def data(self, index, role):
row = index.row()
column = index.column()
if not index.isValid() or row >= Column.ROW_COUNT or column >= Column.COLUMN_COUNT:
return None
assert self.m_coils.size() == Column.ROW_COUNT
assert len(self.m_holdingRegisters) == Column.ROW_COUNT
if column == Column.NUM_COLUMN and role == Qt.DisplayRole:
return f"{row}"
if column == Column.COILS_COLUMN and role == Qt.CheckStateRole: # coils
return Qt.Checked if self.m_coils[row] else Qt.Unchecked
# holding registers
if column == Column.HOLDING_COLUMN and role == Qt.DisplayRole:
reg = self.m_holdingRegisters[row]
return f"0x{reg:x}"
return None
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return None
if orientation == Qt.Horizontal:
if section == Column.NUM_COLUMN:
return "#"
if section == Column.COILS_COLUMN:
return "Coils "
if section == Column.HOLDING_COLUMN:
return "Holding Registers"
return None
def setData(self, index, value, role):
row = index.row()
column = index.column()
if not index.isValid() or row >= Column.ROW_COUNT or column >= Column.COLUMN_COUNT:
return False
assert self.m_coils.size() == Column.ROW_COUNT
assert len(self.m_holdingRegisters) == Column.ROW_COUNT
if column == Column.COILS_COLUMN and role == Qt.CheckStateRole: # coils
s = Qt.CheckState(int(value))
if s == Qt.Checked:
self.m_coils.setBit(row)
else:
self.m_coils.clearBit(row)
self.dataChanged.emit(index, index)
return True
if column == Column.HOLDING_COLUMN and role == Qt.EditRole: # holding registers
base = 16 if value.startswith("0x") else 10
self.m_holdingRegisters[row] = int(value, base=base)
self.dataChanged.emit(index, index)
return True
return False
def flags(self, index):
row = index.row()
column = index.column()
flags = super().flags(index)
if not index.isValid() or row >= Column.ROW_COUNT or column >= Column.COLUMN_COUNT:
return flags
if row < self.m_address or row >= (self.m_address + self.m_number):
flags &= ~Qt.ItemIsEnabled
if column == Column.COILS_COLUMN: # coils
return flags | Qt.ItemIsUserCheckable
if column == Column.HOLDING_COLUMN: # holding registers
return flags | Qt.ItemIsEditable
return flags
@Slot(int)
def set_start_address(self, address):
self.m_address = address
self.update_viewport.emit()
@Slot(str)
def set_number_of_values(self, number):
self.m_number = int(number)
self.update_viewport.emit()
<RCC>
<qresource prefix="/">
<file>images/application-exit.png</file>
<file>images/connect.png</file>
<file>images/disconnect.png</file>
<file>images/settings.png</file>
</qresource>
</RCC>