2 次代码提交 57c84aaf4e ... 7684979972

作者 SHA1 备注 提交日期
  flower_linux 7684979972 平台兼容 6 天之前
  flower_linux 5bb09cd2ff 轮训查设备 1 月之前

+ 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
+

+ 181 - 2
src/serial_manager/serial_manager.cpp

@@ -7,6 +7,7 @@
 #include <QJsonDocument>
 #include <QJsonArray>
 #include <QJsonObject>
+#include <algorithm>
 
 SerialManager::SerialManager(SoftBusCore *busCore, QObject *parent)
     : QObject(parent),
@@ -186,6 +187,14 @@ QStringList SerialManager::getAvailablePorts() const
 
 bool SerialManager::isRealSerialPort(const QSerialPortInfo &info) const
 {
+    const QString portName = info.portName();
+
+    // 如果是我们已经追踪或正在使用的端口,无论是否能立即打开,都视为真实串口
+    if (m_openedPorts.contains(portName) || m_lastDiscoveredPorts.contains(portName))
+    {
+        return true;
+    }
+
     // 1. 尝试打开串口进行验证
     QSerialPort testPort;
     testPort.setPort(info);
@@ -223,8 +232,12 @@ bool SerialManager::isRealSerialPort(const QSerialPortInfo &info) const
     // 3. 检查是否有物理设备属性
     if (info.vendorIdentifier() == 0 && info.productIdentifier() == 0)
     {
-        // 没有硬件ID的可能是虚拟端口
-        return false;
+        // 没有硬件ID的可能是虚拟端口,但也可能是被占用的真实设备
+        // 缓和判定:仅当描述同样为空时才判定为虚拟端口
+        if (info.description().isEmpty() && info.manufacturer().isEmpty())
+        {
+            return false;
+        }
     }
 
     // 4. 检查串口是否在系统设备列表中
@@ -281,12 +294,19 @@ void SerialManager::discoverSerialPorts()
     QSet<QString> currentPorts;
     foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
     {
+        if (!isRealSerialPort(info))
+        {
+            // qDebug() << "Skip virtual serial port:" << info.portName() << "-" << info.description();
+            continue;
+        }
+
         QString portName = info.portName();
         currentPorts.insert(portName);
 
         // 如果是新发现的端口
         if (!m_lastDiscoveredPorts.contains(portName))
         {
+            registerDiscoveredPort(info);
             emit portDiscovered(portName, info.description());
             qDebug() << "New port discovered:" << portName << "-" << info.description();
         }
@@ -301,6 +321,10 @@ void SerialManager::discoverSerialPorts()
         {
             closeSerialPort(portName);
         }
+        if (m_busCore)
+        {
+            m_busCore->markDeviceOfflineByPort(portName);
+        }
         emit portRemoved(portName);
         qDebug() << "Port removed:" << portName;
     }
@@ -375,6 +399,161 @@ void SerialManager::initThreads()
     }
 }
 
+void SerialManager::registerDiscoveredPort(const QSerialPortInfo &info)
+{
+    if (!m_busCore)
+    {
+        qDebug() << "Bus core is not set; skip registering port" << info.portName();
+        return;
+    }
+
+    if (!isRealSerialPort(info))
+    {
+        qDebug() << "Skip registering non-real serial port" << info.portName();
+        return;
+    }
+
+    const QString portName = info.portName();
+    DeviceInfo existingDevice = m_busCore->getDeviceInfoByPortname(portName);
+    if (existingDevice.id != 0)
+    {
+        // 已经注册的设备,如果描述或属性发生变化,更新一次
+        QString desiredName = info.description().isEmpty()
+                                  ? QStringLiteral("串口设备: %1").arg(portName)
+                                  : info.description();
+
+        QJsonObject updatedProperties = existingDevice.properties;
+        auto mergeProperty = [&updatedProperties](const QString &key, const QString &value) {
+            if (value.isEmpty())
+            {
+                return false;
+            }
+            if (!updatedProperties.contains(key) || updatedProperties.value(key).toString() != value)
+            {
+                updatedProperties.insert(key, value);
+                return true;
+            }
+            return false;
+        };
+
+        bool propertyChanged = false;
+        propertyChanged |= mergeProperty(QStringLiteral("description"), info.description());
+        propertyChanged |= mergeProperty(QStringLiteral("manufacturer"), info.manufacturer());
+        propertyChanged |= mergeProperty(QStringLiteral("serialNumber"), info.serialNumber());
+        propertyChanged |= mergeProperty(QStringLiteral("systemLocation"), info.systemLocation());
+
+        // vendorIdentifier / productIdentifier 可能为0,但仍然写入,便于调试
+        const QString vendorId = info.hasVendorIdentifier()
+                                     ? QString::number(info.vendorIdentifier(), 16)
+                                     : QString();
+        const QString productId = info.hasProductIdentifier()
+                                      ? QString::number(info.productIdentifier(), 16)
+                                      : QString();
+        propertyChanged |= mergeProperty(QStringLiteral("vendorId"), vendorId);
+        propertyChanged |= mergeProperty(QStringLiteral("productId"), productId);
+
+        bool nameChanged = existingDevice.name != desiredName;
+
+        const bool needFullUpdate = nameChanged || propertyChanged;
+        if (needFullUpdate)
+        {
+            existingDevice.name = desiredName;
+            existingDevice.properties = updatedProperties;
+            existingDevice.status = QStringLiteral("online");
+            existingDevice.isActive = true;
+            existingDevice.lastSeen = QDateTime::currentDateTimeUtc();
+            m_busCore->updateDevice(existingDevice);
+            qDebug() << "Updated existing serial device registration for port" << portName;
+        }
+
+        if (!needFullUpdate)
+        {
+            m_busCore->updateDeviceStatus(existingDevice.id, QStringLiteral("online"), true,
+                                          QDateTime::currentDateTimeUtc());
+        }
+        return;
+    }
+
+    DeviceInfo device;
+    device.id = generateNextDeviceId();
+    if (device.id <= 0)
+    {
+        qWarning() << "Failed to allocate device ID for port" << portName;
+        return;
+    }
+
+    device.portname = portName;
+    device.type = QStringLiteral("serial");
+    device.protocol = QStringLiteral("modbus");
+    device.protocol_detail = QStringLiteral("modbus-rtu");
+    device.address = 0;
+    device.status = QStringLiteral("online");
+    device.isActive = true;
+    device.lastSeen = QDateTime::currentDateTimeUtc();
+
+    if (info.description().isEmpty())
+    {
+        device.name = QStringLiteral("串口设备: %1").arg(portName);
+    }
+    else if (info.description().contains(portName))
+    {
+        device.name = info.description();
+    }
+    else
+    {
+        device.name = QStringLiteral("%1 (%2)").arg(info.description(), portName);
+    }
+
+    QJsonObject properties;
+    if (!info.description().isEmpty())
+    {
+        properties.insert(QStringLiteral("description"), info.description());
+    }
+    if (!info.manufacturer().isEmpty())
+    {
+        properties.insert(QStringLiteral("manufacturer"), info.manufacturer());
+    }
+    if (!info.serialNumber().isEmpty())
+    {
+        properties.insert(QStringLiteral("serialNumber"), info.serialNumber());
+    }
+    if (!info.systemLocation().isEmpty())
+    {
+        properties.insert(QStringLiteral("systemLocation"), info.systemLocation());
+    }
+    if (info.hasVendorIdentifier())
+    {
+        properties.insert(QStringLiteral("vendorId"), QString::number(info.vendorIdentifier(), 16));
+    }
+    if (info.hasProductIdentifier())
+    {
+        properties.insert(QStringLiteral("productId"), QString::number(info.productIdentifier(), 16));
+    }
+
+    device.properties = properties;
+
+    m_busCore->registerDevice(device);
+    m_busCore->updateDeviceStatus(device.id, QStringLiteral("online"), true,
+                                  device.lastSeen);
+    qDebug() << "Registered serial device in bus core for port" << portName << "with id" << device.id;
+}
+
+int SerialManager::generateNextDeviceId() const
+{
+    if (!m_busCore)
+    {
+        return -1;
+    }
+
+    const QList<DeviceInfo> devices = m_busCore->getAllDevices();
+    int maxId = 0;
+    for (const DeviceInfo &device : devices)
+    {
+        maxId = std::max(maxId, device.id);
+    }
+    return maxId + 1;
+}
+
 void SerialManager::cleanupThreads()
 {
     // 停止并清理存储线程

+ 4 - 0
src/serial_manager/serial_manager.h

@@ -105,6 +105,10 @@ private:
     void initThreads();
     // 清理线程
     void cleanupThreads();
+    // 为新发现的串口注册软总线设备
+    void registerDiscoveredPort(const QSerialPortInfo &info);
+    // 计算下一个可用的设备ID
+    int generateNextDeviceId() const;
 
 private:
     // 软总线核心

+ 236 - 24
src/soft_bus_core/soft_bus_core.cpp

@@ -102,29 +102,132 @@ void SoftBusCore::registerDevice(const DeviceInfo &device) {
         return;
     }
 
-    m_devices.insert(device.id, device);
+    DeviceInfo normalizedDevice = device;
+    if (normalizedDevice.status.isEmpty()) {
+        normalizedDevice.status = QStringLiteral("online");
+    }
+    normalizedDevice.isActive = true;
+    if (!normalizedDevice.lastSeen.isValid()) {
+        normalizedDevice.lastSeen = QDateTime::currentDateTimeUtc();
+    }
+
+    // 如果未显式指定名称,则尝试沿用传入数据
+    if (normalizedDevice.name.isEmpty()) {
+        normalizedDevice.name = device.name;
+    }
+
+    int existingDeviceId = -1;
+    DeviceInfo persistedDevice;
+    const bool hasPort =
+        !normalizedDevice.portname.isEmpty();
+    if (hasPort) {
+        if (m_portnameToId.contains(normalizedDevice.portname)) {
+            existingDeviceId = m_portnameToId.value(normalizedDevice.portname);
+            if (m_devices.contains(existingDeviceId)) {
+                persistedDevice = m_devices.value(existingDeviceId);
+            } else {
+                // 内存缺失该设备,移除映射,稍后尝试从数据库查询
+                m_portnameToId.remove(normalizedDevice.portname);
+                existingDeviceId = -1;
+            }
+        }
+
+        if (existingDeviceId <= 0 && m_dbInitialized) {
+            if (fetchPersistedDeviceByPort(normalizedDevice.portname,
+                                           persistedDevice)) {
+                existingDeviceId = persistedDevice.id;
+                if (existingDeviceId > 0) {
+                    m_devices.insert(existingDeviceId, persistedDevice);
+                    m_portnameToId[normalizedDevice.portname] = existingDeviceId;
+                }
+            }
+        }
+    }
+
+    if (existingDeviceId > 0) {
+        DeviceInfo mergedDevice = normalizedDevice;
+        mergedDevice.id = existingDeviceId;
+        updateDevice(mergedDevice);
+        return;
+    }
+
+    m_devices.insert(normalizedDevice.id, normalizedDevice);
 
     // 如果设备有端口名,建立映射关系
-    if (!device.portname.isEmpty()) {
-        m_portnameToId[device.portname] = device.id;
+    if (!normalizedDevice.portname.isEmpty()) {
+        m_portnameToId[normalizedDevice.portname] = normalizedDevice.id;
     }
 
     if (m_dbInitialized) {
         // 先同步设备信息,再确保端口对应的原始/转化数据表存在
-        upsertDeviceRecord(device);
-        if (!device.portname.isEmpty()) {
-            if (!ensurePerPortRawTable(device.portname)) {
+        upsertDeviceRecord(normalizedDevice, normalizedDevice.lastSeen);
+        if (!normalizedDevice.portname.isEmpty()) {
+            if (!ensurePerPortRawTable(normalizedDevice.portname)) {
                 qCritical() << "Failed to ensure raw data table for port"
-                            << device.portname;
+                            << normalizedDevice.portname;
             }
-            if (!ensurePerPortTransformedTable(device.portname)) {
+            if (!ensurePerPortTransformedTable(normalizedDevice.portname)) {
                 qCritical() << "Failed to ensure transformed data table for port"
-                            << device.portname;
+                            << normalizedDevice.portname;
             }
         }
     }
 
-    emit deviceRegistered(device);
+    emit deviceRegistered(normalizedDevice);
+}
+
+bool SoftBusCore::fetchPersistedDeviceByPort(const QString &portName,
+                                             DeviceInfo &device) {
+    if (!m_dbInitialized || !m_db.isValid() || !m_db.isOpen() ||
+        portName.isEmpty()) {
+        return false;
+    }
+
+    QSqlQuery query(m_db);
+    query.prepare(QStringLiteral(
+        "SELECT device_id, port_name, name, device_type, address, protocol, "
+        "protocol_detail, properties, status, is_active, last_seen "
+        "FROM devices WHERE port_name = :port_name LIMIT 1"));
+    query.bindValue(QStringLiteral(":port_name"), portName);
+    if (!query.exec()) {
+        qCritical() << "Failed to query persisted device for port" << portName
+                    << ":" << query.lastError().text();
+        return false;
+    }
+
+    if (!query.next()) {
+        return false;
+    }
+
+    DeviceInfo info;
+    info.id = query.value(0).toInt();
+    info.portname = query.value(1).toString();
+    info.name = query.value(2).toString();
+    info.type = query.value(3).toString();
+    info.address = query.value(4).toInt();
+    info.protocol = query.value(5).toString();
+    info.protocol_detail = query.value(6).toString();
+
+    const QString propertiesString = query.value(7).toString();
+    if (!propertiesString.isEmpty()) {
+        const QJsonDocument doc =
+            QJsonDocument::fromJson(propertiesString.toUtf8());
+        if (doc.isObject()) {
+            info.properties = doc.object();
+        }
+    }
+
+    info.status = query.value(8).toString();
+    info.isActive = query.value(9).toBool();
+    info.lastSeen = query.value(10).toDateTime();
+
+    if (info.status.isEmpty()) {
+        info.status =
+            info.isActive ? QStringLiteral("online") : QStringLiteral("offline");
+    }
+
+    device = info;
+    return true;
 }
 
 // 更新设备
@@ -151,34 +254,134 @@ void SoftBusCore::updateDevice(const DeviceInfo &device) {
 
     // 如果端口名发生变化,更新映射
     DeviceInfo oldDevice = m_devices[device.id];
-    if (oldDevice.portname != device.portname) {
+    DeviceInfo updatedDevice = device;
+    if (oldDevice.portname != updatedDevice.portname) {
         if (!oldDevice.portname.isEmpty()) {
             m_portnameToId.remove(oldDevice.portname);
         }
-        if (!device.portname.isEmpty()) {
-            m_portnameToId[device.portname] = device.id;
+        if (!updatedDevice.portname.isEmpty()) {
+            m_portnameToId[updatedDevice.portname] = updatedDevice.id;
         }
     }
 
-    m_devices[device.id] = device;
+    if (updatedDevice.status.isEmpty()) {
+        updatedDevice.status =
+            oldDevice.status.isEmpty() ? QStringLiteral("online") : oldDevice.status;
+    }
+    if (!updatedDevice.lastSeen.isValid() && oldDevice.lastSeen.isValid()) {
+        updatedDevice.lastSeen = oldDevice.lastSeen;
+    }
+    // 如果调用方未显式修改活跃状态,则沿用旧值
+    if (device.isActive == oldDevice.isActive) {
+        updatedDevice.isActive = oldDevice.isActive;
+    }
+
+    m_devices[device.id] = updatedDevice;
 
     if (m_dbInitialized) {
-        upsertDeviceRecord(device);
-        if (!device.portname.isEmpty()) {
-            if (!ensurePerPortRawTable(device.portname)) {
+        upsertDeviceRecord(updatedDevice, updatedDevice.lastSeen);
+        if (!updatedDevice.portname.isEmpty()) {
+            if (!ensurePerPortRawTable(updatedDevice.portname)) {
                 qCritical() << "Failed to ensure raw data table for port"
-                            << device.portname;
+                            << updatedDevice.portname;
             }
-            if (!ensurePerPortTransformedTable(device.portname)) {
+            if (!ensurePerPortTransformedTable(updatedDevice.portname)) {
                 qCritical() << "Failed to ensure transformed data table for port"
-                            << device.portname;
+                            << updatedDevice.portname;
             }
         }
     }
 
+    emit deviceUpdate(updatedDevice);
+}
+
+void SoftBusCore::markDeviceOffline(int deviceId) {
+    if (deviceId <= 0) {
+        qWarning() << "Invalid device id when marking offline";
+        return;
+    }
+
+    const bool tracked = m_devices.contains(deviceId);
+    if (!tracked) {
+        qWarning() << "Device" << deviceId
+                   << "not tracked in memory when marking offline";
+        if (m_dbInitialized) {
+            markDeviceInactive(deviceId);
+        }
+        return;
+    }
+
+    updateDeviceStatus(deviceId, QStringLiteral("offline"), false,
+                       QDateTime::currentDateTimeUtc());
+}
+
+void SoftBusCore::markDeviceOfflineByPort(const QString &portname) {
+    if (portname.isEmpty()) {
+        qWarning() << "Empty port name when marking device offline";
+        return;
+    }
+
+    if (!m_portnameToId.contains(portname)) {
+        qWarning() << "No registered device found for port" << portname
+                   << "when marking offline";
+        return;
+    }
+
+    const int deviceId = m_portnameToId.value(portname);
+    markDeviceOffline(deviceId);
+}
+
+void SoftBusCore::updateDeviceStatus(int deviceId, const QString &status,
+                                     bool isActive, const QDateTime &lastSeen) {
+    if (deviceId <= 0) {
+        qWarning() << "Invalid device id when updating status";
+        return;
+    }
+
+    if (!m_devices.contains(deviceId)) {
+        qWarning() << "Device" << deviceId
+                   << "not tracked in memory when updating status";
+        return;
+    }
+
+    DeviceInfo device = m_devices.value(deviceId);
+    if (!status.isEmpty()) {
+        device.status = status;
+    }
+    device.isActive = isActive;
+    if (lastSeen.isValid()) {
+        device.lastSeen = lastSeen;
+    } else if (!device.lastSeen.isValid()) {
+        device.lastSeen = QDateTime::currentDateTimeUtc();
+    }
+
+    m_devices[deviceId] = device;
+
+    if (m_dbInitialized) {
+        upsertDeviceRecord(device, device.lastSeen);
+    }
+
     emit deviceUpdate(device);
 }
 
+void SoftBusCore::updateDeviceStatusByPort(const QString &portname,
+                                           const QString &status, bool isActive,
+                                           const QDateTime &lastSeen) {
+    if (portname.isEmpty()) {
+        qWarning() << "Empty port name when updating device status";
+        return;
+    }
+
+    if (!m_portnameToId.contains(portname)) {
+        qWarning() << "No registered device found for port" << portname
+                   << "when updating status";
+        return;
+    }
+
+    const int deviceId = m_portnameToId.value(portname);
+    updateDeviceStatus(deviceId, status, isActive, lastSeen);
+}
+
 // 注销设备
 /* 
     参数:
@@ -1162,10 +1365,18 @@ void SoftBusCore::upsertDeviceRecord(const DeviceInfo &device,
     query.bindValue(QStringLiteral(":protocol_detail"), device.protocol_detail);
     query.bindValue(QStringLiteral(":properties"),
                     toJsonVariant(device.properties));
-    query.bindValue(QStringLiteral(":status"), QStringLiteral("online"));
-    query.bindValue(QStringLiteral(":is_active"), true);
-    if (lastSeen.isValid()) {
-        query.bindValue(QStringLiteral(":last_seen"), lastSeen);
+    const QString statusValue = device.status.isEmpty()
+                                    ? QStringLiteral("online")
+                                    : device.status;
+    query.bindValue(QStringLiteral(":status"), statusValue);
+    query.bindValue(QStringLiteral(":is_active"), device.isActive);
+
+    QDateTime effectiveLastSeen = lastSeen;
+    if (!effectiveLastSeen.isValid() && device.lastSeen.isValid()) {
+        effectiveLastSeen = device.lastSeen;
+    }
+    if (effectiveLastSeen.isValid()) {
+        query.bindValue(QStringLiteral(":last_seen"), effectiveLastSeen);
     } else {
         query.bindValue(QStringLiteral(":last_seen"), QVariant(QVariant::DateTime));
     }
@@ -1284,3 +1495,4 @@ int SoftBusCore::loadEnvOrDefault(const char *envVar, int defaultValue) const {
     const int parsed = value.toInt(&ok);
     return ok ? parsed : defaultValue;
 }
+

+ 21 - 4
src/soft_bus_core/soft_bus_core.h

@@ -21,15 +21,23 @@ struct DeviceInfo {
   QString
       protocol_detail; // 协议详情: "modbus-rtu", "modbus-tcp", "modbus-ascii"
   QJsonObject properties; // 其他属性
+  QString status;         // 当前状态: "online", "offline", "error" 等
+  bool isActive;          // 是否活跃
+  QDateTime lastSeen;     // 最近一次活跃时间
 
-  DeviceInfo() : id(0) {}
+  DeviceInfo()
+      : id(0), status(QStringLiteral("online")), isActive(true) {}
   DeviceInfo(const int &id, const QString &type, const int &address = 0,
              const QString &protocol = "", const QString &protocol_detail = "",
              const QString &portname = "",
-             const QJsonObject &properties = QJsonObject())
-      : id(id), portname(portname), type(type), address(address),
+             const QJsonObject &properties = QJsonObject(),
+             const QString &status = QStringLiteral("online"),
+             bool isActive = true,
+             const QDateTime &lastSeen = QDateTime())
+      : id(id), portname(portname), name(), type(type), address(address),
         protocol(protocol), protocol_detail(protocol_detail),
-        properties(properties) {}
+        properties(properties), status(status), isActive(isActive),
+        lastSeen(lastSeen) {}
 };
 
 // 软总线消息结构
@@ -61,6 +69,14 @@ public:
   void registerDevice(const DeviceInfo &device);
   void unregisterDevice(int deviceId);
   void updateDevice(const DeviceInfo &device);
+  void markDeviceOffline(int deviceId);
+  void markDeviceOfflineByPort(const QString &portname);
+  void updateDeviceStatus(int deviceId, const QString &status,
+                          bool isActive = true,
+                          const QDateTime &lastSeen = QDateTime());
+  void updateDeviceStatusByPort(const QString &portname,
+                                const QString &status, bool isActive = true,
+                                const QDateTime &lastSeen = QDateTime());
   DeviceInfo getDeviceInfo(int deviceId) const;
   DeviceInfo getDeviceInfoByPortname(const QString &portname) const;
   QList<DeviceInfo> getAllDevices() const;
@@ -109,6 +125,7 @@ private:
   bool ensurePerPortTransformedTable(const QString &portName);
   void upsertDeviceRecord(const DeviceInfo &device,
                           const QDateTime &lastSeen = QDateTime());
+  bool fetchPersistedDeviceByPort(const QString &portName, DeviceInfo &device);
   void markDeviceInactive(int deviceId);
   QString sanitizeIdentifier(const QString &identifier) const;
   QString buildRawTableName(const QString &portName) const;