CAN Bus example

The example sends and receives CAN bus frames. The example sends and receives CAN bus frames. Incoming frames are ordered according to their type. A connect dialog is provided to adjust the CAN Bus connection parameters.

Download this example

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

import sys

from PySide6.QtCore import QCoreApplication, QLoggingCategory
from PySide6.QtWidgets import QApplication
from mainwindow import MainWindow

"""PySide6 port of the CAN example from Qt v6.x"""


if __name__ == "__main__":
    QLoggingCategory.setFilterRules("qt.canbus* = 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 PySide6.QtWidgets import QComboBox
from PySide6.QtGui import QIntValidator
from PySide6.QtCore import Slot


class BitRateBox(QComboBox):

    def __init__(self, parent):
        super().__init__(parent)
        self.m_isFlexibleDataRateEnabled = False
        self.m_customSpeedValidator = None
        self.m_customSpeedValidator = QIntValidator(0, 1000000, self)
        self.fill_bit_rates()
        self.currentIndexChanged.connect(self.check_custom_speed_policy)

    def bit_rate(self):
        index = self.currentIndex()
        if index == self.count() - 1:
            return int(self.currentText)
        return int(self.itemData(index))

    def is_flexible_data_rate_enabled(self):
        return self.m_isFlexibleDataRateEnabled

    def set_flexible_date_rate_enabled(self, enabled):
        self.m_isFlexibleDataRateEnabled = enabled
        self.m_customSpeedValidator.setTop(10000000 if enabled else 1000000)
        self.fill_bit_rates()

    @Slot(int)
    def check_custom_speed_policy(self, idx):
        is_custom_speed = not self.itemData(idx)
        self.setEditable(is_custom_speed)
        if is_custom_speed:
            self.clearEditText()
            self.lineEdit().setValidator(self.m_customSpeedValidator)

    def fill_bit_rates(self):
        rates = [10000, 20000, 50000, 100000, 125000, 250000, 500000,
                 800000, 1000000]
        data_rates = [2000000, 4000000, 8000000]

        self.clear()
        for rate in rates:
            self.addItem(f"{rate}", rate)

        if self.is_flexible_data_rate_enabled():
            for rate in data_rates:
                self.addItem(f"{rate}", rate)

        self.addItem("Custom")
        self.setCurrentIndex(6)  # default is 500000 bits/sec
# 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 Qt
from PySide6.QtWidgets import QGroupBox

from ui_canbusdeviceinfobox import Ui_CanBusDeviceInfoBox


def _set_readonly_and_compact(box):
    box.setAttribute(Qt.WA_TransparentForMouseEvents)
    box.setFocusPolicy(Qt.NoFocus)
    box.setStyleSheet("margin-top:0; margin-bottom:0;")


class CanBusDeviceInfoBox(QGroupBox):

    def __init__(self, parent):
        super().__init__(parent)
        self.m_ui = Ui_CanBusDeviceInfoBox()
        self.m_ui.setupUi(self)
        _set_readonly_and_compact(self.m_ui.isVirtual)
        _set_readonly_and_compact(self.m_ui.isFlexibleDataRateCapable)

    def clear(self):
        self.m_ui.pluginLabel.clear()
        self.m_ui.nameLabel.clear()
        self.m_ui.descriptionLabel.clear()
        self.m_ui.serialNumberLabel.clear()
        self.m_ui.aliasLabel.clear()
        self.m_ui.channelLabel.clear()
        self.m_ui.isVirtual.setChecked(False)
        self.m_ui.isFlexibleDataRateCapable.setChecked(False)

    def set_device_info(self, info):
        self.m_ui.pluginLabel.setText(f"Plugin: {info.plugin()}")
        self.m_ui.nameLabel.setText(f"Name: {info.name()}")
        self.m_ui.descriptionLabel.setText(info.description())
        serial_number = info.serialNumber()
        if not serial_number:
            serial_number = "n/a"
        self.m_ui.serialNumberLabel.setText(f"Serial: {serial_number}")
        alias = info.alias()
        if not alias:
            alias = "n/a"
        self.m_ui.aliasLabel.setText(f"Alias: {alias}")
        self.m_ui.channelLabel.setText(f"Channel: {info.channel()}")
        self.m_ui.isVirtual.setChecked(info.isVirtual())
        self.m_ui.isFlexibleDataRateCapable.setChecked(info.hasFlexibleDataRate())
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>CanBusDeviceInfoBox</class>
 <widget class="QGroupBox" name="CanBusDeviceInfoBox">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>319</width>
    <height>257</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>CAN Interface Properties</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QLabel" name="pluginLabel">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="nameLabel">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="descriptionLabel">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="serialNumberLabel">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="aliasLabel">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QLabel" name="channelLabel">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QCheckBox" name="isFlexibleDataRateCapable">
     <property name="enabled">
      <bool>true</bool>
     </property>
     <property name="text">
      <string>Flexible Data Rate</string>
     </property>
     <property name="checkable">
      <bool>true</bool>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QCheckBox" name="isVirtual">
     <property name="text">
      <string>Virtual</string>
     </property>
     <property name="checkable">
      <bool>true</bool>
     </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 PySide6.QtWidgets import QDialog

from ui_canbusdeviceinfodialog import Ui_CanBusDeviceInfoDialog


class CanBusDeviceInfoDialog(QDialog):

    def __init__(self, info, parent):
        super().__init__(parent)
        self.m_ui = Ui_CanBusDeviceInfoDialog()
        self.m_ui.setupUi(self)
        self.m_ui.deviceInfoBox.set_device_info(info)
        self.m_ui.okButton.pressed.connect(self.close)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>CanBusDeviceInfoDialog</class>
 <widget class="QDialog" name="CanBusDeviceInfoDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>237</width>
    <height>225</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>CAN Interface Properties</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="CanBusDeviceInfoBox" name="deviceInfoBox">
     <property name="enabled">
      <bool>true</bool>
     </property>
     <property name="title">
      <string>CAN Interface Properties</string>
     </property>
    </widget>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout">
     <item>
      <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>
      <widget class="QPushButton" name="okButton">
       <property name="text">
        <string>Ok</string>
       </property>
       <property name="default">
        <bool>true</bool>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <customwidgets>
  <customwidget>
   <class>CanBusDeviceInfoBox</class>
   <extends>QGroupBox</extends>
   <header location="global">canbusdeviceinfobox.h</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <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 QSettings, Qt, Slot
from PySide6.QtGui import QIntValidator
from PySide6.QtWidgets import QDialog
from PySide6.QtSerialBus import QCanBus, QCanBusDevice

from ui_connectdialog import Ui_ConnectDialog


class Settings():
    def __init__(self):
        self.plugin_name = ""
        self.device_interface_name = ""
        self.configurations = []
        self.use_configuration_enabled = False
        self.use_model_ring_buffer = True
        self.model_ring_buffer_size = 1000
        self.use_autoscroll = False


class ConnectDialog(QDialog):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_ui = Ui_ConnectDialog()
        self.m_currentSettings = Settings()
        self.m_interfaces = []
        self.m_settings = QSettings("QtProject", "CAN example")
        self.m_ui.setupUi(self)

        self.m_ui.errorFilterEdit.setValidator(QIntValidator(0, 0x1FFFFFFF, self))

        self.m_ui.loopbackBox.addItem("unspecified")
        self.m_ui.loopbackBox.addItem("False", False)
        self.m_ui.loopbackBox.addItem("True", True)

        self.m_ui.receiveOwnBox.addItem("unspecified")
        self.m_ui.receiveOwnBox.addItem("False", False)
        self.m_ui.receiveOwnBox.addItem("True", True)

        self.m_ui.canFdBox.addItem("False", False)
        self.m_ui.canFdBox.addItem("True", True)

        self.m_ui.dataBitrateBox.set_flexible_date_rate_enabled(True)

        self.m_ui.okButton.clicked.connect(self.ok)
        self.m_ui.cancelButton.clicked.connect(self.cancel)
        self.m_ui.useConfigurationBox.toggled.connect(self.m_ui.configurationBox.setEnabled)
        self.m_ui.pluginListBox.currentTextChanged.connect(self.plugin_changed)
        self.m_ui.interfaceListBox.currentTextChanged.connect(self.interface_changed)
        self.m_ui.ringBufferBox.checkStateChanged.connect(self._ring_buffer_changed)

        self.m_ui.rawFilterEdit.hide()
        self.m_ui.rawFilterLabel.hide()

        self.m_ui.pluginListBox.addItems(QCanBus.instance().plugins())

        self.restore_settings()

    @Slot(int)
    def _ring_buffer_changed(self, state):
        self.m_ui.ringBufferLimitBox.setEnabled(state == Qt.CheckState.Checked)

    def settings(self):
        return self.m_currentSettings

    def save_settings(self):
        qs = self.m_settings
        cur = self.m_currentSettings
        qs.beginGroup("LastSettings")
        qs.setValue("PluginName", self.m_currentSettings.plugin_name)
        qs.setValue("DeviceInterfaceName", cur.device_interface_name)
        qs.setValue("UseAutoscroll", cur.use_autoscroll)
        qs.setValue("UseRingBuffer", cur.use_model_ring_buffer)
        qs.setValue("RingBufferSize", cur.model_ring_buffer_size)
        qs.setValue("UseCustomConfiguration", cur.use_configuration_enabled)

        if cur.use_configuration_enabled:
            qs.setValue("Loopback",
                        self.configuration_value(QCanBusDevice.LoopbackKey))
            qs.setValue("ReceiveOwn",
                        self.configuration_value(QCanBusDevice.ReceiveOwnKey))
            qs.setValue("ErrorFilter",
                        self.configuration_value(QCanBusDevice.ErrorFilterKey))
            qs.setValue("BitRate",
                        self.configuration_value(QCanBusDevice.BitRateKey))
            qs.setValue("CanFd",
                        self.configuration_value(QCanBusDevice.CanFdKey))
            qs.setValue("DataBitRate",
                        self.configuration_value(QCanBusDevice.DataBitRateKey))
        qs.endGroup()

    def restore_settings(self):
        qs = self.m_settings
        cur = self.m_currentSettings
        qs.beginGroup("LastSettings")
        cur.plugin_name = qs.value("PluginName", "", str)
        cur.device_interface_name = qs.value("DeviceInterfaceName", "", str)
        cur.use_autoscroll = qs.value("UseAutoscroll", False, bool)
        cur.use_model_ring_buffer = qs.value("UseRingBuffer", False, bool)
        cur.model_ring_buffer_size = qs.value("RingBufferSize", 0, int)
        cur.use_configuration_enabled = qs.value("UseCustomConfiguration", False, bool)

        self.revert_settings()

        if cur.use_configuration_enabled:
            self.m_ui.loopbackBox.setCurrentText(qs.value("Loopback"))
            self.m_ui.receiveOwnBox.setCurrentText(qs.value("ReceiveOwn"))
            self.m_ui.errorFilterEdit.setText(qs.value("ErrorFilter"))
            self.m_ui.bitrateBox.setCurrentText(qs.value("BitRate"))
            self.m_ui.canFdBox.setCurrentText(qs.value("CanFd"))
            self.m_ui.dataBitrateBox.setCurrentText(qs.value("DataBitRate"))

        qs.endGroup()
        self.update_settings()

    @Slot(str)
    def plugin_changed(self, plugin):
        self.m_ui.interfaceListBox.clear()
        interfaces, error_string = QCanBus.instance().availableDevices(plugin)
        self.m_interfaces = interfaces
        for info in self.m_interfaces:
            self.m_ui.interfaceListBox.addItem(info.name())

    @Slot(str)
    def interface_changed(self, interface):
        for info in self.m_interfaces:
            if interface == info.name():
                self.m_ui.deviceInfoBox.set_device_info(info)
                return
        self.m_ui.deviceInfoBox.clear()

    @Slot()
    def ok(self):
        self.update_settings()
        self.save_settings()
        self.accept()

    @Slot()
    def cancel(self):
        self.revert_settings()
        self.reject()

    def configuration_value(self, key):
        result = None
        for k, v in self.m_currentSettings.configurations:
            if k == key:
                result = v
                break
        if not result and (key == QCanBusDevice.LoopbackKey or key == QCanBusDevice.ReceiveOwnKey):
            return "unspecified"
        return str(result)

    def revert_settings(self):
        self.m_ui.pluginListBox.setCurrentText(self.m_currentSettings.plugin_name)
        self.m_ui.interfaceListBox.setCurrentText(self.m_currentSettings.device_interface_name)
        self.m_ui.useConfigurationBox.setChecked(self.m_currentSettings.use_configuration_enabled)

        self.m_ui.ringBufferBox.setChecked(self.m_currentSettings.use_model_ring_buffer)
        self.m_ui.ringBufferLimitBox.setValue(self.m_currentSettings.model_ring_buffer_size)
        self.m_ui.autoscrollBox.setChecked(self.m_currentSettings.use_autoscroll)

        value = self.configuration_value(QCanBusDevice.LoopbackKey)
        self.m_ui.loopbackBox.setCurrentText(value)

        value = self.configuration_value(QCanBusDevice.ReceiveOwnKey)
        self.m_ui.receiveOwnBox.setCurrentText(value)

        value = self.configuration_value(QCanBusDevice.ErrorFilterKey)
        self.m_ui.errorFilterEdit.setText(value)

        value = self.configuration_value(QCanBusDevice.BitRateKey)
        self.m_ui.bitrateBox.setCurrentText(value)

        value = self.configuration_value(QCanBusDevice.CanFdKey)
        self.m_ui.canFdBox.setCurrentText(value)

        value = self.configuration_value(QCanBusDevice.DataBitRateKey)
        self.m_ui.dataBitrateBox.setCurrentText(value)

    def update_settings(self):
        self.m_currentSettings.plugin_name = self.m_ui.pluginListBox.currentText()
        self.m_currentSettings.device_interface_name = self.m_ui.interfaceListBox.currentText()
        self.m_currentSettings.use_configuration_enabled = self.m_ui.useConfigurationBox.isChecked()

        self.m_currentSettings.use_model_ring_buffer = self.m_ui.ringBufferBox.isChecked()
        self.m_currentSettings.model_ring_buffer_size = self.m_ui.ringBufferLimitBox.value()
        self.m_currentSettings.use_autoscroll = self.m_ui.autoscrollBox.isChecked()

        if self.m_currentSettings.use_configuration_enabled:
            self.m_currentSettings.configurations.clear()
            # process LoopBack
            if self.m_ui.loopbackBox.currentIndex() != 0:
                item = (QCanBusDevice.LoopbackKey, self.m_ui.loopbackBox.currentData())
                self.m_currentSettings.configurations.append(item)

            # process ReceiveOwnKey
            if self.m_ui.receiveOwnBox.currentIndex() != 0:
                item = (QCanBusDevice.ReceiveOwnKey, self.m_ui.receiveOwnBox.currentData())
                self.m_currentSettings.configurations.append(item)

            # process error filter
            error_filter = self.m_ui.errorFilterEdit.text()
            if error_filter:
                ok = False
                try:
                    int(error_filter)  # check if value contains a valid integer
                    ok = True
                except ValueError:
                    pass
                if ok:
                    item = (QCanBusDevice.ErrorFilterKey, error_filter)
                    self.m_currentSettings.configurations.append(item)

            # process raw filter list
            if self.m_ui.rawFilterEdit.text():
                pass  # TODO current ui not sufficient to reflect this param

            # process bitrate
            bitrate = self.m_ui.bitrateBox.bit_rate()
            if bitrate > 0:
                item = (QCanBusDevice.BitRateKey, bitrate)
                self.m_currentSettings.configurations.append(item)

            # process CAN FD setting
            fd_item = (QCanBusDevice.CanFdKey, self.m_ui.canFdBox.currentData())
            self.m_currentSettings.configurations.append(fd_item)

            # process data bitrate
            data_bitrate = self.m_ui.dataBitrateBox.bit_rate()
            if data_bitrate > 0:
                item = (QCanBusDevice.DataBitRateKey, data_bitrate)
                self.m_currentSettings.configurations.append(item)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>ConnectDialog</class>
 <widget class="QDialog" name="ConnectDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>542</width>
    <height>558</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Connect</string>
  </property>
  <layout class="QGridLayout" name="gridLayout_6">
   <item row="0" column="0">
    <layout class="QGridLayout" name="gridLayout_5">
     <item row="0" column="0">
      <widget class="QGroupBox" name="selectPluginBox">
       <property name="title">
        <string>Select CAN plugin</string>
       </property>
       <layout class="QGridLayout" name="gridLayout">
        <item row="0" column="0">
         <widget class="QComboBox" name="pluginListBox"/>
        </item>
       </layout>
      </widget>
     </item>
     <item row="4" column="0" colspan="2">
      <widget class="QGroupBox" name="groupBox">
       <property name="title">
        <string>GUI Settings</string>
       </property>
       <layout class="QGridLayout" name="gridLayout_2">
        <item row="0" column="0">
         <layout class="QVBoxLayout" name="verticalLayout_2">
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout_2">
            <item>
             <widget class="QCheckBox" name="ringBufferBox">
              <property name="toolTip">
               <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use ring buffer in table view model&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
              </property>
              <property name="text">
               <string>Use ring buffer</string>
              </property>
              <property name="checked">
               <bool>true</bool>
              </property>
             </widget>
            </item>
            <item>
             <widget class="QSpinBox" name="ringBufferLimitBox">
              <property name="toolTip">
               <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Limit of ring buffer in table view model&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
              </property>
              <property name="minimum">
               <number>10</number>
              </property>
              <property name="maximum">
               <number>10000000</number>
              </property>
              <property name="singleStep">
               <number>10</number>
              </property>
              <property name="stepType">
               <enum>QAbstractSpinBox::StepType::AdaptiveDecimalStepType</enum>
              </property>
              <property name="value">
               <number>1000</number>
              </property>
             </widget>
            </item>
           </layout>
          </item>
          <item>
           <widget class="QCheckBox" name="autoscrollBox">
            <property name="toolTip">
             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Scroll to bottom table view on each portion of received frames&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
            </property>
            <property name="text">
             <string>Autoscroll</string>
            </property>
           </widget>
          </item>
         </layout>
        </item>
       </layout>
      </widget>
     </item>
     <item row="3" column="0">
      <widget class="QCheckBox" name="useConfigurationBox">
       <property name="text">
        <string>Custom configuration</string>
       </property>
      </widget>
     </item>
     <item row="0" column="1" rowspan="4">
      <widget class="QGroupBox" name="configurationBox">
       <property name="enabled">
        <bool>false</bool>
       </property>
       <property name="title">
        <string>Specify Configuration</string>
       </property>
       <layout class="QGridLayout" name="gridLayout_4">
        <item row="0" column="0">
         <widget class="QLabel" name="rawFilterLabel">
          <property name="text">
           <string>RAW Filter</string>
          </property>
         </widget>
        </item>
        <item row="0" column="1">
         <widget class="QLineEdit" name="rawFilterEdit">
          <property name="alignment">
           <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
          </property>
         </widget>
        </item>
        <item row="1" column="0">
         <widget class="QLabel" name="errorFilterLabel">
          <property name="text">
           <string>Error Filter</string>
          </property>
         </widget>
        </item>
        <item row="1" column="1">
         <widget class="QLineEdit" name="errorFilterEdit">
          <property name="alignment">
           <set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
          </property>
          <property name="placeholderText">
           <string>FrameError bits</string>
          </property>
         </widget>
        </item>
        <item row="2" column="0">
         <widget class="QLabel" name="loopbackLabel">
          <property name="text">
           <string>Loopback</string>
          </property>
         </widget>
        </item>
        <item row="2" column="1">
         <widget class="QComboBox" name="loopbackBox"/>
        </item>
        <item row="3" column="0">
         <widget class="QLabel" name="receiveOwnLabel">
          <property name="text">
           <string>Receive Own</string>
          </property>
         </widget>
        </item>
        <item row="3" column="1">
         <widget class="QComboBox" name="receiveOwnBox"/>
        </item>
        <item row="4" column="0">
         <widget class="QLabel" name="bitrateLabel">
          <property name="text">
           <string>Bitrate</string>
          </property>
         </widget>
        </item>
        <item row="4" column="1">
         <widget class="BitRateBox" name="bitrateBox"/>
        </item>
        <item row="5" column="0">
         <widget class="QLabel" name="canFdLabel">
          <property name="text">
           <string>CAN FD</string>
          </property>
         </widget>
        </item>
        <item row="5" column="1">
         <widget class="QComboBox" name="canFdBox"/>
        </item>
        <item row="6" column="0">
         <widget class="QLabel" name="dataBitrateLabel">
          <property name="text">
           <string>Data Bitrate</string>
          </property>
         </widget>
        </item>
        <item row="6" column="1">
         <widget class="BitRateBox" name="dataBitrateBox"/>
        </item>
       </layout>
      </widget>
     </item>
     <item row="5" column="0" colspan="2">
      <layout class="QHBoxLayout" name="horizontalLayout">
       <item>
        <spacer name="horizontalSpacer">
         <property name="orientation">
          <enum>Qt::Orientation::Horizontal</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>96</width>
           <height>20</height>
          </size>
         </property>
        </spacer>
       </item>
       <item>
        <widget class="QPushButton" name="cancelButton">
         <property name="text">
          <string>Cancel</string>
         </property>
         <property name="autoDefault">
          <bool>false</bool>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QPushButton" name="okButton">
         <property name="text">
          <string>OK</string>
         </property>
         <property name="autoDefault">
          <bool>false</bool>
         </property>
         <property name="default">
          <bool>true</bool>
         </property>
        </widget>
       </item>
      </layout>
     </item>
     <item row="1" column="0">
      <widget class="QGroupBox" name="specifyInterfaceNameBox">
       <property name="title">
        <string>Specify CAN interface name</string>
       </property>
       <layout class="QGridLayout" name="gridLayout_3">
        <item row="0" column="0">
         <widget class="QComboBox" name="interfaceListBox">
          <property name="editable">
           <bool>true</bool>
          </property>
         </widget>
        </item>
       </layout>
      </widget>
     </item>
     <item row="2" column="0">
      <widget class="CanBusDeviceInfoBox" name="deviceInfoBox">
       <property name="enabled">
        <bool>true</bool>
       </property>
       <property name="title">
        <string>CAN Interface Properties</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <customwidgets>
  <customwidget>
   <class>BitRateBox</class>
   <extends>QComboBox</extends>
   <header>bitratebox.h</header>
  </customwidget>
  <customwidget>
   <class>CanBusDeviceInfoBox</class>
   <extends>QGroupBox</extends>
   <header location="global">canbusdeviceinfobox.h</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <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 QTimer, QUrl, Slot
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import QLabel, QMainWindow
from PySide6.QtSerialBus import QCanBus, QCanBusDevice, QCanBusFrame

from connectdialog import ConnectDialog
from canbusdeviceinfodialog import CanBusDeviceInfoDialog
from ui_mainwindow import Ui_MainWindow
from receivedframesmodel import ReceivedFramesModel


def frame_flags(frame):
    result = " --- "
    if frame.hasBitrateSwitch():
        result[1] = 'B'
    if frame.hasErrorStateIndicator():
        result[2] = 'E'
    if frame.hasLocalEcho():
        result[3] = 'L'
    return result


def show_help():
    url = "http://doc.qt.io/qt-6/qtcanbus-backends.html#can-bus-plugins"
    QDesktopServices.openUrl(QUrl(url))


class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_ui = Ui_MainWindow()
        self.m_number_frames_written = 0
        self.m_number_frames_received = 0
        self.m_written = None
        self.m_received = None
        self.m_can_device = None

        self.m_busStatusTimer = QTimer(self)

        self.m_ui.setupUi(self)
        self.m_connect_dialog = ConnectDialog(self)

        self.m_status = QLabel()
        self.m_ui.statusBar.addPermanentWidget(self.m_status)
        self.m_written = QLabel()
        self.m_ui.statusBar.addWidget(self.m_written)
        self.m_received = QLabel()
        self.m_ui.statusBar.addWidget(self.m_received)

        self.m_model = ReceivedFramesModel(self)
        self.m_model.set_queue_limit(1000)
        self.m_ui.receivedFramesView.set_model(self.m_model)

        self.init_actions_connections()
        QTimer.singleShot(50, self.m_connect_dialog.show)

        self.m_busStatusTimer.timeout.connect(self.bus_status)
        self.m_appendTimer = QTimer(self)
        self.m_appendTimer.timeout.connect(self.onAppendFramesTimeout)
        self.m_appendTimer.start(350)

    def init_actions_connections(self):
        self.m_ui.actionDisconnect.setEnabled(False)
        self.m_ui.actionDeviceInformation.setEnabled(False)
        self.m_ui.sendFrameBox.setEnabled(False)

        self.m_ui.sendFrameBox.send_frame.connect(self.send_frame)
        self.m_ui.actionConnect.triggered.connect(self._action_connect)
        self.m_connect_dialog.accepted.connect(self.connect_device)
        self.m_ui.actionDisconnect.triggered.connect(self.disconnect_device)
        self.m_ui.actionResetController.triggered.connect(self._reset_controller)
        self.m_ui.actionQuit.triggered.connect(self.close)
        self.m_ui.actionAboutQt.triggered.connect(qApp.aboutQt)  # noqa: F821
        self.m_ui.actionClearLog.triggered.connect(self.m_model.clear)
        self.m_ui.actionPluginDocumentation.triggered.connect(show_help)
        self.m_ui.actionDeviceInformation.triggered.connect(self._action_device_information)

    @Slot()
    def _action_connect(self):
        if self.m_can_device:
            self.m_can_device.deleteLater()
            self.m_can_device = None
        self.m_connect_dialog.show()

    @Slot()
    def _reset_controller(self):
        self.m_can_device.resetController()

    @Slot()
    def _action_device_information(self):
        info = self.m_can_device.deviceInfo()
        dialog = CanBusDeviceInfoDialog(info, self)
        dialog.exec()

    @Slot(QCanBusDevice.CanBusError)
    def process_errors(self, error):
        if error != QCanBusDevice.NoError:
            self.m_status.setText(self.m_can_device.errorString())

    @Slot()
    def connect_device(self):
        p = self.m_connect_dialog.settings()
        if p.use_model_ring_buffer:
            self.m_model.set_queue_limit(p.model_ring_buffer_size)
        else:
            self.m_model.set_queue_limit(0)

        device, error_string = QCanBus.instance().createDevice(
            p.plugin_name, p.device_interface_name)
        if not device:
            self.m_status.setText(
                f"Error creating device '{p.plugin_name}', reason: '{error_string}'")
            return

        self.m_number_frames_written = 0
        self.m_can_device = device
        self.m_can_device.errorOccurred.connect(self.process_errors)
        self.m_can_device.framesReceived.connect(self.process_received_frames)
        self.m_can_device.framesWritten.connect(self.process_frames_written)

        if p.use_configuration_enabled:
            for k, v in p.configurations:
                self.m_can_device.setConfigurationParameter(k, v)

        if not self.m_can_device.connectDevice():
            e = self.m_can_device.errorString()
            self.m_status.setText(f"Connection error: {e}")
            self.m_can_device = None
        else:
            self.m_ui.actionConnect.setEnabled(False)
            self.m_ui.actionDisconnect.setEnabled(True)
            self.m_ui.actionDeviceInformation.setEnabled(True)
            self.m_ui.sendFrameBox.setEnabled(True)
            config_bit_rate = self.m_can_device.configurationParameter(QCanBusDevice.BitRateKey)
            if config_bit_rate is not None and config_bit_rate > 0:
                is_can_fd = bool(self.m_can_device.configurationParameter(QCanBusDevice.CanFdKey))
                config_data_bit_rate = self.m_can_device.configurationParameter(
                    QCanBusDevice.DataBitRateKey)
                bit_rate = config_bit_rate / 1000
                if is_can_fd and config_data_bit_rate > 0:
                    data_bit_rate = config_data_bit_rate / 1000
                    m = (f"Plugin: {p.plugin_name}, connected to {p.device_interface_name} "
                         f"at {bit_rate} / {data_bit_rate} kBit/s")
                    self.m_status.setText(m)
                else:
                    m = (f"Plugin: {p.plugin_name}, connected to {p.device_interface_name} "
                         f"at {bit_rate} kBit/s")
                    self.m_status.setText(m)

            else:
                self.m_status.setText(
                    f"Plugin: {p.plugin_name}, connected to {p.device_interface_name}")

            if self.m_can_device.hasBusStatus():
                self.m_busStatusTimer.start(2000)
            else:
                self.m_ui.busStatus.setText("No CAN bus status available.")

    def bus_status(self):
        if not self.m_can_device or not self.m_can_device.hasBusStatus():
            self.m_ui.busStatus.setText("No CAN bus status available.")
            self.m_busStatusTimer.stop()
            return

        state = self.m_can_device.busStatus()
        if state == QCanBusDevice.CanBusStatus.Good:
            self.m_ui.busStatus.setText("CAN bus status: Good.")
        elif state == QCanBusDevice.CanBusStatus.Warning:
            self.m_ui.busStatus.setText("CAN bus status: Warning.")
        elif state == QCanBusDevice.CanBusStatus.Error:
            self.m_ui.busStatus.setText("CAN bus status: Error.")
        elif state == QCanBusDevice.CanBusStatus.BusOff:
            self.m_ui.busStatus.setText("CAN bus status: Bus Off.")
        else:
            self.m_ui.busStatus.setText("CAN bus status: Unknown.")

    @Slot()
    def disconnect_device(self):
        if not self.m_can_device:
            return
        self.m_busStatusTimer.stop()
        self.m_can_device.disconnectDevice()
        self.m_ui.actionConnect.setEnabled(True)
        self.m_ui.actionDisconnect.setEnabled(False)
        self.m_ui.actionDeviceInformation.setEnabled(False)
        self.m_ui.sendFrameBox.setEnabled(False)
        self.m_status.setText("Disconnected")

    @Slot(int)
    def process_frames_written(self, count):
        self.m_number_frames_written += count
        self.m_written.setText(f"{self.m_number_frames_written} frames written")

    def closeEvent(self, event):
        self.m_connect_dialog.close()
        event.accept()

    @Slot()
    def process_received_frames(self):
        if not self.m_can_device:
            return
        while self.m_can_device.framesAvailable():
            self.m_number_frames_received = self.m_number_frames_received + 1
            frame = self.m_can_device.readFrame()
            data = ""
            if frame.frameType() == QCanBusFrame.ErrorFrame:
                data = self.m_can_device.interpretErrorFrame(frame)
            else:
                data = frame.payload().toHex(' ').toUpper()

            secs = frame.timeStamp().seconds()
            microsecs = frame.timeStamp().microSeconds() / 100
            time = f"{secs:>10}.{microsecs:0>4}"
            flags = frame_flags(frame)

            id = f"{frame.frameId():x}"
            dlc = f"{frame.payload().size()}"
            frame = [f"{self.m_number_frames_received}", time, flags, id, dlc, data]
            self.m_model.append_frame(frame)

    @Slot(QCanBusFrame)
    def send_frame(self, frame):
        if self.m_can_device:
            self.m_can_device.writeFrame(frame)

    @Slot()
    def onAppendFramesTimeout(self):
        if not self.m_can_device:
            return
        if self.m_model.need_update():
            self.m_model.update()
            if self.m_connect_dialog.settings().use_autoscroll:
                self.m_ui.receivedFramesView.scrollToBottom()
            self.m_received.setText(f"{self.m_number_frames_received} frames received")
<?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>634</width>
    <height>527</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>CAN Example</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="SendFrameBox" name="sendFrameBox">
      <property name="title">
       <string>Send CAN frame</string>
      </property>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="receivedMessagesBox">
      <property name="title">
       <string>Received CAN messages</string>
      </property>
      <layout class="QGridLayout" name="gridLayout">
       <item row="0" column="0">
        <layout class="QVBoxLayout" name="verticalLayout_2">
         <item>
          <widget class="ReceivedFramesView" name="receivedFramesView">
           <property name="editTriggers">
            <set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
           </property>
           <property name="showDropIndicator" stdset="0">
            <bool>false</bool>
           </property>
           <property name="dragDropOverwriteMode">
            <bool>false</bool>
           </property>
           <property name="selectionBehavior">
            <enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
           </property>
          </widget>
         </item>
         <item>
          <layout class="QHBoxLayout" name="horizontalLayout">
           <item>
            <widget class="QLabel" name="busStatus">
             <property name="text">
              <string/>
             </property>
            </widget>
           </item>
           <item>
            <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>
          </layout>
         </item>
        </layout>
       </item>
      </layout>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>634</width>
     <height>26</height>
    </rect>
   </property>
   <widget class="QMenu" name="menuCalls">
    <property name="title">
     <string>&amp;Calls</string>
    </property>
    <addaction name="actionConnect"/>
    <addaction name="actionDisconnect"/>
    <addaction name="actionDeviceInformation"/>
    <addaction name="separator"/>
    <addaction name="actionResetController"/>
    <addaction name="separator"/>
    <addaction name="actionClearLog"/>
    <addaction name="separator"/>
    <addaction name="actionQuit"/>
   </widget>
   <widget class="QMenu" name="menuHelp">
    <property name="title">
     <string>&amp;Help</string>
    </property>
    <addaction name="actionPluginDocumentation"/>
    <addaction name="actionAboutQt"/>
   </widget>
   <addaction name="menuCalls"/>
   <addaction name="menuHelp"/>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
   <addaction name="actionConnect"/>
   <addaction name="actionDisconnect"/>
   <addaction name="separator"/>
   <addaction name="actionClearLog"/>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
  <action name="actionConnect">
   <property name="icon">
    <iconset resource="can.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="can.qrc">
     <normaloff>:/images/disconnect.png</normaloff>:/images/disconnect.png</iconset>
   </property>
   <property name="text">
    <string>&amp;Disconnect</string>
   </property>
  </action>
  <action name="actionQuit">
   <property name="icon">
    <iconset resource="can.qrc">
     <normaloff>:/images/application-exit.png</normaloff>:/images/application-exit.png</iconset>
   </property>
   <property name="text">
    <string>&amp;Quit</string>
   </property>
  </action>
  <action name="actionAboutQt">
   <property name="text">
    <string>&amp;About Qt</string>
   </property>
  </action>
  <action name="actionClearLog">
   <property name="icon">
    <iconset resource="can.qrc">
     <normaloff>:/images/clear.png</normaloff>:/images/clear.png</iconset>
   </property>
   <property name="text">
    <string>Clear &amp;Log</string>
   </property>
  </action>
  <action name="actionPluginDocumentation">
   <property name="text">
    <string>Plugin Documentation</string>
   </property>
   <property name="toolTip">
    <string>Open plugin documentation in Webbrowser</string>
   </property>
  </action>
  <action name="actionResetController">
   <property name="text">
    <string>&amp;Reset CAN Controller</string>
   </property>
   <property name="toolTip">
    <string>Reset CAN Controller</string>
   </property>
  </action>
  <action name="actionDeviceInformation">
   <property name="text">
    <string>Device &amp;Information...</string>
   </property>
  </action>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <customwidgets>
  <customwidget>
   <class>SendFrameBox</class>
   <extends>QGroupBox</extends>
   <header location="global">sendframebox.h</header>
   <container>1</container>
  </customwidget>
  <customwidget>
   <class>ReceivedFramesView</class>
   <extends>QTableView</extends>
   <header location="global">receivedframesview.h</header>
  </customwidget>
 </customwidgets>
 <resources>
  <include location="can.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 enum import IntEnum

from PySide6.QtCore import QAbstractTableModel, QModelIndex, QSize, Qt


class ReceivedFramesModelColumns(IntEnum):
    number = 0
    timestamp = 1
    flags = 2
    can_id = 3
    DLC = 4
    data = 5
    count = 6


clipboard_text_role = Qt.ItemDataRole.UserRole + 1


column_alignment = [Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
                    Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
                    Qt.AlignmentFlag.AlignCenter,
                    Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
                    Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
                    Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter]


class ReceivedFramesModel(QAbstractTableModel):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_framesQueue = []  # QQueue()
        self.m_framesAccumulator = []
        self.m_queueLimit = 0

    def remove_rows(self, row, count, parent):
        self.beginRemoveRows(parent, row, row + count - 1)
        self.m_framesQueue = self.m_framesQueue[0:row] + self.m_framesQueue[row + count:]
        self.endRemoveRows()
        return True

    def headerData(self, section, orientation, role):
        if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
            if section == ReceivedFramesModelColumns.number:
                return "#"
            if section == ReceivedFramesModelColumns.timestamp:
                return "Timestamp"
            if section == ReceivedFramesModelColumns.flags:
                return "Flags"
            if section == ReceivedFramesModelColumns.can_id:
                return "CAN-ID"
            if section == ReceivedFramesModelColumns.DLC:
                return "DLC"
            if section == ReceivedFramesModelColumns.data:
                return "Data"

        if role == Qt.ItemDataRole.SizeHintRole and orientation == Qt.Orientation.Horizontal:
            if section == ReceivedFramesModelColumns.number:
                return QSize(80, 25)
            if section == ReceivedFramesModelColumns.timestamp:
                return QSize(130, 25)
            if section == ReceivedFramesModelColumns.flags:
                return QSize(25, 25)
            if section == ReceivedFramesModelColumns.can_id:
                return QSize(50, 25)
            if section == ReceivedFramesModelColumns.DLC:
                return QSize(25, 25)
            if section == ReceivedFramesModelColumns.data:
                return QSize(200, 25)
        return None

    def data(self, index, role):
        if not self.m_framesQueue:
            return None
        row = index.row()
        column = index.column()
        if role == Qt.ItemDataRole.TextAlignmentRole:
            return column_alignment[index.column()]
        if role == Qt.ItemDataRole.AlignmentFlag.DisplayRole:
            return self.m_framesQueue[row][column]
        if role == clipboard_text_role:
            f = self.m_framesQueue[row][column]
            return f"[{f}]" if column == ReceivedFramesModelColumns.DLC else f
        return None

    def rowCount(self, parent=QModelIndex()):
        return 0 if parent.isValid() else len(self.m_framesQueue)

    def columnCount(self, parent=QModelIndex()):
        return 0 if parent.isValid() else ReceivedFramesModelColumns.count

    def append_frames(self, slvector):
        self.m_framesAccumulator.extend(slvector)

    def need_update(self):
        return self.m_framesAccumulator

    def update(self):
        if not self.m_framesAccumulator:
            return

        if self.m_queueLimit:
            self.append_frames_ring_buffer(self.m_framesAccumulator)
        else:
            self.append_frames_unlimited(self.m_framesAccumulator)
        self.m_framesAccumulator.clear()

    def append_frames_ring_buffer(self, slvector):
        slvector_len = len(slvector)
        row_count = self.rowCount()
        if self.m_queueLimit <= row_count + slvector_len:
            if slvector_len < self.m_queueLimit:
                self.remove_rows(0, row_count + slvector_len - self.m_queueLimit + 1)
            else:
                self.clear()

        self.beginInsertRows(QModelIndex(), row_count, row_count + slvector_len - 1)
        if slvector_len < self.m_queueLimit:
            self.m_framesQueue.extend(slvector)
        else:
            self.m_framesQueue.extend(slvector[slvector_len - self.m_queueLimit:])
        self.endInsertRows()

    def append_frame(self, slist):
        self.append_frames([slist])

    def append_frames_unlimited(self, slvector):
        row_count = self.rowCount()
        self.beginInsertRows(QModelIndex(), row_count, row_count + len(slvector) - 1)
        self.m_framesQueue.extend(slvector)
        self.endInsertRows()

    def clear(self):
        if self.m_framesQueue:
            self.beginResetModel()
            self.m_framesQueue.clear()
            self.endResetModel()

    def set_queue_limit(self, limit):
        self.m_queueLimit = limit
        frame_queue_len = len(self.m_framesQueue)
        if limit and frame_queue_len > limit:
            self.remove_rows(0, frame_queue_len - limit)
# 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 QPoint, Qt, Slot
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtWidgets import QApplication, QMenu, QTableView

from receivedframesmodel import clipboard_text_role


class ReceivedFramesView(QTableView):

    def __init__(self, parent):
        super().__init__(parent)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self._context_menu)

    @Slot(QPoint)
    def _context_menu(self, pos):
        context_menu = QMenu("Context menu", self)
        if self.selectedIndexes():
            copy_action = QAction("Copy", self)
            copy_action.triggered.connect(self.copy_row)
            context_menu.addAction(copy_action)

        select_all_action = QAction("Select all", self)
        select_all_action.triggered.connect(self.selectAll)
        context_menu.addAction(select_all_action)
        context_menu.exec(self.mapToGlobal(pos))

    def set_model(self, model):
        super().setModel(model)
        for i in range(0, model.columnCount()):
            size = model.headerData(i, Qt.Orientation.Horizontal, Qt.ItemDataRole.SizeHintRole)
            self.setColumnWidth(i, size.width())

    def keyPressEvent(self, event):
        if event.matches(QKeySequence.Copy):
            self.copy_row()
        elif event.matches(QKeySequence.SelectAll):
            self.selectAll()
        else:
            super().keyPressEvent(event)

    @Slot()
    def copy_row(self):
        clipboard = QApplication.clipboard()
        str_row = ""
        last_column = self.model().columnCount() - 1
        for index in self.selectedIndexes():
            str_row += index.data(clipboard_text_role) + " "
            if index.column() == last_column:
                str_row += "\n"
        clipboard.setText(str_row)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import re

from PySide6.QtGui import QValidator
from PySide6.QtCore import QByteArray, Signal, Slot
from PySide6.QtWidgets import QGroupBox
from PySide6.QtSerialBus import QCanBusFrame

from ui_sendframebox import Ui_SendFrameBox


THREE_HEX_DIGITS_PATTERN = re.compile("[0-9a-fA-F]{3}")
HEX_NUMBER_PATTERN = re.compile("^[0-9a-fA-F]+$")


MAX_STANDARD_ID = 0x7FF
MAX_EXTENDED_ID = 0x10000000
MAX_PAYLOAD = 8
MAX_PAYLOAD_FD = 64


def is_even_hex(input):
    return len(input.replace(" ", "")) % 2 == 0


def insert_space(string, pos):
    return string[0:pos] + " " + string[pos:]


# Formats a string of hex characters with a space between every byte
# Example: "012345" -> "01 23 45"
def format_hex_data(input):
    out = input.strip()
    while True:
        match = THREE_HEX_DIGITS_PATTERN.search(out)
        if match:
            out = insert_space(out, match.end(0) - 1)
        else:
            break
    return out.strip().upper()


class HexIntegerValidator(QValidator):

    def __init__(self, parent):
        super().__init__(parent)
        self.m_maximum = MAX_STANDARD_ID

    def validate(self, input, pos):
        result = QValidator.Intermediate
        if input:
            result = QValidator.Invalid
            try:
                value = int(input, base=16)
                if value < self.m_maximum:
                    result = QValidator.Acceptable
            except ValueError:
                pass
        return result

    def set_maximum(self, maximum):
        self.m_maximum = maximum


class HexStringValidator(QValidator):

    def __init__(self, parent):
        super().__init__(parent)
        self.m_maxLength = MAX_PAYLOAD

    def validate(self, input, pos):
        max_size = 2 * self.m_maxLength
        data = input.replace(" ", "")
        if not data:
            return QValidator.Intermediate

        # limit maximum size
        if len(data) > max_size:
            return QValidator.Invalid

        # check if all input is valid
        if not HEX_NUMBER_PATTERN.match(data):
            return QValidator.Invalid

        # insert a space after every two hex nibbles
        while True:
            match = THREE_HEX_DIGITS_PATTERN.search(input)
            if not match:
                break
            start = match.start(0)
            end = match.end()
            if pos == start + 1:
                # add one hex nibble before two - Abc
                input = insert_space(input, pos)
            elif pos == start + 2:
                # add hex nibble in the middle - aBc
                input = insert_space(input, end - 1)
                pos = end
            else:
                # add one hex nibble after two - abC
                input = insert_space(input, end - 1)
                pos = end + 1

        return (QValidator.Acceptable, input, pos)

    def set_max_length(self, maxLength):
        self.m_maxLength = maxLength


class SendFrameBox(QGroupBox):

    send_frame = Signal(QCanBusFrame)

    def __init__(self, parent):
        super().__init__(parent)
        self.m_ui = Ui_SendFrameBox()
        self.m_ui.setupUi(self)

        self.m_hexIntegerValidator = HexIntegerValidator(self)
        self.m_ui.frameIdEdit.setValidator(self.m_hexIntegerValidator)
        self.m_hexStringValidator = HexStringValidator(self)
        self.m_ui.payloadEdit.setValidator(self.m_hexStringValidator)

        self.m_ui.dataFrame.toggled.connect(self._data_frame)
        self.m_ui.remoteFrame.toggled.connect(self._remote_frame)
        self.m_ui.errorFrame.toggled.connect(self._error_frame)
        self.m_ui.extendedFormatBox.toggled.connect(self._extended_format)
        self.m_ui.flexibleDataRateBox.toggled.connect(self._flexible_datarate)
        self.m_ui.frameIdEdit.textChanged.connect(self._frameid_or_payload_changed)
        self.m_ui.payloadEdit.textChanged.connect(self._frameid_or_payload_changed)
        self._frameid_or_payload_changed()
        self.m_ui.sendButton.clicked.connect(self._send)

    @Slot(bool)
    def _data_frame(self, value):
        if value:
            self.m_ui.flexibleDataRateBox.setEnabled(True)

    @Slot(bool)
    def _remote_frame(self, value):
        if value:
            self.m_ui.flexibleDataRateBox.setEnabled(False)
            self.m_ui.flexibleDataRateBox.setChecked(False)

    @Slot(bool)
    def _error_frame(self, value):
        if value:
            self.m_ui.flexibleDataRateBox.setEnabled(False)
            self.m_ui.flexibleDataRateBox.setChecked(False)

    @Slot(bool)
    def _extended_format(self, value):
        m = MAX_EXTENDED_ID if value else MAX_STANDARD_ID
        self.m_hexIntegerValidator.set_maximum(m)

    @Slot(bool)
    def _flexible_datarate(self, value):
        len = MAX_PAYLOAD_FD if value else MAX_PAYLOAD
        self.m_hexStringValidator.set_max_length(len)
        self.m_ui.bitrateSwitchBox.setEnabled(value)
        if not value:
            self.m_ui.bitrateSwitchBox.setChecked(False)

    @Slot()
    def _frameid_or_payload_changed(self):
        has_frame_id = bool(self.m_ui.frameIdEdit.text())
        self.m_ui.sendButton.setEnabled(has_frame_id)
        tt = "" if has_frame_id else "Cannot send because no Frame ID was given."
        self.m_ui.sendButton.setToolTip(tt)
        if has_frame_id:
            is_even = is_even_hex(self.m_ui.payloadEdit.text())
            self.m_ui.sendButton.setEnabled(is_even)
            tt = "" if is_even else "Cannot send because Payload hex string is invalid."
            self.m_ui.sendButton.setToolTip(tt)

    @Slot()
    def _send(self):
        frame_id = int(self.m_ui.frameIdEdit.text(), base=16)
        data = self.m_ui.payloadEdit.text().replace(" ", "")
        self.m_ui.payloadEdit.setText(format_hex_data(data))
        payload = QByteArray.fromHex(bytes(data, encoding='utf8'))

        frame = QCanBusFrame(frame_id, payload)
        frame.setExtendedFrameFormat(self.m_ui.extendedFormatBox.isChecked())
        frame.setFlexibleDataRateFormat(self.m_ui.flexibleDataRateBox.isChecked())
        frame.setBitrateSwitch(self.m_ui.bitrateSwitchBox.isChecked())

        if self.m_ui.errorFrame.isChecked():
            frame.setFrameType(QCanBusFrame.ErrorFrame)
        elif self.m_ui.remoteFrame.isChecked():
            frame.setFrameType(QCanBusFrame.RemoteRequestFrame)

        self.send_frame.emit(frame)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SendFrameBox</class>
 <widget class="QGroupBox" name="SendFrameBox">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>505</width>
    <height>219</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <property name="title">
   <string/>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout_4">
   <property name="sizeConstraint">
    <enum>QLayout::SizeConstraint::SetMinimumSize</enum>
   </property>
   <item>
    <widget class="QGroupBox" name="frameTypeBox">
     <property name="title">
      <string>Frame Type</string>
     </property>
     <property name="checkable">
      <bool>false</bool>
     </property>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <property name="topMargin">
       <number>0</number>
      </property>
      <property name="bottomMargin">
       <number>0</number>
      </property>
      <item>
       <widget class="QRadioButton" name="dataFrame">
        <property name="toolTip">
         <string>Sends a CAN data frame.</string>
        </property>
        <property name="text">
         <string>D&amp;ata Frame</string>
        </property>
        <property name="checked">
         <bool>true</bool>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QRadioButton" name="remoteFrame">
        <property name="toolTip">
         <string>Sends a CAN remote request frame.</string>
        </property>
        <property name="text">
         <string>Re&amp;mote Request Frame</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QRadioButton" name="errorFrame">
        <property name="toolTip">
         <string>Sends an error frame.</string>
        </property>
        <property name="text">
         <string>&amp;Error Frame</string>
        </property>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
   <item>
    <widget class="QGroupBox" name="frameOptionsBox">
     <property name="title">
      <string>Frame Options</string>
     </property>
     <layout class="QHBoxLayout" name="horizontalLayout_2">
      <property name="topMargin">
       <number>0</number>
      </property>
      <property name="bottomMargin">
       <number>0</number>
      </property>
      <item>
       <widget class="QCheckBox" name="extendedFormatBox">
        <property name="toolTip">
         <string>Allows extended frames with 29 bit identifier.</string>
        </property>
        <property name="text">
         <string>E&amp;xtended Format</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QCheckBox" name="flexibleDataRateBox">
        <property name="toolTip">
         <string>Allows up to 64 byte payload data.</string>
        </property>
        <property name="text">
         <string>&amp;Flexible Data-Rate</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QCheckBox" name="bitrateSwitchBox">
        <property name="enabled">
         <bool>false</bool>
        </property>
        <property name="toolTip">
         <string>Sends payload at higher data rate.</string>
        </property>
        <property name="text">
         <string>&amp;Bitrate Switch</string>
        </property>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout_3">
     <property name="sizeConstraint">
      <enum>QLayout::SizeConstraint::SetMinimumSize</enum>
     </property>
     <item>
      <layout class="QVBoxLayout" name="verticalLayout">
       <item>
        <widget class="QLabel" name="frameIdLabel">
         <property name="text">
          <string>Frame &amp;ID (hex)</string>
         </property>
         <property name="buddy">
          <cstring>frameIdEdit</cstring>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QLineEdit" name="frameIdEdit">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
           <horstretch>1</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
         <property name="placeholderText">
          <string>123</string>
         </property>
         <property name="clearButtonEnabled">
          <bool>true</bool>
         </property>
        </widget>
       </item>
      </layout>
     </item>
     <item>
      <layout class="QVBoxLayout" name="verticalLayout_2">
       <item>
        <widget class="QLabel" name="payloadLabel">
         <property name="text">
          <string>&amp;Payload (hex)</string>
         </property>
         <property name="buddy">
          <cstring>payloadEdit</cstring>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QLineEdit" name="payloadEdit">
         <property name="sizePolicy">
          <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
           <horstretch>2</horstretch>
           <verstretch>0</verstretch>
          </sizepolicy>
         </property>
         <property name="placeholderText">
          <string>12 34 AB CE</string>
         </property>
         <property name="clearButtonEnabled">
          <bool>true</bool>
         </property>
        </widget>
       </item>
      </layout>
     </item>
     <item>
      <layout class="QVBoxLayout" name="verticalLayout_3">
       <item>
        <widget class="QLabel" name="label">
         <property name="text">
          <string/>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QPushButton" name="sendButton">
         <property name="text">
          <string>&amp;Send</string>
         </property>
        </widget>
       </item>
      </layout>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>
<RCC>
    <qresource prefix="/">
        <file>images/connect.png</file>
        <file>images/disconnect.png</file>
        <file>images/application-exit.png</file>
        <file>images/clear.png</file>
    </qresource>
</RCC>