Modbus客户端示例

该示例充当Modbus客户端,分别通过串行线路和TCP发送Modbus请求。显示的对话框允许定义标准请求并显示传入的响应。

该示例必须与Modbus服务器示例或通过TCP或串行端口连接的其他Modbus设备一起使用。

下载 这个 示例

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""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
from __future__ import annotations

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,
                                 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:50200")

        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(self.onReadReady)
            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 = self.sender()
        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(self._write_finished)
        else:
            message = "Write error: " + self._modbus_device.errorString()
            self.statusBar().showMessage(message, 5000)

    @Slot()
    def _write_finished(self):
        reply = self.sender()
        if not reply:
            return
        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(self.onReadReady)
            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>601</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::Orientation::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::Orientation::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>601</width>
     <height>26</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 __future__ import annotations

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>259</width>
    <height>321</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::Orientation::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 __future__ import annotations

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.ItemDataRole.DisplayRole:
            return f"{row}"

        if column == Column.COILS_COLUMN and role == Qt.ItemDataRole.CheckStateRole:  # coils
            return Qt.Checked if self.m_coils[row] else Qt.Unchecked

        # holding registers
        if column == Column.HOLDING_COLUMN and role == Qt.ItemDataRole.DisplayRole:
            reg = self.m_holdingRegisters[row]
            return f"0x{reg:x}"
        return None

    def headerData(self, section, orientation, role):
        if role != Qt.ItemDataRole.DisplayRole:
            return None

        if orientation == Qt.Orientation.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.ItemDataRole.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.ItemDataRole.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>