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.

Download this example

# 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>&amp;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&amp;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>&amp;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>&amp;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>&amp;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>&amp;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>