Browse Source

平台兼容

flower_linux 6 days ago
parent
commit
7684979972

+ 25 - 0
CMakeLists.txt

@@ -39,6 +39,25 @@ qt_add_executable(soft_bus
     src/serial_manager/serial_storage_thread.cpp
     src/serial_manager/serial_parser_thread.h
     src/serial_manager/serial_parser_thread.cpp
+    src/devices/DeviceWatcherBase.h
+    src/devices/DeviceWatcherBase.cpp
+    src/devices/DeviceManager.h
+    src/devices/DeviceManager.cpp
+    src/devices/serial/SerialPortInfo.h
+    src/devices/serial/SerialPortWatcher.h
+    src/devices/serial/SerialPortWatcher.cpp
+    src/devices/usb/UsbDeviceInfo.h
+    src/devices/usb/UsbDeviceWatcher.h
+    src/devices/usb/UsbDeviceWatcher.cpp
+    src/devices/network/NetworkDeviceInfo.h
+    src/devices/network/NetworkPortWatcher.h
+    src/devices/network/NetworkPortWatcher.cpp
+    src/devices/platform/linux/UdevMonitor.h
+    src/devices/platform/linux/UdevMonitor.cpp
+    src/devices/platform/linux/LinuxDeviceUtils.h
+    src/devices/platform/windows/WinDeviceMonitor.h
+    src/devices/platform/windows/WinDeviceMonitor.cpp
+    src/devices/platform/windows/WinDeviceUtils.h
     src/can_manager/can_manager.h
     src/can_manager/can_manager.cpp
 
@@ -131,6 +150,12 @@ else()
         target_link_libraries(soft_bus PRIVATE ${ZLIB_LIBRARIES})
     endif()
 
+    # 查找并链接 libudev 用于设备热插拔监控
+    find_library(UDEV_LIB udev REQUIRED)
+    if(UDEV_LIB)
+        target_link_libraries(soft_bus PRIVATE ${UDEV_LIB})
+    endif()
+
     # 添加CAN API库目录到链接路径(Linux)
     target_link_directories(soft_bus PRIVATE "${CAN_API_DIR}/lib")
     

+ 292 - 282
src/database_manager/databasepage.ui

@@ -13,305 +13,315 @@
   <property name="windowTitle">
    <string>数据库页面</string>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout">
-   <item>
-    <widget class="QWidget" name="tablePanel">
-     <layout class="QVBoxLayout" name="tablePanelLayout">
-     <property name="spacing">
-      <number>8</number>
-     </property>
-     <property name="leftMargin">
-      <number>8</number>
-     </property>
-     <property name="topMargin">
-      <number>8</number>
-     </property>
-     <property name="rightMargin">
-      <number>8</number>
-     </property>
-     <property name="bottomMargin">
-      <number>8</number>
-     </property>
-     <item>
-      <layout class="QHBoxLayout" name="buttonLayout">
-       <property name="spacing">
-        <number>12</number>
-       </property>
-       <item>
-        <widget class="QPushButton" name="refreshButton">
-         <property name="sizePolicy">
-          <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
-           <horstretch>0</horstretch>
-           <verstretch>0</verstretch>
-          </sizepolicy>
-         </property>
-         <property name="text">
-          <string>刷新</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="submitButton">
-         <property name="sizePolicy">
-          <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
-           <horstretch>0</horstretch>
-           <verstretch>0</verstretch>
-          </sizepolicy>
-         </property>
-         <property name="text">
-          <string>提交修改</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="revertButton">
-         <property name="sizePolicy">
-          <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
-           <horstretch>0</horstretch>
-           <verstretch>0</verstretch>
-          </sizepolicy>
-         </property>
-         <property name="text">
-          <string>撤销修改</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <spacer name="buttonSpacer">
-         <property name="orientation">
-          <enum>Qt::Orientation::Horizontal</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>20</width>
-           <height>20</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-      </layout>
-     </item>
-     <item>
-      <widget class="QListWidget" name="tableList">
-       <property name="selectionMode">
-        <enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
-       </property>
-      </widget>
-     </item>
-    </layout>
-   </widget>
-   </item>
-   <item>
-    <widget class="QWidget" name="dataPanel">
-     <layout class="QVBoxLayout" name="dataPanelLayout">
-     <property name="spacing">
-      <number>6</number>
-     </property>
-     <property name="leftMargin">
-      <number>8</number>
-     </property>
-     <property name="topMargin">
-      <number>8</number>
-     </property>
-     <property name="rightMargin">
-      <number>8</number>
-     </property>
-     <property name="bottomMargin">
-      <number>8</number>
-     </property>
-     <item>
-      <widget class="QLabel" name="currentTableLabel">
-       <property name="text">
-        <string>当前未选择表</string>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QLabel" name="infoLabel">
-       <property name="text">
-        <string/>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <widget class="QToolBar" name="tableToolbar">
-       <property name="movable">
-        <bool>false</bool>
-       </property>
-       <property name="toolButtonStyle">
-        <enum>Qt::ToolButtonStyle::ToolButtonTextOnly</enum>
-       </property>
-       <property name="floatable">
-        <bool>false</bool>
-       </property>
-      </widget>
-     </item>
-     <item>
-      <layout class="QHBoxLayout" name="controlRow">
-       <property name="spacing">
-        <number>8</number>
-       </property>
-       <item>
-        <widget class="QLabel" name="sortLabel">
-         <property name="text">
-          <string>排序列</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QComboBox" name="sortColumnCombo">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QLabel" name="orderLabel">
-         <property name="text">
-          <string>排序方式</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QComboBox" name="sortOrderCombo">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
-         <item>
-          <property name="text">
-           <string>升序</string>
-          </property>
-         </item>
-         <item>
-          <property name="text">
-           <string>降序</string>
-          </property>
-         </item>
-        </widget>
-       </item>
-       <item>
-        <widget class="QLabel" name="pageSizeLabel">
-         <property name="text">
-          <string>每页条数</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QSpinBox" name="pageSizeSpin">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
-         <property name="minimum">
-          <number>5</number>
-         </property>
-         <property name="maximum">
-          <number>500</number>
-         </property>
-         <property name="singleStep">
-          <number>5</number>
-         </property>
-         <property name="value">
-          <number>20</number>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <spacer name="controlSpacer">
-         <property name="orientation">
-          <enum>Qt::Orientation::Horizontal</enum>
-         </property>
-         <property name="sizeHint" stdset="0">
-          <size>
-           <width>20</width>
-           <height>20</height>
-          </size>
-         </property>
-        </spacer>
-       </item>
-       <item>
-        <widget class="QPushButton" name="prevPageButton">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
-         <property name="text">
-          <string>上一页</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QPushButton" name="nextPageButton">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
+  <widget class="QWidget" name="tablePanel">
+   <property name="geometry">
+    <rect>
+     <x>9</x>
+     <y>9</y>
+     <width>314</width>
+     <height>248</height>
+    </rect>
+   </property>
+   <layout class="QVBoxLayout" name="tablePanelLayout">
+    <property name="spacing">
+     <number>8</number>
+    </property>
+    <property name="leftMargin">
+     <number>8</number>
+    </property>
+    <property name="topMargin">
+     <number>8</number>
+    </property>
+    <property name="rightMargin">
+     <number>8</number>
+    </property>
+    <property name="bottomMargin">
+     <number>8</number>
+    </property>
+    <item>
+     <layout class="QHBoxLayout" name="buttonLayout">
+      <property name="spacing">
+       <number>12</number>
+      </property>
+      <item>
+       <widget class="QPushButton" name="refreshButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>刷新</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="submitButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>提交修改</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="revertButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>撤销修改</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="buttonSpacer">
+        <property name="orientation">
+         <enum>Qt::Orientation::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </item>
+    <item>
+     <widget class="QListWidget" name="tableList">
+      <property name="selectionMode">
+       <enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QWidget" name="dataPanel">
+   <property name="geometry">
+    <rect>
+     <x>9</x>
+     <y>311</y>
+     <width>709</width>
+     <height>320</height>
+    </rect>
+   </property>
+   <layout class="QVBoxLayout" name="dataPanelLayout">
+    <property name="spacing">
+     <number>6</number>
+    </property>
+    <property name="leftMargin">
+     <number>8</number>
+    </property>
+    <property name="topMargin">
+     <number>8</number>
+    </property>
+    <property name="rightMargin">
+     <number>8</number>
+    </property>
+    <property name="bottomMargin">
+     <number>8</number>
+    </property>
+    <item>
+     <widget class="QLabel" name="currentTableLabel">
+      <property name="text">
+       <string>当前未选择表</string>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QLabel" name="infoLabel">
+      <property name="text">
+       <string/>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <widget class="QToolBar" name="tableToolbar">
+      <property name="movable">
+       <bool>false</bool>
+      </property>
+      <property name="toolButtonStyle">
+       <enum>Qt::ToolButtonStyle::ToolButtonTextOnly</enum>
+      </property>
+      <property name="floatable">
+       <bool>false</bool>
+      </property>
+     </widget>
+    </item>
+    <item>
+     <layout class="QHBoxLayout" name="controlRow">
+      <property name="spacing">
+       <number>8</number>
+      </property>
+      <item>
+       <widget class="QLabel" name="sortLabel">
+        <property name="text">
+         <string>排序列</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="sortColumnCombo">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLabel" name="orderLabel">
+        <property name="text">
+         <string>排序方式</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QComboBox" name="sortOrderCombo">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <item>
          <property name="text">
-          <string>下一页</string>
+          <string>升序</string>
          </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QLabel" name="pageInfoLabel">
+        </item>
+        <item>
          <property name="text">
-          <string>未加载数据</string>
+          <string>降序</string>
          </property>
-        </widget>
-       </item>
-      </layout>
-     </item>
-     <item>
-      <widget class="QSplitter" name="tableSplitter">
-       <property name="orientation">
-        <enum>Qt::Orientation::Vertical</enum>
-       </property>
-       <widget class="QTableView" name="tableView">
-        <property name="editTriggers">
-         <set>QAbstractItemView::EditTrigger::AnyKeyPressed|QAbstractItemView::EditTrigger::DoubleClicked|QAbstractItemView::EditTrigger::EditKeyPressed</set>
+        </item>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLabel" name="pageSizeLabel">
+        <property name="text">
+         <string>每页条数</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QSpinBox" name="pageSizeSpin">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="minimum">
+         <number>5</number>
         </property>
-        <property name="alternatingRowColors">
-         <bool>true</bool>
+        <property name="maximum">
+         <number>500</number>
         </property>
-        <property name="selectionMode">
-         <enum>QAbstractItemView::SelectionMode::ExtendedSelection</enum>
+        <property name="singleStep">
+         <number>5</number>
         </property>
-        <property name="selectionBehavior">
-         <enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
+        <property name="value">
+         <number>20</number>
         </property>
        </widget>
-       <widget class="QTreeWidget" name="detailView">
-        <property name="visible">
-         <bool>false</bool>
+      </item>
+      <item>
+       <spacer name="controlSpacer">
+        <property name="orientation">
+         <enum>Qt::Orientation::Horizontal</enum>
         </property>
-        <property name="rootIsDecorated">
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QPushButton" name="prevPageButton">
+        <property name="enabled">
          <bool>false</bool>
         </property>
-        <property name="uniformRowHeights">
-         <bool>true</bool>
+        <property name="text">
+         <string>上一页</string>
         </property>
-        <property name="wordWrap">
-         <bool>true</bool>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="nextPageButton">
+        <property name="enabled">
+         <bool>false</bool>
         </property>
-        <property name="columnCount">
-         <number>2</number>
+        <property name="text">
+         <string>下一页</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QLabel" name="pageInfoLabel">
+        <property name="text">
+         <string>未加载数据</string>
         </property>
-        <attribute name="headerVisible">
-         <bool>true</bool>
-        </attribute>
-        <column>
-         <property name="text">
-          <string>字段</string>
-         </property>
-        </column>
-        <column>
-         <property name="text">
-          <string>值</string>
-         </property>
-        </column>
        </widget>
+      </item>
+     </layout>
+    </item>
+    <item>
+     <widget class="QSplitter" name="tableSplitter">
+      <property name="orientation">
+       <enum>Qt::Orientation::Vertical</enum>
+      </property>
+      <widget class="QTableView" name="tableView">
+       <property name="editTriggers">
+        <set>QAbstractItemView::EditTrigger::AnyKeyPressed|QAbstractItemView::EditTrigger::DoubleClicked|QAbstractItemView::EditTrigger::EditKeyPressed</set>
+       </property>
+       <property name="alternatingRowColors">
+        <bool>true</bool>
+       </property>
+       <property name="selectionMode">
+        <enum>QAbstractItemView::SelectionMode::ExtendedSelection</enum>
+       </property>
+       <property name="selectionBehavior">
+        <enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
+       </property>
+      </widget>
+      <widget class="QTreeWidget" name="detailView">
+       <property name="visible">
+        <bool>false</bool>
+       </property>
+       <property name="rootIsDecorated">
+        <bool>false</bool>
+       </property>
+       <property name="uniformRowHeights">
+        <bool>true</bool>
+       </property>
+       <property name="wordWrap">
+        <bool>true</bool>
+       </property>
+       <property name="columnCount">
+        <number>2</number>
+       </property>
+       <attribute name="headerVisible">
+        <bool>true</bool>
+       </attribute>
+       <column>
+        <property name="text">
+         <string>字段</string>
+        </property>
+       </column>
+       <column>
+        <property name="text">
+         <string>值</string>
+        </property>
+       </column>
       </widget>
-     </item>
-    </layout>
-   </widget>
-   </item>
-  </layout>
+     </widget>
+    </item>
+   </layout>
+  </widget>
  </widget>
  <resources/>
  <connections/>

+ 153 - 0
src/devices/DeviceManager.cpp

@@ -0,0 +1,153 @@
+#include "DeviceManager.h"
+
+#include <QDebug>
+#include <utility>
+
+namespace devices
+{
+
+DeviceManager::DeviceManager(QObject *parent)
+    : QObject(parent)
+{
+}
+
+DeviceManager::~DeviceManager()
+{
+    stopAll();
+    for (auto &watcher : m_watchers)
+    {
+        if (watcher)
+        {
+            disconnectWatcher(watcher);
+        }
+    }
+}
+
+void DeviceManager::registerWatcher(DeviceWatcherBase *watcher)
+{
+    if (!watcher || m_watchers.contains(watcher))
+    {
+        return;
+    }
+
+    m_watchers.append(watcher);
+    connectWatcher(watcher);
+}
+
+void DeviceManager::unregisterWatcher(DeviceWatcherBase *watcher)
+{
+    if (!watcher)
+    {
+        return;
+    }
+
+    stopWatcher(watcher);
+    disconnectWatcher(watcher);
+    m_watchers.removeAll(watcher);
+}
+
+bool DeviceManager::startAll()
+{
+    bool ok = true;
+    for (auto watcher : std::as_const(m_watchers))
+    {
+        if (!watcher)
+        {
+            continue;
+        }
+
+        ok = startWatcher(watcher) && ok;
+    }
+    return ok;
+}
+
+void DeviceManager::stopAll()
+{
+    for (auto watcher : std::as_const(m_watchers))
+    {
+        if (watcher)
+        {
+            stopWatcher(watcher);
+        }
+    }
+}
+
+bool DeviceManager::startWatcher(DeviceWatcherBase *watcher)
+{
+    if (!watcher)
+    {
+        return false;
+    }
+
+    if (watcher->isRunning())
+    {
+        return true;
+    }
+
+    if (!watcher->startWatching())
+    {
+        qWarning() << "Failed to start watcher" << watcher;
+        return false;
+    }
+
+    return true;
+}
+
+void DeviceManager::stopWatcher(DeviceWatcherBase *watcher)
+{
+    if (!watcher || !watcher->isRunning())
+    {
+        return;
+    }
+    watcher->stopWatching();
+}
+
+QList<DeviceWatcherBase *> DeviceManager::watchers() const
+{
+    QList<DeviceWatcherBase *> result;
+    for (auto watcher : m_watchers)
+    {
+        if (watcher)
+        {
+            result.append(watcher);
+        }
+    }
+    return result;
+}
+
+void DeviceManager::connectWatcher(DeviceWatcherBase *watcher)
+{
+    if (!watcher)
+    {
+        return;
+    }
+
+    connect(watcher,
+            &DeviceWatcherBase::deviceArrived,
+            this,
+            &DeviceManager::deviceArrived);
+    connect(watcher,
+            &DeviceWatcherBase::deviceRemoved,
+            this,
+            &DeviceManager::deviceRemoved);
+}
+
+void DeviceManager::disconnectWatcher(DeviceWatcherBase *watcher)
+{
+    if (!watcher)
+    {
+        return;
+    }
+
+    disconnect(watcher,
+               &DeviceWatcherBase::deviceArrived,
+               this,
+               &DeviceManager::deviceArrived);
+    disconnect(watcher,
+               &DeviceWatcherBase::deviceRemoved,
+               this,
+               &DeviceManager::deviceRemoved);
+}
+
+} // namespace devices
+

+ 43 - 0
src/devices/DeviceManager.h

@@ -0,0 +1,43 @@
+#pragma once
+
+#include <QObject>
+#include <QPointer>
+#include <QVector>
+
+#include "DeviceWatcherBase.h"
+
+namespace devices
+{
+
+class DeviceManager : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit DeviceManager(QObject *parent = nullptr);
+    ~DeviceManager() override;
+
+    void registerWatcher(DeviceWatcherBase *watcher);
+    void unregisterWatcher(DeviceWatcherBase *watcher);
+
+    bool startAll();
+    void stopAll();
+
+    bool startWatcher(DeviceWatcherBase *watcher);
+    void stopWatcher(DeviceWatcherBase *watcher);
+
+    QList<DeviceWatcherBase *> watchers() const;
+
+signals:
+    void deviceArrived(const devices::DeviceEvent &event);
+    void deviceRemoved(const devices::DeviceEvent &event);
+
+private:
+    QList<QPointer<DeviceWatcherBase>> m_watchers;
+
+    void connectWatcher(DeviceWatcherBase *watcher);
+    void disconnectWatcher(DeviceWatcherBase *watcher);
+};
+
+} // namespace devices
+

+ 61 - 0
src/devices/DeviceWatcherBase.cpp

@@ -0,0 +1,61 @@
+#include "DeviceWatcherBase.h"
+
+#include <QMetaType>
+#include <QVariantMap>
+
+namespace devices
+{
+
+DeviceWatcherBase::DeviceWatcherBase(QObject *parent)
+    : QObject(parent),
+      m_running(false)
+{
+    qRegisterMetaType<devices::DeviceEvent>("devices::DeviceEvent");
+}
+
+DeviceWatcherBase::~DeviceWatcherBase() = default;
+
+bool DeviceWatcherBase::startWatching()
+{
+    if (m_running)
+    {
+        return true;
+    }
+
+    if (!doStart())
+    {
+        return false;
+    }
+
+    m_running = true;
+    return true;
+}
+
+void DeviceWatcherBase::stopWatching()
+{
+    if (!m_running)
+    {
+        return;
+    }
+
+    doStop();
+    m_running = false;
+}
+
+bool DeviceWatcherBase::isRunning() const noexcept
+{
+    return m_running;
+}
+
+void DeviceWatcherBase::emitDeviceArrived(const QString &identifier, const QVariantMap &properties)
+{
+    emit deviceArrived({identifier, properties});
+}
+
+void DeviceWatcherBase::emitDeviceRemoved(const QString &identifier, const QVariantMap &properties)
+{
+    emit deviceRemoved({identifier, properties});
+}
+
+} // namespace devices
+

+ 46 - 0
src/devices/DeviceWatcherBase.h

@@ -0,0 +1,46 @@
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QVariantMap>
+
+namespace devices
+{
+
+struct DeviceEvent
+{
+    QString identifier;
+    QVariantMap properties;
+};
+
+class DeviceWatcherBase : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit DeviceWatcherBase(QObject *parent = nullptr);
+    ~DeviceWatcherBase() override;
+
+    bool startWatching();
+    void stopWatching();
+    bool isRunning() const noexcept;
+
+signals:
+    void deviceArrived(const devices::DeviceEvent &event);
+    void deviceRemoved(const devices::DeviceEvent &event);
+
+protected:
+    virtual bool doStart() = 0;
+    virtual void doStop() = 0;
+
+    void emitDeviceArrived(const QString &identifier, const QVariantMap &properties = {});
+    void emitDeviceRemoved(const QString &identifier, const QVariantMap &properties = {});
+
+private:
+    bool m_running;
+};
+
+} // namespace devices
+
+Q_DECLARE_METATYPE(devices::DeviceEvent)
+

+ 21 - 0
src/devices/network/NetworkDeviceInfo.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include <QMetaType>
+#include <QString>
+
+namespace devices::network
+{
+
+struct NetworkDeviceInfo
+{
+    QString interfaceName;
+    QString hardwareAddress;
+    QString ipv4Address;
+    QString ipv6Address;
+    QString description;
+};
+
+} // namespace devices::network
+
+Q_DECLARE_METATYPE(devices::network::NetworkDeviceInfo)
+

+ 26 - 0
src/devices/network/NetworkPortWatcher.cpp

@@ -0,0 +1,26 @@
+#include "devices/network/NetworkPortWatcher.h"
+
+namespace devices::network
+{
+
+NetworkPortWatcher::NetworkPortWatcher(QObject *parent)
+    : DeviceWatcherBase(parent)
+{
+    qRegisterMetaType<NetworkDeviceInfo>("devices::network::NetworkDeviceInfo");
+}
+
+NetworkPortWatcher::~NetworkPortWatcher() = default;
+
+bool NetworkPortWatcher::doStart()
+{
+    // Placeholder implementation. Real network monitoring will be added later.
+    return true;
+}
+
+void NetworkPortWatcher::doStop()
+{
+    // Placeholder
+}
+
+} // namespace devices::network
+

+ 27 - 0
src/devices/network/NetworkPortWatcher.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include "devices/DeviceWatcherBase.h"
+#include "devices/network/NetworkDeviceInfo.h"
+
+namespace devices::network
+{
+
+class NetworkPortWatcher : public devices::DeviceWatcherBase
+{
+    Q_OBJECT
+
+public:
+    explicit NetworkPortWatcher(QObject *parent = nullptr);
+    ~NetworkPortWatcher() override;
+
+signals:
+    void networkDeviceArrived(const NetworkDeviceInfo &info);
+    void networkDeviceRemoved(const NetworkDeviceInfo &info);
+
+protected:
+    bool doStart() override;
+    void doStop() override;
+};
+
+} // namespace devices::network
+

+ 21 - 0
src/devices/platform/linux/LinuxDeviceUtils.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#ifdef Q_OS_LINUX
+
+#include <QtGlobal>
+#include <QString>
+
+namespace devices::platform::linux_os
+{
+
+inline quint16 parseHexId(const QString &value)
+{
+    bool ok = false;
+    quint32 parsed = value.toUInt(&ok, 16);
+    return ok ? static_cast<quint16>(parsed & 0xFFFF) : 0;
+}
+
+} // namespace devices::platform::linux_os
+
+#endif // Q_OS_LINUX
+

+ 202 - 0
src/devices/platform/linux/UdevMonitor.cpp

@@ -0,0 +1,202 @@
+#include <QtGlobal>
+
+#if defined(Q_OS_LINUX)
+
+#include "devices/platform/linux/UdevMonitor.h"
+
+#include <QByteArray>
+#include <QDebug>
+#include <QSocketNotifier>
+
+#include <libudev.h>
+
+#ifdef linux
+#undef linux
+#endif
+
+namespace devices::platform::linux_os
+{
+
+UdevMonitor::UdevMonitor(QObject *parent)
+    : QObject(parent),
+      m_udev(nullptr),
+      m_monitor(nullptr),
+      m_fd(-1),
+      m_notifier(nullptr)
+{
+}
+
+UdevMonitor::~UdevMonitor()
+{
+    stop();
+}
+
+bool UdevMonitor::start(const QString &subsystem)
+{
+    if (isRunning())
+    {
+        return true;
+    }
+
+    m_udev = udev_new();
+    if (!m_udev)
+    {
+        qWarning() << "UdevMonitor: failed to create udev context";
+        return false;
+    }
+
+    m_monitor = udev_monitor_new_from_netlink(m_udev, "udev");
+    if (!m_monitor)
+    {
+        qWarning() << "UdevMonitor: failed to create monitor";
+        stop();
+        return false;
+    }
+
+    m_subsystem = subsystem;
+    QByteArray subsystemUtf8 = subsystem.toUtf8();
+    if (!subsystemUtf8.isEmpty())
+    {
+        if (udev_monitor_filter_add_match_subsystem_devtype(m_monitor, subsystemUtf8.constData(), nullptr) < 0)
+        {
+            qWarning() << "UdevMonitor: failed to add filter for subsystem" << subsystem;
+        }
+    }
+
+    if (udev_monitor_enable_receiving(m_monitor) < 0)
+    {
+        qWarning() << "UdevMonitor: failed to enable receiving";
+        stop();
+        return false;
+    }
+
+    m_fd = udev_monitor_get_fd(m_monitor);
+    if (m_fd < 0)
+    {
+        qWarning() << "UdevMonitor: invalid monitor file descriptor";
+        stop();
+        return false;
+    }
+
+    m_notifier = new QSocketNotifier(m_fd, QSocketNotifier::Read, this);
+    connect(m_notifier, &QSocketNotifier::activated, this, &UdevMonitor::onReadyRead);
+
+    return true;
+}
+
+void UdevMonitor::stop()
+{
+    if (m_notifier)
+    {
+        m_notifier->setEnabled(false);
+        m_notifier->deleteLater();
+        m_notifier = nullptr;
+    }
+
+    if (m_monitor)
+    {
+        udev_monitor_unref(m_monitor);
+        m_monitor = nullptr;
+    }
+
+    if (m_udev)
+    {
+        udev_unref(m_udev);
+        m_udev = nullptr;
+    }
+
+    m_fd = -1;
+    m_subsystem.clear();
+}
+
+bool UdevMonitor::isRunning() const noexcept
+{
+    return m_monitor != nullptr;
+}
+
+void UdevMonitor::onReadyRead()
+{
+    if (!m_monitor)
+    {
+        return;
+    }
+
+    udev_device *device = nullptr;
+    while ((device = udev_monitor_receive_device(m_monitor)) != nullptr)
+    {
+        const char *action = udev_device_get_action(device);
+        QVariantMap properties = extractProperties(device);
+
+        if (action)
+        {
+            const QString actionStr = QString::fromLatin1(action);
+            if (actionStr.compare(QStringLiteral("add"), Qt::CaseInsensitive) == 0)
+            {
+                emit deviceAdded(properties);
+            }
+            else if (actionStr.compare(QStringLiteral("remove"), Qt::CaseInsensitive) == 0)
+            {
+                emit deviceRemoved(properties);
+            }
+            else if (actionStr.compare(QStringLiteral("change"), Qt::CaseInsensitive) == 0)
+            {
+                // Treat change as removal + addition for clients that only listen to add/remove.
+                emit deviceRemoved(properties);
+                emit deviceAdded(properties);
+            }
+        }
+
+        udev_device_unref(device);
+    }
+}
+
+QVariantMap UdevMonitor::extractProperties(struct udev_device *device) const
+{
+    QVariantMap map;
+    if (!device)
+    {
+        return map;
+    }
+
+    if (const char *devNode = udev_device_get_devnode(device))
+    {
+        map.insert(QStringLiteral("devnode"), QString::fromLatin1(devNode));
+    }
+
+    if (const char *subsys = udev_device_get_subsystem(device))
+    {
+        map.insert(QStringLiteral("subsystem"), QString::fromLatin1(subsys));
+    }
+
+    if (const char *devtype = udev_device_get_devtype(device))
+    {
+        map.insert(QStringLiteral("devtype"), QString::fromLatin1(devtype));
+    }
+
+    if (const char *vendor = udev_device_get_property_value(device, "ID_VENDOR_ID"))
+    {
+        map.insert(QStringLiteral("vendorId"), QString::fromLatin1(vendor));
+    }
+
+    if (const char *product = udev_device_get_property_value(device, "ID_MODEL_ID"))
+    {
+        map.insert(QStringLiteral("productId"), QString::fromLatin1(product));
+    }
+
+    if (const char *serial = udev_device_get_property_value(device, "ID_SERIAL_SHORT"))
+    {
+        map.insert(QStringLiteral("serialNumber"), QString::fromLatin1(serial));
+    }
+
+    if (const char *friendly = udev_device_get_property_value(device, "ID_MODEL_FROM_DATABASE"))
+    {
+        map.insert(QStringLiteral("description"), QString::fromLatin1(friendly));
+    }
+
+    return map;
+}
+
+} // namespace devices::platform::linux_os
+
+#endif // defined(Q_OS_LINUX)
+

+ 90 - 0
src/devices/platform/linux/UdevMonitor.h

@@ -0,0 +1,90 @@
+#pragma once
+
+#include <QObject>
+#include <QString>
+#include <QVariantMap>
+
+QT_BEGIN_NAMESPACE
+class QSocketNotifier;
+QT_END_NAMESPACE
+
+struct udev;
+struct udev_monitor;
+struct udev_device;
+
+#ifdef linux
+#undef linux
+#endif
+
+namespace devices::platform::linux_os
+{
+
+class UdevMonitor : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit UdevMonitor(QObject *parent = nullptr);
+    ~UdevMonitor() override;
+
+    bool start(const QString &subsystem = QStringLiteral("tty"));
+    void stop();
+    bool isRunning() const noexcept;
+
+signals:
+    void deviceAdded(const QVariantMap &properties);
+    void deviceRemoved(const QVariantMap &properties);
+
+private slots:
+    void onReadyRead();
+
+private:
+    QVariantMap extractProperties(struct udev_device *device) const;
+
+#if defined(Q_OS_LINUX)
+    struct udev *m_udev;
+    struct udev_monitor *m_monitor;
+    int m_fd;
+    QSocketNotifier *m_notifier;
+    QString m_subsystem;
+#endif
+};
+
+} // namespace devices::platform::linux_os
+
+#if !defined(Q_OS_LINUX)
+
+inline devices::platform::linux_os::UdevMonitor::UdevMonitor(QObject *parent)
+    : QObject(parent)
+{
+}
+
+inline devices::platform::linux_os::UdevMonitor::~UdevMonitor() = default;
+
+inline bool devices::platform::linux_os::UdevMonitor::start(const QString &subsystem)
+{
+    (void)subsystem;
+    return false;
+}
+
+inline void devices::platform::linux_os::UdevMonitor::stop()
+{
+}
+
+inline bool devices::platform::linux_os::UdevMonitor::isRunning() const noexcept
+{
+    return false;
+}
+
+inline void devices::platform::linux_os::UdevMonitor::onReadyRead()
+{
+}
+
+inline QVariantMap devices::platform::linux_os::UdevMonitor::extractProperties(struct udev_device *device) const
+{
+    (void)device;
+    return {};
+}
+
+#endif // !defined(Q_OS_LINUX)
+

+ 45 - 0
src/devices/platform/windows/WinDeviceMonitor.cpp

@@ -0,0 +1,45 @@
+#ifdef Q_OS_WIN
+
+#include "devices/platform/windows/WinDeviceMonitor.h"
+
+#include <QDebug>
+
+namespace devices::platform::windows
+{
+
+WinDeviceMonitor::WinDeviceMonitor(QObject *parent)
+    : QObject(parent),
+      m_running(false)
+{
+}
+
+WinDeviceMonitor::~WinDeviceMonitor()
+{
+    stop();
+}
+
+bool WinDeviceMonitor::start()
+{
+    if (m_running)
+    {
+        return true;
+    }
+
+    qWarning() << "WinDeviceMonitor::start() is not implemented yet";
+    return false;
+}
+
+void WinDeviceMonitor::stop()
+{
+    m_running = false;
+}
+
+bool WinDeviceMonitor::isRunning() const noexcept
+{
+    return m_running;
+}
+
+} // namespace devices::platform::windows
+
+#endif // Q_OS_WIN
+

+ 34 - 0
src/devices/platform/windows/WinDeviceMonitor.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#ifdef Q_OS_WIN
+
+#include <QObject>
+#include <QVariantMap>
+
+namespace devices::platform::windows
+{
+
+class WinDeviceMonitor : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit WinDeviceMonitor(QObject *parent = nullptr);
+    ~WinDeviceMonitor() override;
+
+    bool start();
+    void stop();
+    bool isRunning() const noexcept;
+
+signals:
+    void deviceAdded(const QVariantMap &properties);
+    void deviceRemoved(const QVariantMap &properties);
+
+private:
+    bool m_running;
+};
+
+} // namespace devices::platform::windows
+
+#endif // Q_OS_WIN
+

+ 18 - 0
src/devices/platform/windows/WinDeviceUtils.h

@@ -0,0 +1,18 @@
+#pragma once
+
+#ifdef Q_OS_WIN
+
+#include <QString>
+
+namespace devices::platform::windows
+{
+
+inline QString normalizeDevicePath(const QString &path)
+{
+    return path;
+}
+
+} // namespace devices::platform::windows
+
+#endif // Q_OS_WIN
+

+ 32 - 0
src/devices/serial/SerialPortInfo.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include <QMetaType>
+#include <QString>
+
+namespace devices::serial
+{
+
+struct SerialPortInfo
+{
+    QString portName; 
+    QString systemLocation;
+    QString description;
+    QString manufacturer;
+    QString serialNumber;
+    quint16 vendorId = 0;
+    quint16 productId = 0;
+
+    bool operator==(const SerialPortInfo &other) const noexcept
+    {
+        return portName == other.portName && systemLocation == other.systemLocation;
+    }
+
+    bool isValid() const noexcept
+    {
+        return !portName.isEmpty() || !systemLocation.isEmpty();
+    }
+};
+
+} // namespace devices::serial
+
+Q_DECLARE_METATYPE(devices::serial::SerialPortInfo)

+ 348 - 0
src/devices/serial/SerialPortWatcher.cpp

@@ -0,0 +1,348 @@
+#include "SerialPortWatcher.h"
+
+#include <QDebug>
+#include <QFileInfo>
+#include <QSet>
+#include <QTimer>
+
+#ifdef Q_OS_LINUX
+#include "devices/platform/linux/UdevMonitor.h"
+#endif
+
+namespace devices::serial
+{
+
+namespace
+{
+constexpr int kFallbackPollIntervalMs = 60 * 1000;
+
+quint16 parseHexQuint16(const QVariant &value)
+{
+    bool ok = false;
+    const QString stringValue = value.toString();
+    if (stringValue.isEmpty())
+    {
+        return 0;
+    }
+    const quint16 parsedValue = stringValue.toUShort(&ok, 16);
+    return ok ? parsedValue : 0;
+}
+
+QString portNameFromSystemLocation(const QString &systemLocation)
+{
+    if (systemLocation.isEmpty())
+    {
+        return {};
+    }
+
+    const QFileInfo fileInfo(systemLocation);
+    return fileInfo.fileName();
+}
+
+QVariantMap toVariantMap(const SerialPortInfo &info)
+{
+    QVariantMap map;
+    map.insert(QStringLiteral("portName"), info.portName);
+    map.insert(QStringLiteral("systemLocation"), info.systemLocation);
+    map.insert(QStringLiteral("description"), info.description);
+    map.insert(QStringLiteral("manufacturer"), info.manufacturer);
+    map.insert(QStringLiteral("serialNumber"), info.serialNumber);
+    map.insert(QStringLiteral("vendorId"), info.vendorId);
+    map.insert(QStringLiteral("productId"), info.productId);
+    return map;
+}
+} // namespace
+
+SerialPortWatcher::SerialPortWatcher(QObject *parent)
+    : DeviceWatcherBase(parent)
+{
+    qRegisterMetaType<SerialPortInfo>("devices::serial::SerialPortInfo");
+}
+
+SerialPortWatcher::~SerialPortWatcher()
+{
+    stopWatching();
+}
+
+QList<SerialPortInfo> SerialPortWatcher::knownPorts() const
+{
+    return m_knownPorts.values();
+}
+
+bool SerialPortWatcher::doStart()
+{
+#ifdef Q_OS_LINUX
+    startUdevMonitor();
+#endif // Q_OS_LINUX
+
+    if (!m_pollTimer)
+    {
+        m_pollTimer = new QTimer(this);
+        connect(m_pollTimer, &QTimer::timeout, this, &SerialPortWatcher::refreshPorts);
+    }
+
+    m_pollTimer->setInterval(kFallbackPollIntervalMs);
+    if (!m_pollTimer->isActive())
+    {
+        m_pollTimer->start();
+    }
+
+    refreshPorts();
+    return true;
+}
+
+void SerialPortWatcher::doStop()
+{
+#ifdef Q_OS_LINUX
+    stopUdevMonitor();
+#endif
+
+    if (m_pollTimer)
+    {
+        m_pollTimer->stop();
+    }
+
+    m_knownPorts.clear();
+}
+
+void SerialPortWatcher::emitPortArrived(const SerialPortInfo &info)
+{
+    emit serialPortArrived(info);
+    emitDeviceArrived(info.portName, toVariantMap(info));
+}
+
+void SerialPortWatcher::emitPortRemoved(const SerialPortInfo &info)
+{
+    emit serialPortRemoved(info);
+    emitDeviceRemoved(info.portName, toVariantMap(info));
+}
+
+void SerialPortWatcher::refreshPorts()
+{
+    const QList<QSerialPortInfo> currentInfos = QSerialPortInfo::availablePorts();
+    QSet<QString> currentKeys;
+
+    for (const QSerialPortInfo &info : currentInfos)
+    {
+        const SerialPortInfo serialInfo = toSerialPortInfo(info);
+        const QString key = portIdentifier(serialInfo);
+        if (key.isEmpty())
+        {
+            continue;
+        }
+
+        currentKeys.insert(key);
+        const bool alreadyKnown = m_knownPorts.contains(key);
+        m_knownPorts.insert(key, serialInfo);
+
+        if (!alreadyKnown)
+        {
+            emitPortArrived(serialInfo);
+        }
+    }
+
+    const QList<QString> knownKeys = m_knownPorts.keys();
+    for (const QString &knownKey : knownKeys)
+    {
+        if (!currentKeys.contains(knownKey))
+        {
+            const SerialPortInfo removedInfo = m_knownPorts.take(knownKey);
+            SerialPortInfo infoToEmit = removedInfo;
+            if (!infoToEmit.isValid())
+            {
+                infoToEmit.portName = portNameFromSystemLocation(knownKey);
+                infoToEmit.systemLocation = knownKey;
+            }
+            emitPortRemoved(infoToEmit);
+        }
+    }
+}
+
+SerialPortInfo SerialPortWatcher::toSerialPortInfo(const QSerialPortInfo &info) const
+{
+    SerialPortInfo serialInfo;
+    serialInfo.portName = info.portName();
+    serialInfo.systemLocation = info.systemLocation();
+    serialInfo.description = info.description();
+    serialInfo.manufacturer = info.manufacturer();
+    serialInfo.serialNumber = info.serialNumber();
+    serialInfo.vendorId = info.hasVendorIdentifier() ? info.vendorIdentifier() : 0;
+    serialInfo.productId = info.hasProductIdentifier() ? info.productIdentifier() : 0;
+    return serialInfo;
+}
+
+#ifdef Q_OS_LINUX
+bool SerialPortWatcher::startUdevMonitor()
+{
+    if (m_udevMonitor)
+    {
+        return true;
+    }
+
+    m_udevMonitor = std::make_unique<devices::platform::linux_os::UdevMonitor>(this);
+    if (!m_udevMonitor->start(QStringLiteral("tty")))
+    {
+        m_udevMonitor.reset();
+        return false;
+    }
+
+    connect(m_udevMonitor.get(),
+            &devices::platform::linux_os::UdevMonitor::deviceAdded,
+            this,
+            &SerialPortWatcher::handleUdevAdd);
+    connect(m_udevMonitor.get(),
+            &devices::platform::linux_os::UdevMonitor::deviceRemoved,
+            this,
+            &SerialPortWatcher::handleUdevRemove);
+    return true;
+}
+
+void SerialPortWatcher::stopUdevMonitor()
+{
+    if (!m_udevMonitor)
+    {
+        return;
+    }
+
+    disconnect(m_udevMonitor.get(),
+               &devices::platform::linux_os::UdevMonitor::deviceAdded,
+               this,
+               &SerialPortWatcher::handleUdevAdd);
+    disconnect(m_udevMonitor.get(),
+               &devices::platform::linux_os::UdevMonitor::deviceRemoved,
+               this,
+               &SerialPortWatcher::handleUdevRemove);
+
+    m_udevMonitor->stop();
+    m_udevMonitor.reset();
+}
+
+void SerialPortWatcher::handleUdevAdd(const QVariantMap &properties)
+{
+    const SerialPortInfo info = resolvePortFromProperties(properties);
+    if (!info.isValid())
+    {
+        refreshPorts();
+        return;
+    }
+
+    const QString key = portIdentifier(info);
+    if (key.isEmpty())
+    {
+        refreshPorts();
+        return;
+    }
+
+    const bool alreadyKnown = m_knownPorts.contains(key);
+    m_knownPorts.insert(key, info);
+
+    if (!alreadyKnown)
+    {
+        emitPortArrived(info);
+    }
+}
+
+void SerialPortWatcher::handleUdevRemove(const QVariantMap &properties)
+{
+    const QString devnode = properties.value(QStringLiteral("devnode")).toString();
+    const QString portName = properties.value(QStringLiteral("portName")).toString();
+
+    SerialPortInfo info = takeKnownPort(portName, devnode);
+    if (!info.isValid())
+    {
+        info.portName = !portName.isEmpty() ? portName : portNameFromSystemLocation(devnode);
+        info.systemLocation = devnode;
+    }
+
+    emitPortRemoved(info);
+}
+#endif
+
+QString SerialPortWatcher::portIdentifier(const SerialPortInfo &info) const
+{
+    return portIdentifier(info.portName, info.systemLocation);
+}
+
+QString SerialPortWatcher::portIdentifier(const QString &portName, const QString &systemLocation) const
+{
+    return !systemLocation.isEmpty() ? systemLocation : portName;
+}
+
+SerialPortInfo SerialPortWatcher::resolvePortFromProperties(const QVariantMap &properties) const
+{
+    QString devnode = properties.value(QStringLiteral("devnode")).toString();
+    QString portName = properties.value(QStringLiteral("portName")).toString();
+
+    if (portName.isEmpty())
+    {
+        portName = portNameFromSystemLocation(devnode);
+    }
+
+    const QList<QSerialPortInfo> availablePorts = QSerialPortInfo::availablePorts();
+    for (const QSerialPortInfo &info : availablePorts)
+    {
+        if ((!devnode.isEmpty() && info.systemLocation() == devnode) ||
+            (!portName.isEmpty() && info.portName() == portName))
+        {
+            return toSerialPortInfo(info);
+        }
+    }
+
+    SerialPortInfo fallback;
+    fallback.portName = portName;
+    fallback.systemLocation = devnode;
+    fallback.description = properties.value(QStringLiteral("description")).toString();
+    fallback.serialNumber = properties.value(QStringLiteral("serialNumber")).toString();
+    fallback.vendorId = parseHexQuint16(properties.value(QStringLiteral("vendorId")));
+    fallback.productId = parseHexQuint16(properties.value(QStringLiteral("productId")));
+    return fallback;
+}
+
+SerialPortInfo SerialPortWatcher::takeKnownPort(const QString &portName, const QString &systemLocation)
+{
+    const QString key = portIdentifier(portName, systemLocation);
+    if (!key.isEmpty())
+    {
+        auto it = m_knownPorts.find(key);
+        if (it != m_knownPorts.end())
+        {
+            SerialPortInfo info = it.value();
+            m_knownPorts.erase(it);
+            return info;
+        }
+    }
+
+    if (!systemLocation.isEmpty())
+    {
+        for (auto it = m_knownPorts.begin(); it != m_knownPorts.end(); ++it)
+        {
+            if (it.value().systemLocation == systemLocation)
+            {
+                SerialPortInfo info = it.value();
+                m_knownPorts.erase(it);
+                return info;
+            }
+        }
+    }
+
+    if (!portName.isEmpty())
+    {
+        for (auto it = m_knownPorts.begin(); it != m_knownPorts.end(); ++it)
+        {
+            if (it.value().portName == portName)
+            {
+                SerialPortInfo info = it.value();
+                m_knownPorts.erase(it);
+                return info;
+            }
+        }
+    }
+
+    SerialPortInfo fallback;
+    fallback.portName = portName;
+    fallback.systemLocation = systemLocation;
+    return fallback;
+}
+
+} // namespace devices::serial
+

+ 79 - 0
src/devices/serial/SerialPortWatcher.h

@@ -0,0 +1,79 @@
+#pragma once
+
+#include <QHash>
+#include <QPointer>
+#include <QSerialPortInfo>
+#include <QVariantMap>
+#include <memory>
+
+#include "devices/DeviceWatcherBase.h"
+#include "devices/serial/SerialPortInfo.h"
+
+QT_BEGIN_NAMESPACE
+class QTimer;
+QT_END_NAMESPACE
+
+#ifdef Q_OS_LINUX
+#ifdef linux
+#undef linux
+#endif
+namespace devices
+{
+namespace platform
+{
+namespace linux_os
+{
+class UdevMonitor;
+}
+} // namespace platform
+} // namespace devices
+#endif
+
+namespace devices::serial
+{
+
+class SerialPortWatcher : public devices::DeviceWatcherBase
+{
+    Q_OBJECT
+
+public:
+    explicit SerialPortWatcher(QObject *parent = nullptr);
+    ~SerialPortWatcher() override;
+
+    QList<SerialPortInfo> knownPorts() const;
+
+signals:
+    void serialPortArrived(const devices::serial::SerialPortInfo &info);
+    void serialPortRemoved(const devices::serial::SerialPortInfo &info);
+
+protected:
+    bool doStart() override;
+    void doStop() override;
+
+private:
+    void emitPortArrived(const SerialPortInfo &info);
+    void emitPortRemoved(const SerialPortInfo &info);
+    void refreshPorts();
+    SerialPortInfo toSerialPortInfo(const QSerialPortInfo &info) const;
+    QString portIdentifier(const SerialPortInfo &info) const;
+    QString portIdentifier(const QString &portName, const QString &systemLocation) const;
+    SerialPortInfo resolvePortFromProperties(const QVariantMap &properties) const;
+    SerialPortInfo takeKnownPort(const QString &portName, const QString &systemLocation);
+
+    QHash<QString, SerialPortInfo> m_knownPorts;
+    QPointer<QTimer> m_pollTimer;
+
+#ifdef Q_OS_LINUX
+    std::unique_ptr<devices::platform::linux_os::UdevMonitor> m_udevMonitor;
+    bool startUdevMonitor();
+    void stopUdevMonitor();
+    void handleUdevAdd(const QVariantMap &properties);
+    void handleUdevRemove(const QVariantMap &properties);
+#else
+    inline bool startUdevMonitor() { return false; }
+    inline void stopUdevMonitor() {}
+#endif
+};
+
+} // namespace devices::serial
+

+ 23 - 0
src/devices/serial/protocol.py

@@ -0,0 +1,23 @@
+protocol ModbusTCP {
+    version: "1.0"
+    transport: TCP
+    port: 502
+    
+    message ReadHoldingRegisters {
+        function_code: 0x03
+        request {
+            starting_address: uint16
+            quantity: uint16
+        }
+        response {
+            byte_count: uint8
+            register_values: byte[byte_count]
+        }
+    }
+    
+    qos {
+        priority: HIGH
+        timeout: 2000ms
+        retry_count: 3
+    }
+}

+ 22 - 0
src/devices/usb/UsbDeviceInfo.h

@@ -0,0 +1,22 @@
+#pragma once
+
+#include <QMetaType>
+#include <QString>
+
+namespace devices::usb
+{
+
+struct UsbDeviceInfo
+{
+    QString devicePath;
+    QString productName;
+    QString manufacturer;
+    QString serialNumber;
+    quint16 vendorId = 0;
+    quint16 productId = 0;
+};
+
+} // namespace devices::usb
+
+Q_DECLARE_METATYPE(devices::usb::UsbDeviceInfo)
+

+ 26 - 0
src/devices/usb/UsbDeviceWatcher.cpp

@@ -0,0 +1,26 @@
+#include "devices/usb/UsbDeviceWatcher.h"
+
+namespace devices::usb
+{
+
+UsbDeviceWatcher::UsbDeviceWatcher(QObject *parent)
+    : DeviceWatcherBase(parent)
+{
+    qRegisterMetaType<UsbDeviceInfo>("devices::usb::UsbDeviceInfo");
+}
+
+UsbDeviceWatcher::~UsbDeviceWatcher() = default;
+
+bool UsbDeviceWatcher::doStart()
+{
+    // Placeholder implementation. Real USB monitoring will be added later.
+    return true;
+}
+
+void UsbDeviceWatcher::doStop()
+{
+    // Placeholder
+}
+
+} // namespace devices::usb
+

+ 27 - 0
src/devices/usb/UsbDeviceWatcher.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include "devices/DeviceWatcherBase.h"
+#include "devices/usb/UsbDeviceInfo.h"
+
+namespace devices::usb
+{
+
+class UsbDeviceWatcher : public devices::DeviceWatcherBase
+{
+    Q_OBJECT
+
+public:
+    explicit UsbDeviceWatcher(QObject *parent = nullptr);
+    ~UsbDeviceWatcher() override;
+
+signals:
+    void usbDeviceArrived(const UsbDeviceInfo &info);
+    void usbDeviceRemoved(const UsbDeviceInfo &info);
+
+protected:
+    bool doStart() override;
+    void doStop() override;
+};
+
+} // namespace devices::usb
+