From 90ad86b4d01cb807581162687a333cd04a58016c Mon Sep 17 00:00:00 2001
From: flower_linux <737666956@qq.com>
Date: Wed, 13 May 2026 16:46:07 +0800
Subject: [PATCH] init
---
.gitignore | 2 +
CMakeLists.txt | 89 +++
CMakeLists.txt.user | 308 +++++++++
README.md | 85 +++
config/edge_manifest.example.json | 14 +
config/edge_manifest.schema.json | 22 +
docs/UPLINK_PROTOCOL_v1.md | 7 +
src/core/memory/MemoryPool.h | 98 +++
src/core/models/MessageRoutingUtils.h | 59 ++
src/core/models/PayloadRef.h | 35 +
src/core/models/RawBusMessage.h | 80 +++
src/device_bus/EdgeDeviceBus.cpp | 222 +++++++
src/device_bus/EdgeDeviceBus.h | 61 ++
src/device_bus/manager/CanDeviceManager.cpp | 105 +++
src/device_bus/manager/CanDeviceManager.h | 31 +
.../manager/SerialDeviceManager.cpp | 152 +++++
src/device_bus/manager/SerialDeviceManager.h | 32 +
src/device_bus/monitor/DeviceEvent.h | 50 ++
.../monitor/EdgeDiscoveryCoordinator.cpp | 286 ++++++++
.../monitor/EdgeDiscoveryCoordinator.h | 81 +++
src/device_bus/monitor/IDeviceMonitor.h | 47 ++
src/device_bus/monitor/UdevDeviceMonitor.cpp | 362 ++++++++++
src/device_bus/monitor/UdevDeviceMonitor.h | 61 ++
src/device_bus/tree/DeviceTreeModel.cpp | 619 ++++++++++++++++++
src/device_bus/tree/DeviceTreeModel.h | 89 +++
src/devices/DeviceTypes.cpp | 66 ++
src/devices/DeviceTypes.h | 107 +++
src/devices/base/IDevice.h | 17 +
src/devices/base/IPhysicalDevice.h | 19 +
src/devices/physical/CanDevice.cpp | 147 +++++
src/devices/physical/CanDevice.h | 53 ++
src/devices/physical/SerialDevice.cpp | 145 ++++
src/devices/physical/SerialDevice.h | 61 ++
src/fake_driver.cpp | 20 +
src/fake_driver.hpp | 22 +
.../drivers/serial/SerialCapabilities.h | 67 ++
.../drivers/serial/SerialQtDriver.cpp | 187 ++++++
src/hardware/drivers/serial/SerialQtDriver.h | 93 +++
src/hardware/interfaces/IDeviceWatcher.h | 37 ++
src/hardware/interfaces/IFrameDriver.h | 41 ++
src/hardware/interfaces/IStreamDriver.h | 35 +
src/main.cpp | 213 ++++++
src/message_bus/ingress/EdgeSession.cpp | 91 +++
src/message_bus/ingress/EdgeSession.h | 41 ++
src/message_bus/ingress/EdgeTcpUplinkPort.cpp | 53 ++
src/message_bus/ingress/EdgeTcpUplinkPort.h | 28 +
src/message_bus/ingress/EdgeUplinkWire.cpp | 178 +++++
src/message_bus/ingress/EdgeUplinkWire.h | 65 ++
src/message_bus/ingress/IIngressPort.h | 19 +
src/ops_ui_http.cpp | 374 +++++++++++
src/ops_ui_http.hpp | 39 ++
src/utils/logs/logging.h | 36 +
52 files changed, 5251 insertions(+)
create mode 100644 .gitignore
create mode 100644 CMakeLists.txt
create mode 100644 CMakeLists.txt.user
create mode 100644 README.md
create mode 100644 config/edge_manifest.example.json
create mode 100644 config/edge_manifest.schema.json
create mode 100644 docs/UPLINK_PROTOCOL_v1.md
create mode 100644 src/core/memory/MemoryPool.h
create mode 100644 src/core/models/MessageRoutingUtils.h
create mode 100644 src/core/models/PayloadRef.h
create mode 100644 src/core/models/RawBusMessage.h
create mode 100644 src/device_bus/EdgeDeviceBus.cpp
create mode 100644 src/device_bus/EdgeDeviceBus.h
create mode 100644 src/device_bus/manager/CanDeviceManager.cpp
create mode 100644 src/device_bus/manager/CanDeviceManager.h
create mode 100644 src/device_bus/manager/SerialDeviceManager.cpp
create mode 100644 src/device_bus/manager/SerialDeviceManager.h
create mode 100644 src/device_bus/monitor/DeviceEvent.h
create mode 100644 src/device_bus/monitor/EdgeDiscoveryCoordinator.cpp
create mode 100644 src/device_bus/monitor/EdgeDiscoveryCoordinator.h
create mode 100644 src/device_bus/monitor/IDeviceMonitor.h
create mode 100644 src/device_bus/monitor/UdevDeviceMonitor.cpp
create mode 100644 src/device_bus/monitor/UdevDeviceMonitor.h
create mode 100644 src/device_bus/tree/DeviceTreeModel.cpp
create mode 100644 src/device_bus/tree/DeviceTreeModel.h
create mode 100644 src/devices/DeviceTypes.cpp
create mode 100644 src/devices/DeviceTypes.h
create mode 100644 src/devices/base/IDevice.h
create mode 100644 src/devices/base/IPhysicalDevice.h
create mode 100644 src/devices/physical/CanDevice.cpp
create mode 100644 src/devices/physical/CanDevice.h
create mode 100644 src/devices/physical/SerialDevice.cpp
create mode 100644 src/devices/physical/SerialDevice.h
create mode 100644 src/fake_driver.cpp
create mode 100644 src/fake_driver.hpp
create mode 100644 src/hardware/drivers/serial/SerialCapabilities.h
create mode 100644 src/hardware/drivers/serial/SerialQtDriver.cpp
create mode 100644 src/hardware/drivers/serial/SerialQtDriver.h
create mode 100644 src/hardware/interfaces/IDeviceWatcher.h
create mode 100644 src/hardware/interfaces/IFrameDriver.h
create mode 100644 src/hardware/interfaces/IStreamDriver.h
create mode 100644 src/main.cpp
create mode 100644 src/message_bus/ingress/EdgeSession.cpp
create mode 100644 src/message_bus/ingress/EdgeSession.h
create mode 100644 src/message_bus/ingress/EdgeTcpUplinkPort.cpp
create mode 100644 src/message_bus/ingress/EdgeTcpUplinkPort.h
create mode 100644 src/message_bus/ingress/EdgeUplinkWire.cpp
create mode 100644 src/message_bus/ingress/EdgeUplinkWire.h
create mode 100644 src/message_bus/ingress/IIngressPort.h
create mode 100644 src/ops_ui_http.cpp
create mode 100644 src/ops_ui_http.hpp
create mode 100644 src/utils/logs/logging.h
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..65af97a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/build
+/.cache
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..2eb93ec
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,89 @@
+cmake_minimum_required(VERSION 3.19)
+project(softbus_edge_agent LANGUAGES CXX VERSION 0.1.0)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_AUTOMOC ON)
+
+find_package(Qt6 6.5 REQUIRED COMPONENTS Core Network SerialPort)
+find_package(Qt6 QUIET COMPONENTS SerialBus)
+if(TARGET Qt6::SerialBus)
+ set(SOFTBUS_HAVE_CAN 1)
+else()
+ set(SOFTBUS_HAVE_CAN 0)
+ message(STATUS "Qt6 SerialBus not found: building edge_agent without SocketCAN (CAN reconcile disabled)")
+endif()
+
+qt_standard_project_setup()
+
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+
+set(EDGE_AGENT_SOURCES
+ src/main.cpp
+ src/fake_driver.cpp
+ src/fake_driver.hpp
+ src/device_bus/monitor/DeviceEvent.h
+ src/device_bus/monitor/IDeviceMonitor.h
+ src/device_bus/monitor/UdevDeviceMonitor.cpp
+ src/device_bus/monitor/UdevDeviceMonitor.h
+ src/device_bus/monitor/EdgeDiscoveryCoordinator.cpp
+ src/device_bus/monitor/EdgeDiscoveryCoordinator.h
+ src/ops_ui_http.cpp
+ src/ops_ui_http.hpp
+ src/device_bus/EdgeDeviceBus.cpp
+ src/device_bus/EdgeDeviceBus.h
+ src/device_bus/tree/DeviceTreeModel.cpp
+ src/device_bus/tree/DeviceTreeModel.h
+ src/device_bus/manager/SerialDeviceManager.cpp
+ src/device_bus/manager/SerialDeviceManager.h
+ src/devices/DeviceTypes.cpp
+ src/devices/DeviceTypes.h
+ src/devices/physical/SerialDevice.cpp
+ src/devices/physical/SerialDevice.h
+ src/hardware/drivers/serial/SerialQtDriver.cpp
+ src/hardware/drivers/serial/SerialQtDriver.h
+ src/message_bus/ingress/EdgeSession.cpp
+ src/message_bus/ingress/EdgeSession.h
+ src/message_bus/ingress/EdgeTcpUplinkPort.cpp
+ src/message_bus/ingress/EdgeTcpUplinkPort.h
+ src/message_bus/ingress/EdgeUplinkWire.cpp
+ src/message_bus/ingress/EdgeUplinkWire.h
+ src/message_bus/ingress/IIngressPort.h
+)
+
+if(SOFTBUS_HAVE_CAN)
+ list(APPEND EDGE_AGENT_SOURCES
+ src/device_bus/manager/CanDeviceManager.cpp
+ src/device_bus/manager/CanDeviceManager.h
+ src/devices/physical/CanDevice.cpp
+ src/devices/physical/CanDevice.h
+ )
+endif()
+
+qt_add_executable(softbus_edge_agent ${EDGE_AGENT_SOURCES})
+
+target_include_directories(softbus_edge_agent
+ PRIVATE
+ ${CMAKE_SOURCE_DIR}/src
+)
+
+target_link_libraries(softbus_edge_agent PRIVATE Qt6::Core Qt6::Network Qt6::SerialPort)
+if(SOFTBUS_HAVE_CAN)
+ target_link_libraries(softbus_edge_agent PRIVATE Qt6::SerialBus)
+endif()
+
+target_compile_definitions(softbus_edge_agent PRIVATE SOFTBUS_HAVE_CAN=${SOFTBUS_HAVE_CAN})
+
+if(UNIX AND NOT APPLE)
+ find_package(PkgConfig QUIET)
+ if(PkgConfig_FOUND)
+ pkg_check_modules(UDEV QUIET libudev)
+ endif()
+ if(UDEV_FOUND)
+ target_include_directories(softbus_edge_agent PRIVATE ${UDEV_INCLUDE_DIRS})
+ target_link_directories(softbus_edge_agent PRIVATE ${UDEV_LIBRARY_DIRS})
+ target_link_libraries(softbus_edge_agent PRIVATE ${UDEV_LIBRARIES})
+ else()
+ target_link_libraries(softbus_edge_agent PRIVATE udev)
+ endif()
+endif()
diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user
new file mode 100644
index 0000000..4b3ba3c
--- /dev/null
+++ b/CMakeLists.txt.user
@@ -0,0 +1,308 @@
+
+
+
+
+
+ EnvironmentId
+ {a7c4c772-b559-4291-a2ea-267a2e71edb0}
+
+
+ ProjectExplorer.Project.ActiveTarget
+ 0
+
+
+ ProjectExplorer.Project.EditorSettings
+
+ true
+ true
+ true
+
+ Cpp
+
+ CppGlobal
+
+
+
+ QmlJS
+
+ QmlJSGlobal
+
+
+ 2
+ UTF-8
+ false
+ 4
+ false
+ 0
+ 80
+ true
+ true
+ 1
+ 0
+ false
+ true
+ false
+ 2
+ true
+ true
+ 0
+ 8
+ true
+ false
+ 1
+ true
+ true
+ true
+ *.md, *.MD, Makefile
+ false
+ true
+ true
+
+
+
+ ProjectExplorer.Project.PluginSettings
+
+
+ true
+ false
+ true
+ true
+ true
+ true
+
+ false
+
+
+ 0
+ true
+
+ true
+ true
+ Builtin.DefaultTidyAndClazy
+ 8
+ true
+
+
+
+ true
+
+
+
+
+ ProjectExplorer.Project.Target.0
+
+ Desktop
+ true
+ Qt 6.9.2
+ Qt 6.9.2
+ {ac766883-1e6c-4832-9072-86a8b976b89e}
+ 0
+ 0
+ 0
+
+ Build
+ 2
+ false
+
+ -DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX}
+-DCMAKE_BUILD_TYPE:STRING=Build
+-DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx}
+-DCMAKE_GENERATOR:STRING=Unix Makefiles
+-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG}
+-DQT_MAINTENANCE_TOOL:FILEPATH=/home/dt/app/qt/MaintenanceTool
+-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable}
+-DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake
+-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C}
+-DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON
+ /home/dt/myCode/bus/soft_bus/edge_agent
+ /home/dt/myCode/bus/soft_bus/edge_agent/build
+
+
+
+
+ all
+
+ false
+
+ true
+ 构建
+ CMakeProjectManager.MakeStep
+
+ 1
+ 构建
+ 构建
+ ProjectExplorer.BuildSteps.Build
+
+
+
+
+
+ clean
+
+ false
+
+ true
+ 构建
+ CMakeProjectManager.MakeStep
+
+ 1
+ 清除
+ 清除
+ ProjectExplorer.BuildSteps.Clean
+
+ 2
+ false
+
+ false
+
+ 构建 (imported)
+ CMakeProjectManager.CMakeBuildConfiguration
+ 0
+ 0
+
+
+ 0
+ 部署
+ 部署
+ ProjectExplorer.BuildSteps.Deploy
+
+ 1
+
+ false
+ ProjectExplorer.DefaultDeployConfiguration
+
+
+
+
+
+
+
+
+ false
+
+ true
+ ApplicationManagerPlugin.Deploy.CMakePackageStep
+
+
+ install-package --acknowledge
+ true
+ Install Application Manager package
+ ApplicationManagerPlugin.Deploy.InstallPackageStep
+
+
+
+
+
+
+
+ 2
+ 部署
+ 部署
+ ProjectExplorer.BuildSteps.Deploy
+
+ 1
+
+ false
+ ApplicationManagerPlugin.Deploy.Configuration
+
+ 2
+
+ true
+ true
+ 0
+ true
+
+ 2
+
+ false
+ -e cpu-cycles --call-graph dwarf,4096 -F 250
+
+ ProjectExplorer.CustomExecutableRunConfiguration
+
+ false
+ true
+ true
+
+ 1
+
+ 1
+
+
+ 0
+ 部署
+ 部署
+ ProjectExplorer.BuildSteps.Deploy
+
+ 1
+
+ false
+ ProjectExplorer.DefaultDeployConfiguration
+
+
+
+
+
+
+
+
+ false
+
+ true
+ ApplicationManagerPlugin.Deploy.CMakePackageStep
+
+
+ install-package --acknowledge
+ true
+ Install Application Manager package
+ ApplicationManagerPlugin.Deploy.InstallPackageStep
+
+
+
+
+
+
+
+ 2
+ 部署
+ 部署
+ ProjectExplorer.BuildSteps.Deploy
+
+ 1
+
+ false
+ ApplicationManagerPlugin.Deploy.Configuration
+
+ 2
+
+ true
+ true
+ 0
+ true
+
+ 2
+
+ false
+ -e cpu-cycles --call-graph dwarf,4096 -F 250
+
+ ProjectExplorer.CustomExecutableRunConfiguration
+
+ false
+ true
+ true
+
+ 1
+
+
+
+ ProjectExplorer.Project.TargetCount
+ 1
+
+
+ ProjectExplorer.Project.Updater.FileVersion
+ 22
+
+
+ Version
+ 22
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cafe21b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+# softbus_edge_agent
+
+独立边缘进程:连接核心 `softbus_daemon` 的 **edgeIngress** TCP 端口,发送 **HELLO**、**DATA_RAWBUS**,以及通过极简 **本地 HTTP**(`GET /`、`GET /ui`、`POST /propose` 等)触发 **TREE_PROPOSE** 反向同步。
+
+## 目录布局(与 `softbus_daemon` 的 `src/` 对齐)
+
+| 路径 | 说明 |
+|------|------|
+| `src/device_bus/tree/` | 与核心同源的 `DeviceTreeModel`(本地子树、`TREE_FULL` 落盘加载) |
+| `src/device_bus/monitor/` | **udev 串口发现**、`EdgeDiscoveryCoordinator`(防抖后组 `TREE_PROPOSE` / `upsert` 向核心注册) |
+| `src/device_bus/manager/` | `SerialDeviceManager`、`CanDeviceManager`(无 Registry / Pipeline,仅 `IIngressPort` + 内存池) |
+| `src/device_bus/EdgeDeviceBus.*` | 边缘总线编排:加载本地树、**reconcile** 串口与 **SocketCAN** 实例 |
+| `src/devices/`、`src/hardware/` | 与核心同源的 `SerialDevice`、`SerialQtDriver` 等 |
+| `src/message_bus/ingress/` | `EdgeSession`、`EdgeUplinkWire`、`EdgeTcpUplinkPort`(`RawBusMessage` → SBUP 上行) |
+
+协议与核心侧文档对齐:
+
+- 核心权威说明:[softbus_daemon/docs/EDGE_UPLINK_PROTOCOL_v1.md](../softbus_daemon/docs/EDGE_UPLINK_PROTOCOL_v1.md)(若仓库路径不同请自行调整相对链接)。
+
+## 构建
+
+```bash
+cmake -S . -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x/gcc_64
+cmake --build build
+./build/bin/softbus_edge_agent config/edge_manifest.example.json
+```
+
+依赖 **Qt6 Core、Network、SerialPort**;若 Qt 安装包含 **SerialBus** 模块,CMake 会自动检测并链接,从而在边缘启用 **SocketCAN reconcile**(设备树 `can:...` 节点)。未安装时仍可完整构建,CAN 节点将被忽略。
+
+## 配置 `edge_manifest.json`(可选)
+
+若默认路径 **`config/edge_manifest.json`** 不存在、或传入的 manifest 路径无法打开、或 JSON 非法,进程会使用**内置默认值**启动(等价于示例 manifest 的字段),并在日志中提示。
+
+仍可通过命令行指定路径覆盖默认文件位置:
+
+```text
+./build/bin/softbus_edge_agent /path/to/edge_manifest.json
+```
+
+见 [config/edge_manifest.schema.json](config/edge_manifest.schema.json) 与示例 [config/edge_manifest.example.json](config/edge_manifest.example.json)。
+
+要点:
+
+- **`coreHost` / `corePort`**:与 `daemon_config.json` 中 `edgeIngress` 一致。
+- **`edgeId`**:HELLO 与会话绑定;`TREE_PROPOSE` 必须相同。
+- **`edgeDiscoveryEnabled`**(bool,默认 `true`):在 TCP 连接且 **HELLO 成功** 后启动 udev 发现协调器,将串口以 `op: upsert` 写入核心设备树(需 Linux + `libudev`)。设为 `false` 可仅依赖手工 `TREE_PROPOSE` 或核心下发树。
+- **`endpoint`**:用于计算 `endpointHash`(与核心一致:`qHash(endpoint)` 即 Qt 默认字符串哈希)。
+- **`fakeDeviceId`**:组装 `routingKey = (endpointHash << 32) | deviceId` 与核心 `makeRoutingKey` 一致。
+- **`fakeIntervalMs`**:设为 `0` 可关闭假数据定时器;**真实串口**由核心下发的设备树节点驱动(`type`/`endpoint` 可识别为 Serial,且 `params.online` 为 true)。节点 `params.deviceId` 会写入上行 `RawBusMessage::deviceId`(未设则 `-1`)。
+
+## 运维 HTTP
+
+若 `opsListenPort > 0`,监听 `127.0.0.1:opsListenPort`:
+
+- `GET /`:返回 `localTreePath` 文件内容(无文件则返回占位 HTML,含指向配置页的链接)。
+- **`GET /ui`**:**阶段 5** 简易配置页——从 **`GET /discovered`** 同源列表选择已发现串口节点,填写 `params.protocol`(如 `modbus_rtu`)、波特率等,提交后由边缘组装 **`op: upsert`** 的 `TREE_PROPOSE` 发往核心(使用当前会话的 **`baseTreeRevision`**,须已 HELLO 且收到过 `TREE_RESULT`)。
+- **`POST /register_discovered`**:可由浏览器表单(`application/x-www-form-urlencoded`)或 **JSON** 调用;字段含 `nodeId`(必填,一般为 `edges//discovered/`)、`devnode`(可选,若不在发现列表中需填 `/dev/tty…`)、`protocol`、`baudRate`、`name`。表单提交成功返回简短 **HTML** 确认页;JSON 请求返回 `{"accepted":true,"sentNodeId":...}`。若尚未拿到有效 `treeRevision`,返回 **503**。
+- **`GET /discovered`**:返回最近 udev 发现事件 JSON(`{ "items": [ ... ] }`),便于无核心时调试;与 `EdgeDiscoveryCoordinator` 内存环缓冲一致(最多约 64 条)。
+- `POST /propose`:body 为 **完整** `CTRL_TREE_PROPOSE` JSON(含 `edgeId`、`baseTreeRevision`、`entries`),将原样经 TCP 发往核心。
+
+示例:
+
+```bash
+curl -s http://127.0.0.1:9080/
+curl -s http://127.0.0.1:9080/ui
+curl -s http://127.0.0.1:9080/discovered
+curl -s -X POST http://127.0.0.1:9080/register_discovered \
+ -H 'Content-Type: application/json' \
+ -d '{"nodeId":"edges/edge-dev-1/discovered/12345","protocol":"modbus_rtu","baudRate":115200}'
+curl -s -X POST http://127.0.0.1:9080/propose \
+ -H 'Content-Type: application/json' \
+ -d '{"edgeId":"edge-dev-1","baseTreeRevision":1,"entries":[{"id":"edges/edge-dev-1/gw","patch":{"name":"gw"}}]}'
+```
+
+(`entries` 中节点 id 必须满足核心 `DeviceTreeModel::nodeBelongsToEdge` 规则。)
+
+## 本地树文件
+
+`TREE_FULL` 落盘格式为 `{ "treeRevision", "edgeId", "tree": [ ... ] }`。**`EdgeDeviceBus`** 会将其中的 `tree` 当作 `device_tree` 加载,并对 **Serial** 与 **CAN**(`endpoint` 形如 `can:vcan0`,`type`/`endpoint` 推断为 CAN)执行 **reconcile**:打开设备后读到的数据经 `EdgeTcpUplinkPort` 编码为 `DATA_RAWBUS` 发往核心。与核心真源同步仍通过 **TREE_PROPOSE**(含边缘 **`upsert`** 自动注册)、核心侧 D-Bus `patch_device_tree_node` 或运维 **`push_edge_tree`**。
+
+### 发现 → 注册流程(简要)
+
+1. 边缘连接核心并 **HELLO**;核心下发 **`TREE_FULL`**,本地落盘并 reconcile 已有节点。
+2. `edgeDiscoveryEnabled` 为真时,`EdgeDiscoveryCoordinator` 扫描/监听 udev 串口 **add/remove**,在拿到有效 **`baseTreeRevision`**(来自 `TREE_RESULT`)后发送 **`TREE_PROPOSE`**:`op: upsert` 注册 `edges//discovered/`,remove 时 **`patch`** `params.online: false`。
+3. 核心 `applyEdgePropose` 成功后再次 **`TREE_FULL`**;边缘 reload 树文件后对 **Serial/CAN** 执行 reconcile,**raw 仅上行**。
diff --git a/config/edge_manifest.example.json b/config/edge_manifest.example.json
new file mode 100644
index 0000000..3f88e58
--- /dev/null
+++ b/config/edge_manifest.example.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "./edge_manifest.schema.json",
+ "coreHost": "127.0.0.1",
+ "corePort": 18765,
+ "edgeId": "edge-dev-1",
+ "authToken": "",
+ "opsListenPort": 9080,
+ "localTreePath": "/tmp/edge_local_tree.json",
+ "fakeIntervalMs": 5000,
+ "fakeDeviceId": 1,
+ "endpoint": "edge:fake",
+ "protocol": 0,
+ "edgeDiscoveryEnabled": true
+}
diff --git a/config/edge_manifest.schema.json b/config/edge_manifest.schema.json
new file mode 100644
index 0000000..97cd155
--- /dev/null
+++ b/config/edge_manifest.schema.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "title": "softbus_edge_manifest",
+ "type": "object",
+ "required": ["coreHost", "corePort", "edgeId"],
+ "properties": {
+ "coreHost": { "type": "string", "description": "softbus_daemon 所在主机" },
+ "corePort": { "type": "integer", "description": "daemon_config edgeIngress.port" },
+ "edgeId": { "type": "string", "description": "HELLO / PROPOSE 与会话绑定的边缘 ID" },
+ "authToken": { "type": "string", "description": "若核心配置了 edgeIngress.authToken,则此处必须一致" },
+ "opsListenPort": { "type": "integer", "minimum": 0, "description": "0 关闭本地运维 HTTP;否则监听 127.0.0.1:port" },
+ "localTreePath": { "type": "string", "description": "GET / 返回的本地树文件路径(展示用)" },
+ "fakeIntervalMs": { "type": "integer", "minimum": 100, "description": "假数据 DATA_RAWBUS 发送周期(毫秒)" },
+ "fakeDeviceId": { "type": "integer", "description": "RawBusMessage.deviceId" },
+ "endpoint": { "type": "string", "description": "用于 qHash(endpoint) 得到 endpointHash,须与核心设备树约定一致" },
+ "protocol": { "type": "integer", "description": "ProtocolType 枚举数值(与核心 RawBusMessage 一致)" },
+ "edgeDiscoveryEnabled": {
+ "type": "boolean",
+ "description": "HELLO 成功后是否启动 udev 串口发现并向核心发送 TREE_PROPOSE(upsert/patch);需 Linux libudev"
+ }
+ }
+}
diff --git a/docs/UPLINK_PROTOCOL_v1.md b/docs/UPLINK_PROTOCOL_v1.md
new file mode 100644
index 0000000..185883d
--- /dev/null
+++ b/docs/UPLINK_PROTOCOL_v1.md
@@ -0,0 +1,7 @@
+# 边缘上行协议(与核心同步)
+
+完整规范见 **softbus_daemon** 仓库:`docs/EDGE_UPLINK_PROTOCOL_v1.md`。
+
+本仓库在 `src/message_bus/ingress/EdgeUplinkWire.*` 中实现 **编码侧**(外层 `SBUP` 帧 + `DATA_RAWBUS` 内层二进制),须与核心 **逐字节一致**。
+
+**下行**:`EdgeSession` 会解析核心发来的 `TREE_RESULT` 与 **`TREE_FULL`**(`frame_kind=4`),后者由 `main` 写入 manifest 中的 `localTreePath` 后,**`EdgeDeviceBus`** 会加载并 **reconcile** 串口(与核心 `devices` + `hardware` + `device_bus` 行为对齐的裁剪版)。
diff --git a/src/core/memory/MemoryPool.h b/src/core/memory/MemoryPool.h
new file mode 100644
index 0000000..c651ef3
--- /dev/null
+++ b/src/core/memory/MemoryPool.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace softbus::core::memory
+{
+
+class MemoryPool
+{
+public:
+ struct PoolBlock
+ {
+ std::uint8_t* data{nullptr};
+ std::size_t capacity{0};
+ std::size_t size{0};
+ std::size_t slotIndex{0};
+ };
+
+ using BlockPtr = std::shared_ptr;
+
+ explicit MemoryPool(std::size_t blockSize = 4096, std::size_t blockCount = 1024)
+ : m_blockSize(blockSize), m_storage(blockSize * blockCount), m_inUse(blockCount, false)
+ {
+ for (std::size_t i = 0; i < blockCount; ++i) {
+ m_freeSlots.push(i);
+ }
+ }
+
+ BlockPtr allocate(std::size_t requiredSize)
+ {
+ if (requiredSize > m_blockSize) {
+ return {};
+ }
+
+ std::size_t slot = 0;
+ {
+ std::lock_guard lk(m_mutex);
+ if (m_freeSlots.empty()) {
+ return {};
+ }
+ slot = m_freeSlots.front();
+ m_freeSlots.pop();
+ m_inUse[slot] = true;
+ }
+
+ auto* raw = new PoolBlock{
+ m_storage.data() + slot * m_blockSize,
+ m_blockSize,
+ requiredSize,
+ slot,
+ };
+ return BlockPtr(raw, [this](PoolBlock* b) {
+ if (!b) return;
+ this->release(*b);
+ delete b;
+ });
+ }
+
+ void release(const PoolBlock& block)
+ {
+ std::lock_guard lk(m_mutex);
+ if (block.slotIndex >= m_inUse.size()) {
+ return;
+ }
+ if (!m_inUse[block.slotIndex]) {
+ return;
+ }
+ m_inUse[block.slotIndex] = false;
+ m_freeSlots.push(block.slotIndex);
+ }
+
+ std::size_t blockSize() const { return m_blockSize; }
+ std::size_t totalBlocks() const { return m_inUse.size(); }
+
+ std::size_t inUseBlocks() const
+ {
+ std::lock_guard lk(m_mutex);
+ std::size_t used = 0;
+ for (const bool v : m_inUse) {
+ if (v) ++used;
+ }
+ return used;
+ }
+
+private:
+ std::size_t m_blockSize;
+ std::vector m_storage;
+ mutable std::mutex m_mutex;
+ std::queue m_freeSlots;
+ std::vector m_inUse;
+};
+
+} // namespace softbus::core::memory
diff --git a/src/core/models/MessageRoutingUtils.h b/src/core/models/MessageRoutingUtils.h
new file mode 100644
index 0000000..c48c5c1
--- /dev/null
+++ b/src/core/models/MessageRoutingUtils.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include
+
+#include
+
+#include "core/models/RawBusMessage.h"
+
+namespace softbus::core::models
+{
+
+// Hash physical endpoint string into a compact routing key.
+inline std::uint32_t makeEndpointHash(const QString& endpoint)
+{
+ return qHash(endpoint);
+}
+
+// Build a deterministic string key from endpoint hash (for session maps).
+inline QString makeEndpointKey(std::uint32_t endpointHash)
+{
+ return QString::number(endpointHash, 16);
+}
+
+// Build a 64-bit routing key for fast lookup paths.
+inline std::uint64_t makeRoutingKey(std::uint32_t endpointHash, int deviceId)
+{
+ return (static_cast(endpointHash) << 32) | static_cast(deviceId);
+}
+
+// Bridge routing key to legacy string-based matching interfaces.
+inline QString routingKeyToStableKey(std::uint64_t routingKey)
+{
+ return routingKey == 0 ? QString() : QString::number(routingKey);
+}
+
+inline ProtocolType protocolTypeFromHint(const QString& hint)
+{
+ if (hint.compare(QStringLiteral("modbus_rtu"), Qt::CaseInsensitive) == 0) {
+ return ProtocolType::MODBUS_RTU;
+ }
+ if (hint.compare(QStringLiteral("modbus_ascii"), Qt::CaseInsensitive) == 0) {
+ return ProtocolType::MODBUS_ASCII;
+ }
+ if (hint.compare(QStringLiteral("modbus_tcp"), Qt::CaseInsensitive) == 0) {
+ return ProtocolType::MODBUS_TCP;
+ }
+ if (hint.compare(QStringLiteral("can_raw"), Qt::CaseInsensitive) == 0) {
+ return ProtocolType::CAN_RAW;
+ }
+ if (hint.compare(QStringLiteral("can_open"), Qt::CaseInsensitive) == 0) {
+ return ProtocolType::CAN_OPEN;
+ }
+ if (hint.compare(QStringLiteral("udp_custom"), Qt::CaseInsensitive) == 0) {
+ return ProtocolType::UDP_CUSTOM;
+ }
+ return ProtocolType::UNKNOWN;
+}
+
+} // namespace softbus::core::models
diff --git a/src/core/models/PayloadRef.h b/src/core/models/PayloadRef.h
new file mode 100644
index 0000000..0bee0ad
--- /dev/null
+++ b/src/core/models/PayloadRef.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include "core/memory/MemoryPool.h"
+
+namespace softbus::core::models
+{
+
+struct PayloadRef
+{
+ softbus::core::memory::MemoryPool::BlockPtr block;
+ std::size_t offset{0};
+ std::size_t length{0};
+
+ const std::uint8_t* bytes() const
+ {
+ if (!block || !block->data || offset > block->capacity) {
+ return nullptr;
+ }
+ return block->data + offset;
+ }
+
+ bool valid() const
+ {
+ return block && block->data && (offset + length <= block->capacity);
+ }
+};
+
+} // namespace softbus::core::models
+
+Q_DECLARE_METATYPE(softbus::core::models::PayloadRef)
diff --git a/src/core/models/RawBusMessage.h b/src/core/models/RawBusMessage.h
new file mode 100644
index 0000000..1e7e5af
--- /dev/null
+++ b/src/core/models/RawBusMessage.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+#include "core/models/PayloadRef.h"
+
+namespace softbus::core::models
+{
+
+enum class BusDirection
+{
+ Unknown,
+ Upstream,
+ Downstream,
+};
+
+enum class ProtocolType : std::uint16_t
+{
+ UNKNOWN = 0,
+ MODBUS_RTU,
+ MODBUS_ASCII,
+ MODBUS_TCP,
+ CAN_RAW,
+ CAN_OPEN,
+ UDP_CUSTOM,
+};
+
+inline uint qHash(ProtocolType key, uint seed = 0) noexcept
+{
+ return ::qHash(static_cast(key), seed);
+}
+
+union ProtocolHeader
+{
+ struct Can
+ {
+ std::uint32_t cobId{0};
+ bool isExtended{false};
+ bool isRtr{false};
+ std::uint8_t padding[2]{0, 0};
+ } can;
+
+ struct Modbus
+ {
+ std::uint8_t slaveId{0};
+ std::uint8_t functionCode{0};
+ std::uint8_t padding[6]{0, 0, 0, 0, 0, 0};
+ } modbus;
+
+ struct Net
+ {
+ std::uint32_t srcIp{0};
+ std::uint16_t srcPort{0};
+ std::uint16_t padding{0};
+ } net;
+
+ std::uint64_t alignmentPadding{0};
+};
+
+struct RawBusMessage
+{
+ int deviceId{-1}; // Device instance id, -1 means unbound.
+ std::uint32_t endpointHash{0}; // Physical endpoint/channel hash.
+ ProtocolType protocol{ProtocolType::UNKNOWN};
+ std::uint32_t logicalAddress{0}; // Slave ID / node ID for protocol routing.
+ std::uint64_t routingKey{0}; // Stable key for MetadataRegistry lookup.
+ ProtocolHeader header{}; // Out-of-band protocol header metadata.
+ std::int64_t timestampMs{0}; // UTC unix timestamp in milliseconds.
+ QString traceId;
+
+ softbus::core::models::PayloadRef payload;
+ BusDirection direction{BusDirection::Unknown};
+};
+
+} // namespace softbus::core::models
+
+Q_DECLARE_METATYPE(softbus::core::models::RawBusMessage)
diff --git a/src/device_bus/EdgeDeviceBus.cpp b/src/device_bus/EdgeDeviceBus.cpp
new file mode 100644
index 0000000..6aba344
--- /dev/null
+++ b/src/device_bus/EdgeDeviceBus.cpp
@@ -0,0 +1,222 @@
+#include "device_bus/EdgeDeviceBus.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "device_bus/manager/SerialDeviceManager.h"
+#if SOFTBUS_HAVE_CAN
+#include "device_bus/manager/CanDeviceManager.h"
+#endif
+#include "devices/DeviceTypes.h"
+#include "utils/logs/logging.h"
+
+namespace softbus::device_bus
+{
+
+EdgeDeviceBus::EdgeDeviceBus()
+ : m_tree(std::make_shared())
+ , m_pool(std::make_shared())
+{
+}
+
+EdgeDeviceBus::~EdgeDeviceBus()
+{
+ shutdownAllSerial();
+#if SOFTBUS_HAVE_CAN
+ shutdownAllCan();
+#endif
+}
+
+void EdgeDeviceBus::setIngressPort(softbus::message_bus::ingress::IIngressPort* port)
+{
+ m_ingress = port;
+}
+
+bool EdgeDeviceBus::loadLocalEdgeTreeFile(const QString& path, QString* errorMessage)
+{
+ QFile f(path);
+ if (!f.open(QIODevice::ReadOnly)) {
+ if (errorMessage) {
+ *errorMessage = QStringLiteral("cannot open file");
+ }
+ return false;
+ }
+ QJsonParseError pe;
+ const QJsonDocument doc = QJsonDocument::fromJson(f.readAll(), &pe);
+ if (pe.error != QJsonParseError::NoError || !doc.isObject()) {
+ if (errorMessage) {
+ *errorMessage = pe.errorString();
+ }
+ return false;
+ }
+ const QJsonObject rootObj = doc.object();
+ const QJsonArray treeArr = rootObj.value(QStringLiteral("tree")).toArray();
+ if (treeArr.isEmpty()) {
+ if (errorMessage) {
+ *errorMessage = QStringLiteral("missing or empty 'tree' array");
+ }
+ return false;
+ }
+ QJsonObject wrapped;
+ wrapped.insert(QStringLiteral("device_tree"), treeArr);
+ return m_tree->loadFromDaemonConfig(wrapped, errorMessage);
+}
+
+void EdgeDeviceBus::shutdownAllSerial()
+{
+ const auto keys = m_serialByEndpoint.keys();
+ for (const QString& ep : keys) {
+ auto dev = m_serialByEndpoint.value(ep);
+ if (dev) {
+ dev->stop();
+ }
+ }
+ m_serialByEndpoint.clear();
+}
+
+#if SOFTBUS_HAVE_CAN
+void EdgeDeviceBus::shutdownAllCan()
+{
+ const auto keys = m_canByEndpoint.keys();
+ for (const QString& ep : keys) {
+ auto dev = m_canByEndpoint.value(ep);
+ if (dev) {
+ dev->stop();
+ }
+ }
+ m_canByEndpoint.clear();
+}
+#endif
+
+void EdgeDeviceBus::reconcileSerialFromTree()
+{
+ if (!m_ingress || !m_pool) {
+ return;
+ }
+
+ QSet desired;
+ const auto nodes = m_tree->nodes();
+ for (const auto& n : nodes) {
+ if (!n.enabled) {
+ continue;
+ }
+ if (!n.params.value(QStringLiteral("online")).toBool(true)) {
+ continue;
+ }
+ auto kind = softbus::devices::kindFromType(n.type);
+ if (kind == softbus::devices::DeviceKind::Unknown) {
+ kind = softbus::devices::inferDeviceKindFromEndpoint(n.endpoint);
+ }
+ if (kind != softbus::devices::DeviceKind::Serial) {
+ continue;
+ }
+ if (n.endpoint.isEmpty()) {
+ continue;
+ }
+ desired.insert(n.endpoint);
+ }
+
+ const auto current = m_serialByEndpoint.keys();
+ for (const QString& ep : current) {
+ if (!desired.contains(ep)) {
+ auto dev = m_serialByEndpoint.take(ep);
+ if (dev) {
+ dev->stop();
+ }
+ }
+ }
+
+ softbus::device_bus::manager::SerialInitDeps deps;
+ deps.treeModel = m_tree.get();
+ deps.dirtyCb = []() {};
+ deps.ingressPort = m_ingress;
+ deps.memoryPool = m_pool;
+ deps.serialDevicesByEndpoint = &m_serialByEndpoint;
+
+ for (const QString& ep : desired) {
+ if (m_serialByEndpoint.contains(ep)) {
+ continue;
+ }
+ const int idx = m_tree->findIndexByEndpoint(ep);
+ if (idx < 0) {
+ continue;
+ }
+ const auto& n = m_tree->nodes().at(idx);
+ if (!initializeSerialDeviceWithDeps(ep, n.params, deps)) {
+ LOG_WARNING() << "EdgeDeviceBus: failed to init serial" << ep;
+ }
+ }
+}
+
+#if SOFTBUS_HAVE_CAN
+void EdgeDeviceBus::reconcileCanFromTree()
+{
+ if (!m_ingress || !m_pool) {
+ return;
+ }
+
+ QSet desired;
+ const auto nodes = m_tree->nodes();
+ for (const auto& n : nodes) {
+ if (!n.enabled) {
+ continue;
+ }
+ if (!n.params.value(QStringLiteral("online")).toBool(true)) {
+ continue;
+ }
+ auto kind = softbus::devices::kindFromType(n.type);
+ if (kind == softbus::devices::DeviceKind::Unknown) {
+ kind = softbus::devices::inferDeviceKindFromEndpoint(n.endpoint);
+ }
+ if (kind != softbus::devices::DeviceKind::Can) {
+ continue;
+ }
+ if (n.endpoint.isEmpty()) {
+ continue;
+ }
+ desired.insert(n.endpoint);
+ }
+
+ const auto current = m_canByEndpoint.keys();
+ for (const QString& ep : current) {
+ if (!desired.contains(ep)) {
+ auto dev = m_canByEndpoint.take(ep);
+ if (dev) {
+ dev->stop();
+ }
+ }
+ }
+
+ softbus::device_bus::manager::CanInitDeps deps;
+ deps.treeModel = m_tree.get();
+ deps.dirtyCb = []() {};
+ deps.ingressPort = m_ingress;
+ deps.memoryPool = m_pool;
+ deps.canDevicesByEndpoint = &m_canByEndpoint;
+
+ for (const QString& ep : desired) {
+ if (m_canByEndpoint.contains(ep)) {
+ continue;
+ }
+ const int idx = m_tree->findIndexByEndpoint(ep);
+ if (idx < 0) {
+ continue;
+ }
+ const auto& n = m_tree->nodes().at(idx);
+ if (!initializeCanDeviceWithDeps(ep, n.params, deps)) {
+ LOG_WARNING() << "EdgeDeviceBus: failed to init CAN" << ep;
+ }
+ }
+}
+#else
+void EdgeDeviceBus::reconcileCanFromTree()
+{
+ // Qt SerialBus 未随构建启用(见 CMake `SOFTBUS_HAVE_CAN`)
+}
+#endif
+
+} // namespace softbus::device_bus
diff --git a/src/device_bus/EdgeDeviceBus.h b/src/device_bus/EdgeDeviceBus.h
new file mode 100644
index 0000000..2f00e91
--- /dev/null
+++ b/src/device_bus/EdgeDeviceBus.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include
+
+#include
+#include
+#include
+
+#include "core/memory/MemoryPool.h"
+#include "device_bus/tree/DeviceTreeModel.h"
+#include "devices/physical/SerialDevice.h"
+#if SOFTBUS_HAVE_CAN
+#include "devices/physical/CanDevice.h"
+#endif
+#include "message_bus/ingress/IIngressPort.h"
+
+namespace softbus::device_bus
+{
+
+/**
+ * 边缘侧设备总线编排(目录对齐 daemon 的 device_bus + devices + hardware)。
+ * 从本地 `TREE_FULL` 落盘文件加载 `device_tree`,按节点拉起串口并绑定 `IIngressPort`(通常为核心 TCP 上行)。
+ */
+class EdgeDeviceBus final
+{
+public:
+ EdgeDeviceBus();
+ ~EdgeDeviceBus();
+
+ void setIngressPort(softbus::message_bus::ingress::IIngressPort* port);
+
+ /** 读取边缘落盘:`{ "tree": [...], "treeRevision", "edgeId" }`,包装为 `loadFromDaemonConfig`。 */
+ bool loadLocalEdgeTreeFile(const QString& path, QString* errorMessage = nullptr);
+
+ /** 按当前设备树启停串口实例(与核心 reconcile 类似,无 Pipeline/Registry)。 */
+ void reconcileSerialFromTree();
+
+ /** 按当前设备树启停 SocketCAN 实例(endpoint 形如 `can:vcan0`)。 */
+ void reconcileCanFromTree();
+
+ DeviceTreeModel& treeModel() { return *m_tree; }
+ const DeviceTreeModel& treeModel() const { return *m_tree; }
+
+ std::shared_ptr memoryPool() const { return m_pool; }
+
+private:
+ void shutdownAllSerial();
+#if SOFTBUS_HAVE_CAN
+ void shutdownAllCan();
+#endif
+
+ std::shared_ptr m_tree;
+ std::shared_ptr m_pool;
+ softbus::message_bus::ingress::IIngressPort* m_ingress{nullptr};
+ QHash> m_serialByEndpoint;
+#if SOFTBUS_HAVE_CAN
+ QHash> m_canByEndpoint;
+#endif
+};
+
+} // namespace softbus::device_bus
diff --git a/src/device_bus/manager/CanDeviceManager.cpp b/src/device_bus/manager/CanDeviceManager.cpp
new file mode 100644
index 0000000..a641a8a
--- /dev/null
+++ b/src/device_bus/manager/CanDeviceManager.cpp
@@ -0,0 +1,105 @@
+#include "device_bus/manager/CanDeviceManager.h"
+
+#include
+
+#include
+#include
+
+#include "device_bus/tree/DeviceTreeModel.h"
+#include "devices/DeviceTypes.h"
+#include "utils/logs/logging.h"
+
+namespace softbus::device_bus::manager
+{
+
+namespace
+{
+
+static bool setIfChanged(QJsonObject& params, const QString& key, const QJsonValue& value)
+{
+ const auto it = params.find(key);
+ if (it == params.end() || it.value() != value) {
+ params.insert(key, value);
+ return true;
+ }
+ return false;
+}
+
+static bool persistCanBitrate(DeviceTreeModel* treeModel,
+ const QString& endpoint,
+ int bitrate,
+ const std::function& dirtyCb)
+{
+ if (!treeModel || endpoint.isEmpty() || bitrate <= 0) {
+ return false;
+ }
+ const int idx = treeModel->findIndexByEndpoint(endpoint);
+ if (idx < 0) {
+ return false;
+ }
+ auto& nodes = treeModel->nodesMutable();
+ if (idx >= static_cast(nodes.size())) {
+ return false;
+ }
+ auto& p = nodes[idx].params;
+ bool changed = setIfChanged(p, QStringLiteral("bitrate"), bitrate);
+ if (changed && dirtyCb) {
+ dirtyCb();
+ }
+ return changed;
+}
+
+} // namespace
+
+bool initializeCanDeviceWithDeps(const QString& endpoint, const QJsonObject& params, const CanInitDeps& deps)
+{
+ if (!deps.canDevicesByEndpoint || !deps.ingressPort || !deps.memoryPool) {
+ return false;
+ }
+ if (deps.canDevicesByEndpoint->contains(endpoint)) {
+ LOG_WARNING() << "Edge CanDeviceManager: device already exists for" << endpoint;
+ return false;
+ }
+
+ int bitrate = params.value(QStringLiteral("bitrate")).toInt(500'000);
+ if (bitrate <= 0) {
+ bitrate = 500'000;
+ }
+ persistCanBitrate(deps.treeModel, endpoint, bitrate, deps.dirtyCb);
+
+ QString iface = endpoint.trimmed();
+ if (iface.startsWith(QStringLiteral("can:"), Qt::CaseInsensitive)) {
+ iface = iface.mid(4);
+ }
+ QString err;
+ std::unique_ptr bus(QCanBus::instance()->createDevice(QStringLiteral("socketcan"), iface, &err));
+ if (!bus) {
+ LOG_ERROR() << "Edge CanDeviceManager: createDevice failed" << endpoint << err;
+ return false;
+ }
+
+ softbus::devices::DeviceInfo info;
+ info.id = params.value(QStringLiteral("deviceId")).toInt(-1);
+ info.endpoint = endpoint;
+ info.type = QStringLiteral("can");
+ info.kind = softbus::devices::DeviceKind::Can;
+ info.protocol = params.value(QStringLiteral("protocol")).toString(QStringLiteral("can_raw"));
+
+ softbus::devices::physical::CanDevice::Config cfg;
+ cfg.endpoint = endpoint;
+ cfg.bitrate = bitrate;
+
+ QSharedPointer device(
+ new softbus::devices::physical::CanDevice(std::move(bus), cfg, info));
+ device->bindIngress(deps.ingressPort, deps.memoryPool);
+
+ if (!device->start()) {
+ LOG_ERROR() << "Edge CanDeviceManager: connectDevice failed" << endpoint;
+ return false;
+ }
+
+ deps.canDevicesByEndpoint->insert(endpoint, device);
+ return true;
+}
+
+} // namespace softbus::device_bus::manager
diff --git a/src/device_bus/manager/CanDeviceManager.h b/src/device_bus/manager/CanDeviceManager.h
new file mode 100644
index 0000000..e8003fa
--- /dev/null
+++ b/src/device_bus/manager/CanDeviceManager.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "core/memory/MemoryPool.h"
+#include "devices/physical/CanDevice.h"
+#include "message_bus/ingress/IIngressPort.h"
+
+class DeviceTreeModel;
+
+namespace softbus::device_bus::manager
+{
+
+struct CanInitDeps
+{
+ DeviceTreeModel* treeModel{nullptr};
+ std::function dirtyCb;
+ softbus::message_bus::ingress::IIngressPort* ingressPort{nullptr};
+ std::shared_ptr memoryPool;
+ QHash>* canDevicesByEndpoint{nullptr};
+};
+
+bool initializeCanDeviceWithDeps(const QString& endpoint, const QJsonObject& params, const CanInitDeps& deps);
+
+} // namespace softbus::device_bus::manager
diff --git a/src/device_bus/manager/SerialDeviceManager.cpp b/src/device_bus/manager/SerialDeviceManager.cpp
new file mode 100644
index 0000000..b59a3b1
--- /dev/null
+++ b/src/device_bus/manager/SerialDeviceManager.cpp
@@ -0,0 +1,152 @@
+#include "device_bus/manager/SerialDeviceManager.h"
+
+#include
+#include
+
+#include "device_bus/tree/DeviceTreeModel.h"
+#include "devices/DeviceTypes.h"
+#include "hardware/drivers/serial/SerialCapabilities.h"
+#include "hardware/drivers/serial/SerialQtDriver.h"
+#include "utils/logs/logging.h"
+
+using softbus::hardware::drivers::serial::SerialQtDriver;
+namespace serial_cap = softbus::hardware::drivers::serial::capabilities;
+
+namespace softbus::device_bus::manager
+{
+
+static bool setIfChanged(QJsonObject& params, const QString& key, const QJsonValue& value)
+{
+ const auto it = params.find(key);
+ if (it == params.end() || it.value() != value) {
+ params.insert(key, value);
+ return true;
+ }
+ return false;
+}
+
+static bool persistSerialParams(DeviceTreeModel* treeModel,
+ const QString& endpoint,
+ const QJsonObject& mergedParams,
+ const std::function& dirtyCb)
+{
+ if (!treeModel || endpoint.isEmpty() || mergedParams.isEmpty()) {
+ return false;
+ }
+
+ const int idx = treeModel->findIndexByEndpoint(endpoint);
+ if (idx < 0) {
+ return false;
+ }
+
+ auto& nodes = treeModel->nodesMutable();
+ if (idx >= nodes.size()) {
+ return false;
+ }
+
+ auto& node = nodes[idx];
+ auto& p = node.params;
+
+ bool changed = false;
+ changed = setIfChanged(p, QStringLiteral("baudRate"), mergedParams.value(QStringLiteral("baudRate"))) || changed;
+ changed = setIfChanged(p, QStringLiteral("dataBits"), mergedParams.value(QStringLiteral("dataBits"))) || changed;
+ changed = setIfChanged(p, QStringLiteral("parity"), mergedParams.value(QStringLiteral("parity"))) || changed;
+ changed = setIfChanged(p, QStringLiteral("stopBits"), mergedParams.value(QStringLiteral("stopBits"))) || changed;
+ changed =
+ setIfChanged(p, QStringLiteral("flowControl"), mergedParams.value(QStringLiteral("flowControl"))) || changed;
+
+ if (changed && dirtyCb) {
+ dirtyCb();
+ }
+ return changed;
+}
+
+bool initializeSerialDeviceWithDeps(const QString& endpoint, const QJsonObject& params, const SerialInitDeps& deps)
+{
+ if (!deps.serialDevicesByEndpoint || !deps.ingressPort || !deps.memoryPool) {
+ return false;
+ }
+
+ if (deps.serialDevicesByEndpoint->contains(endpoint)) {
+ LOG_WARNING() << "Edge SerialDeviceManager: serial device already exists for" << endpoint;
+ return false;
+ }
+
+ serial_cap::SerialDefaults def = serial_cap::kDefaults;
+
+ int baud = params.value(QStringLiteral("baudRate")).toInt(def.baudRate);
+ if (!serial_cap::isValidBaud(baud)) baud = def.baudRate;
+
+ int dataBits = params.value(QStringLiteral("dataBits")).toInt(def.dataBits);
+ if (!serial_cap::isValidDataBits(dataBits)) dataBits = def.dataBits;
+
+ QString parityStr = params.value(QStringLiteral("parity")).toString(QLatin1String(def.parity));
+ if (!serial_cap::isValidParity(parityStr)) parityStr = QLatin1String(def.parity);
+
+ int stopBitsVal = params.value(QStringLiteral("stopBits")).toInt(def.stopBits);
+ if (!serial_cap::isValidStopBits(stopBitsVal)) stopBitsVal = def.stopBits;
+
+ QString flowStr = params.value(QStringLiteral("flowControl")).toString(QLatin1String(def.flowControl));
+ if (!serial_cap::isValidFlowControl(flowStr)) flowStr = QLatin1String(def.flowControl);
+
+ QJsonObject merged;
+ merged.insert(QStringLiteral("baudRate"), baud);
+ merged.insert(QStringLiteral("dataBits"), dataBits);
+ merged.insert(QStringLiteral("parity"), parityStr);
+ merged.insert(QStringLiteral("stopBits"), stopBitsVal);
+ merged.insert(QStringLiteral("flowControl"), flowStr);
+
+ persistSerialParams(deps.treeModel, endpoint, merged, deps.dirtyCb);
+
+ auto driver = std::make_unique(nullptr);
+ driver->SetBaudRate(static_cast(baud));
+ driver->SetDataBits(static_cast(dataBits));
+
+ QSerialPort::Parity parity = QSerialPort::NoParity;
+ if (parityStr == QStringLiteral("even")) {
+ parity = QSerialPort::EvenParity;
+ } else if (parityStr == QStringLiteral("odd")) {
+ parity = QSerialPort::OddParity;
+ }
+ driver->SetParity(parity);
+
+ QSerialPort::StopBits stopBits = (stopBitsVal == 2) ? QSerialPort::TwoStop : QSerialPort::OneStop;
+ driver->SetStopBits(stopBits);
+
+ QSerialPort::FlowControl flow = QSerialPort::NoFlowControl;
+ if (flowStr == QStringLiteral("hardware")) {
+ flow = QSerialPort::HardwareControl;
+ } else if (flowStr == QStringLiteral("software")) {
+ flow = QSerialPort::SoftwareControl;
+ }
+ driver->SetFlowControl(flow);
+
+ softbus::devices::DeviceInfo info;
+ info.id = params.value(QStringLiteral("deviceId")).toInt(-1);
+ info.endpoint = endpoint;
+ info.type = QStringLiteral("serial");
+ info.kind = softbus::devices::DeviceKind::Serial;
+ info.protocol = params.value(QStringLiteral("protocol")).toString(QStringLiteral("modbus_rtu"));
+
+ softbus::devices::physical::SerialDevice::Config cfg;
+ cfg.endpoint = endpoint;
+ cfg.baudRate = baud;
+ cfg.dataBits = dataBits;
+ cfg.parity = parityStr;
+ cfg.stopBits = stopBitsVal;
+ cfg.flowControl = flowStr;
+
+ QSharedPointer device(
+ new softbus::devices::physical::SerialDevice(std::move(driver), cfg, info));
+ device->bindIngress(deps.ingressPort, deps.memoryPool);
+
+ if (!device->start()) {
+ LOG_ERROR() << "Edge SerialDeviceManager: failed to start serial device for" << endpoint;
+ return false;
+ }
+
+ deps.serialDevicesByEndpoint->insert(endpoint, device);
+ return true;
+}
+
+} // namespace softbus::device_bus::manager
diff --git a/src/device_bus/manager/SerialDeviceManager.h b/src/device_bus/manager/SerialDeviceManager.h
new file mode 100644
index 0000000..30094f6
--- /dev/null
+++ b/src/device_bus/manager/SerialDeviceManager.h
@@ -0,0 +1,32 @@
+#pragma once
+// 与 softbus_daemon 同源布局;边缘侧去掉 Registry / Egress / Pipeline,仅保留串口 + IIngressPort。
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "core/memory/MemoryPool.h"
+#include "devices/physical/SerialDevice.h"
+#include "message_bus/ingress/IIngressPort.h"
+
+class DeviceTreeModel;
+
+namespace softbus::device_bus::manager
+{
+
+struct SerialInitDeps
+{
+ DeviceTreeModel* treeModel{nullptr};
+ std::function dirtyCb;
+ softbus::message_bus::ingress::IIngressPort* ingressPort{nullptr};
+ std::shared_ptr memoryPool;
+ QHash>* serialDevicesByEndpoint{nullptr};
+};
+
+bool initializeSerialDeviceWithDeps(const QString& endpoint, const QJsonObject& params, const SerialInitDeps& deps);
+
+} // namespace softbus::device_bus::manager
diff --git a/src/device_bus/monitor/DeviceEvent.h b/src/device_bus/monitor/DeviceEvent.h
new file mode 100644
index 0000000..b2819eb
--- /dev/null
+++ b/src/device_bus/monitor/DeviceEvent.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace softbus::device_bus::monitor
+{
+
+enum class DeviceEventType
+{
+ Serial,
+ Usb,
+ Storage,
+ NetInterface,
+ Unknown,
+};
+
+enum class DeviceEventAction
+{
+ Add,
+ Remove,
+ Change,
+ Unknown,
+};
+
+struct DeviceEvent
+{
+ DeviceEventType type{DeviceEventType::Unknown};
+ DeviceEventAction action{DeviceEventAction::Unknown};
+ QDateTime timestamp{QDateTime::currentDateTimeUtc()};
+
+ QString subsystem;
+ QString devnode;
+ QString devicePath;
+
+ QString vid;
+ QString pid;
+ QString serial;
+ QString model;
+ QString vendor;
+
+ QString ifname;
+ int ifindex{-1};
+ bool linkUp{false};
+
+ QMap props;
+};
+
+} // namespace softbus::device_bus::monitor
diff --git a/src/device_bus/monitor/EdgeDiscoveryCoordinator.cpp b/src/device_bus/monitor/EdgeDiscoveryCoordinator.cpp
new file mode 100644
index 0000000..5c75bed
--- /dev/null
+++ b/src/device_bus/monitor/EdgeDiscoveryCoordinator.cpp
@@ -0,0 +1,286 @@
+#include "device_bus/monitor/EdgeDiscoveryCoordinator.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "device_bus/monitor/UdevDeviceMonitor.h"
+#include "message_bus/ingress/EdgeSession.h"
+#include "utils/logs/logging.h"
+
+namespace softbus_edge
+{
+
+EdgeDiscoveryCoordinator::EdgeDiscoveryCoordinator(QObject* parent)
+ : QObject(parent)
+{
+}
+
+EdgeDiscoveryCoordinator::~EdgeDiscoveryCoordinator()
+{
+ stop();
+}
+
+void EdgeDiscoveryCoordinator::setEdgeSession(softbus::message_bus::ingress::EdgeSession* session)
+{
+ m_session = session;
+}
+
+void EdgeDiscoveryCoordinator::setEdgeId(const QString& edgeId)
+{
+ m_edgeId = edgeId;
+}
+
+void EdgeDiscoveryCoordinator::setCoreTreeRevisionGetter(std::function fn)
+{
+ m_getCoreRevision = std::move(fn);
+}
+
+QString EdgeDiscoveryCoordinator::stableKeyForSerial(const softbus::device_bus::monitor::DeviceEvent& ev) const
+{
+ const QString syspath = ev.props.value(QStringLiteral("SYSPATH"));
+ const QString basis = !syspath.isEmpty() ? syspath : ev.devnode;
+ return QString::number(qHash(basis));
+}
+
+QString EdgeDiscoveryCoordinator::nodeIdForStableKey(const QString& stableKey) const
+{
+ return QStringLiteral("edges/%1/discovered/%2").arg(m_edgeId, stableKey);
+}
+
+void EdgeDiscoveryCoordinator::enqueueUpsert(const softbus::device_bus::monitor::DeviceEvent& ev)
+{
+ if (m_edgeId.isEmpty() || ev.devnode.isEmpty()) {
+ return;
+ }
+ const QString sk = stableKeyForSerial(ev);
+ const QString nid = nodeIdForStableKey(sk);
+
+ QJsonObject params;
+ params.insert(QStringLiteral("edgeId"), m_edgeId);
+ params.insert(QStringLiteral("stableKey"), sk);
+ params.insert(QStringLiteral("online"), true);
+ params.insert(QStringLiteral("baudRate"), 115200);
+ params.insert(QStringLiteral("protocol"), QStringLiteral("modbus_rtu"));
+ params.insert(QStringLiteral("discoveredVia"), QStringLiteral("udev"));
+ if (ev.props.contains(QStringLiteral("SYSPATH"))) {
+ params.insert(QStringLiteral("syspath"), ev.props.value(QStringLiteral("SYSPATH")));
+ }
+
+ QJsonObject node;
+ node.insert(QStringLiteral("id"), nid);
+ node.insert(QStringLiteral("name"), QStringLiteral("discovered %1").arg(ev.devnode));
+ node.insert(QStringLiteral("type"), QStringLiteral("serial"));
+ node.insert(QStringLiteral("endpoint"), ev.devnode);
+ node.insert(QStringLiteral("enabled"), true);
+ node.insert(QStringLiteral("params"), params);
+
+ PendingUpsert pu;
+ pu.stableKey = sk;
+ pu.node = node;
+ m_pendingUpsert.insert(sk, pu);
+ m_devnodeToStableKey.insert(ev.devnode, sk);
+
+ QJsonObject recent;
+ recent.insert(QStringLiteral("ts"), ev.timestamp.toString(Qt::ISODate));
+ recent.insert(QStringLiteral("action"), QStringLiteral("add"));
+ recent.insert(QStringLiteral("devnode"), ev.devnode);
+ recent.insert(QStringLiteral("nodeId"), nid);
+ {
+ QMutexLocker lk(&m_recentMutex);
+ m_recent.push_back(recent);
+ if (m_recent.size() > 64) {
+ m_recent.remove(0, m_recent.size() - 64);
+ }
+ }
+
+ scheduleFlush();
+}
+
+void EdgeDiscoveryCoordinator::enqueueOfflinePatch(const QString& devnode)
+{
+ const QString sk = m_devnodeToStableKey.value(devnode);
+ if (sk.isEmpty()) {
+ return;
+ }
+ const QString nid = nodeIdForStableKey(sk);
+
+ QJsonObject recent;
+ recent.insert(QStringLiteral("ts"), QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
+ recent.insert(QStringLiteral("action"), QStringLiteral("remove"));
+ recent.insert(QStringLiteral("devnode"), devnode);
+ recent.insert(QStringLiteral("nodeId"), nid);
+ {
+ QMutexLocker lk(&m_recentMutex);
+ m_recent.push_back(recent);
+ if (m_recent.size() > 64) {
+ m_recent.remove(0, m_recent.size() - 64);
+ }
+ }
+
+ m_pendingUpsert.remove(sk);
+ m_devnodeToStableKey.remove(devnode);
+
+ QJsonObject patchEntry;
+ patchEntry.insert(QStringLiteral("op"), QStringLiteral("patch"));
+ patchEntry.insert(QStringLiteral("id"), nid);
+ QJsonObject patch;
+ QJsonObject pparams;
+ pparams.insert(QStringLiteral("online"), false);
+ patch.insert(QStringLiteral("params"), pparams);
+ patchEntry.insert(QStringLiteral("patch"), patch);
+ m_pendingPatchEntries.append(patchEntry);
+
+ scheduleFlush();
+}
+
+void EdgeDiscoveryCoordinator::handleDeviceEvent(const softbus::device_bus::monitor::DeviceEvent& ev)
+{
+ if (ev.type != softbus::device_bus::monitor::DeviceEventType::Serial) {
+ return;
+ }
+ if (ev.action == softbus::device_bus::monitor::DeviceEventAction::Add) {
+ enqueueUpsert(ev);
+ } else if (ev.action == softbus::device_bus::monitor::DeviceEventAction::Remove) {
+ enqueueOfflinePatch(ev.devnode);
+ }
+}
+
+void EdgeDiscoveryCoordinator::scheduleFlush()
+{
+ if (!m_debounceTimer) {
+ m_debounceTimer = new QTimer(this);
+ m_debounceTimer->setSingleShot(true);
+ connect(m_debounceTimer, &QTimer::timeout, this, [this]() { flushPending(); });
+ }
+ m_debounceTimer->start(400);
+}
+
+void EdgeDiscoveryCoordinator::flushPending()
+{
+ if (!m_session || m_edgeId.isEmpty()) {
+ return;
+ }
+ if (m_proposeInFlight) {
+ return;
+ }
+ const qint64 base = m_getCoreRevision ? m_getCoreRevision() : 0;
+ if (base <= 0) {
+ LOG_INFO() << "EdgeDiscoveryCoordinator: skip flush, core treeRevision not ready yet";
+ return;
+ }
+ if (m_pendingUpsert.isEmpty() && m_pendingPatchEntries.isEmpty()) {
+ return;
+ }
+
+ QJsonArray entries;
+ for (auto it = m_pendingUpsert.constBegin(); it != m_pendingUpsert.constEnd(); ++it) {
+ QJsonObject e;
+ e.insert(QStringLiteral("op"), QStringLiteral("upsert"));
+ e.insert(QStringLiteral("id"), it->node.value(QStringLiteral("id")).toString());
+ e.insert(QStringLiteral("node"), it->node);
+ entries.append(e);
+ }
+ for (const QJsonObject& pe : m_pendingPatchEntries) {
+ entries.append(pe);
+ }
+
+ if (entries.isEmpty()) {
+ return;
+ }
+
+ QJsonObject body;
+ body.insert(QStringLiteral("edgeId"), m_edgeId);
+ body.insert(QStringLiteral("baseTreeRevision"), base);
+ body.insert(QStringLiteral("entries"), entries);
+
+ if (!m_session->sendTreePropose(body)) {
+ LOG_WARNING() << "EdgeDiscoveryCoordinator: sendTreePropose failed";
+ return;
+ }
+
+ m_proposeInFlight = true;
+ LOG_INFO() << "EdgeDiscoveryCoordinator: sent TREE_PROPOSE" << entries.size() << "entries baseRev" << base;
+}
+
+void EdgeDiscoveryCoordinator::onTreeResultFromCore(const QJsonObject& o)
+{
+ if (o.value(QStringLiteral("ok")).toBool()) {
+ const QString msg = o.value(QStringLiteral("message")).toString();
+ if (msg == QStringLiteral("hello")) {
+ m_proposeInFlight = false;
+ if (o.contains(QStringLiteral("currentTreeRevision"))) {
+ m_lastKnownCoreRevision = o.value(QStringLiteral("currentTreeRevision")).toVariant().toLongLong();
+ }
+ scheduleFlush();
+ return;
+ }
+ m_proposeInFlight = false;
+ if (o.contains(QStringLiteral("newTreeRevision"))) {
+ m_lastKnownCoreRevision = o.value(QStringLiteral("newTreeRevision")).toVariant().toLongLong();
+ m_pendingUpsert.clear();
+ m_pendingPatchEntries.clear();
+ } else if (o.contains(QStringLiteral("currentTreeRevision"))) {
+ m_lastKnownCoreRevision = o.value(QStringLiteral("currentTreeRevision")).toVariant().toLongLong();
+ }
+ scheduleFlush();
+ return;
+ }
+ m_proposeInFlight = false;
+ if (o.value(QStringLiteral("code")).toString() == QStringLiteral("CONFLICT")) {
+ if (o.contains(QStringLiteral("currentTreeRevision"))) {
+ m_lastKnownCoreRevision = o.value(QStringLiteral("currentTreeRevision")).toVariant().toLongLong();
+ }
+ LOG_WARNING() << "EdgeDiscoveryCoordinator: CONFLICT from core, retrying pending";
+ scheduleFlush();
+ }
+}
+
+QJsonArray EdgeDiscoveryCoordinator::recentDiscoveriesJson() const
+{
+ QMutexLocker lk(&m_recentMutex);
+ QJsonArray arr;
+ for (const QJsonObject& o : m_recent) {
+ arr.append(o);
+ }
+ return arr;
+}
+
+bool EdgeDiscoveryCoordinator::start()
+{
+ if (m_udev) {
+ return true;
+ }
+ m_udev = std::make_unique(this);
+ m_udev->setCallback([this](const softbus::device_bus::monitor::DeviceEvent& ev) { handleDeviceEvent(ev); });
+ if (!m_udev->start()) {
+ m_udev.reset();
+ LOG_WARNING() << "EdgeDiscoveryCoordinator: udev monitor not started (non-Linux or udev init failed)";
+ return false;
+ }
+ const auto snap = m_udev->snapshotExisting();
+ for (const auto& ev : snap) {
+ handleDeviceEvent(ev);
+ }
+ LOG_INFO() << "EdgeDiscoveryCoordinator: started, initial snapshot size" << snap.size();
+ return true;
+}
+
+void EdgeDiscoveryCoordinator::stop()
+{
+ if (m_debounceTimer) {
+ m_debounceTimer->stop();
+ }
+ if (m_udev) {
+ m_udev->stop();
+ m_udev.reset();
+ }
+ m_pendingUpsert.clear();
+ m_pendingPatchEntries.clear();
+ m_devnodeToStableKey.clear();
+}
+
+} // namespace softbus_edge
diff --git a/src/device_bus/monitor/EdgeDiscoveryCoordinator.h b/src/device_bus/monitor/EdgeDiscoveryCoordinator.h
new file mode 100644
index 0000000..79afac5
--- /dev/null
+++ b/src/device_bus/monitor/EdgeDiscoveryCoordinator.h
@@ -0,0 +1,81 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QTimer;
+
+#include "device_bus/monitor/DeviceEvent.h"
+
+namespace softbus::device_bus::monitor
+{
+class UdevDeviceMonitor;
+}
+
+namespace softbus::message_bus::ingress
+{
+class EdgeSession;
+}
+
+namespace softbus_edge
+{
+
+/** udev 串口发现 → 组装 `TREE_PROPOSE`(含 `op: upsert`)向核心注册设备树节点。 */
+class EdgeDiscoveryCoordinator final : public QObject
+{
+public:
+ explicit EdgeDiscoveryCoordinator(QObject* parent = nullptr);
+ ~EdgeDiscoveryCoordinator() override;
+
+ void setEdgeSession(softbus::message_bus::ingress::EdgeSession* session);
+ void setEdgeId(const QString& edgeId);
+ void setCoreTreeRevisionGetter(std::function fn);
+
+ bool start();
+ void stop();
+
+ void onTreeResultFromCore(const QJsonObject& o);
+
+ QJsonArray recentDiscoveriesJson() const;
+
+private:
+ QString stableKeyForSerial(const softbus::device_bus::monitor::DeviceEvent& ev) const;
+ QString nodeIdForStableKey(const QString& stableKey) const;
+ void handleDeviceEvent(const softbus::device_bus::monitor::DeviceEvent& ev);
+ void enqueueUpsert(const softbus::device_bus::monitor::DeviceEvent& ev);
+ void enqueueOfflinePatch(const QString& devnode);
+ void scheduleFlush();
+ void flushPending();
+
+ softbus::message_bus::ingress::EdgeSession* m_session{nullptr};
+ QString m_edgeId;
+ std::function m_getCoreRevision;
+
+ std::unique_ptr m_udev;
+
+ struct PendingUpsert
+ {
+ QString stableKey;
+ QJsonObject node;
+ };
+ QHash m_pendingUpsert;
+ QVector m_pendingPatchEntries;
+ QHash m_devnodeToStableKey;
+
+ mutable QMutex m_recentMutex;
+ QVector m_recent;
+
+ qint64 m_lastKnownCoreRevision{0};
+ QTimer* m_debounceTimer{nullptr};
+ bool m_proposeInFlight{false};
+};
+
+} // namespace softbus_edge
diff --git a/src/device_bus/monitor/IDeviceMonitor.h b/src/device_bus/monitor/IDeviceMonitor.h
new file mode 100644
index 0000000..46d6486
--- /dev/null
+++ b/src/device_bus/monitor/IDeviceMonitor.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include
+#include
+
+#include
+
+#include "device_bus/monitor/DeviceEvent.h"
+
+namespace softbus::device_bus::monitor
+{
+
+class IDeviceMonitor : public QObject
+{
+ Q_OBJECT
+public:
+ using EventCallback = std::function;
+
+ explicit IDeviceMonitor(QObject* parent = nullptr)
+ : QObject(parent)
+ {
+ }
+ ~IDeviceMonitor() override = default;
+
+ virtual bool start() = 0;
+ virtual void stop() = 0;
+ virtual std::vector snapshotExisting() = 0;
+
+ void setCallback(EventCallback cb) { m_cb = std::move(cb); }
+
+protected:
+ void emitEvent(const DeviceEvent& ev)
+ {
+ if (m_cb) {
+ m_cb(ev);
+ }
+ emit event(ev);
+ }
+
+signals:
+ void event(const softbus::device_bus::monitor::DeviceEvent& ev);
+
+private:
+ EventCallback m_cb;
+};
+
+} // namespace softbus::device_bus::monitor
diff --git a/src/device_bus/monitor/UdevDeviceMonitor.cpp b/src/device_bus/monitor/UdevDeviceMonitor.cpp
new file mode 100644
index 0000000..3897f4f
--- /dev/null
+++ b/src/device_bus/monitor/UdevDeviceMonitor.cpp
@@ -0,0 +1,362 @@
+#include "device_bus/monitor/UdevDeviceMonitor.h"
+
+#if defined(__linux__)
+
+#include
+
+#include
+
+#include
+#include
+
+#include "utils/logs/logging.h"
+
+namespace softbus::device_bus::monitor
+{
+namespace
+{
+
+bool isWhitelistedSerialDevnode(const QString& devnode)
+{
+ if (devnode.startsWith(QStringLiteral("/dev/ttyUSB"))) {
+ return true;
+ }
+ if (devnode.startsWith(QStringLiteral("/dev/ttyACM"))) {
+ return true;
+ }
+
+ static const QRegularExpression kTtySRe(QStringLiteral("^/dev/ttyS(\\d+)$"));
+ const auto m = kTtySRe.match(devnode);
+ if (m.hasMatch()) {
+ bool ok = false;
+ const int n = m.captured(1).toInt(&ok);
+ if (ok && n >= 1 && n <= 6) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool isConsoleLikeTty(const QString& devnode)
+{
+ if (devnode == QStringLiteral("/dev/console")) {
+ return true;
+ }
+ if (devnode == QStringLiteral("/dev/tty")) {
+ return true;
+ }
+ if (devnode == QStringLiteral("/dev/ptmx")) {
+ return true;
+ }
+ if (devnode.startsWith(QStringLiteral("/dev/pts/"))) {
+ return true;
+ }
+
+ static const QRegularExpression kVtRe(QStringLiteral("^/dev/tty(\\d+)$"));
+ return kVtRe.match(devnode).hasMatch();
+}
+
+} // namespace
+
+UdevDeviceMonitor::UdevDeviceMonitor(QObject* parent)
+ : IDeviceMonitor(parent)
+{
+}
+
+UdevDeviceMonitor::~UdevDeviceMonitor()
+{
+ stop();
+}
+
+bool UdevDeviceMonitor::start()
+{
+ if (m_mon) {
+ return true;
+ }
+
+ m_udev = udev_new();
+ if (!m_udev) {
+ LOG_ERROR() << "UdevDeviceMonitor: udev_new failed";
+ return false;
+ }
+
+ m_mon = udev_monitor_new_from_netlink(m_udev, "udev");
+ if (!m_mon) {
+ LOG_ERROR() << "UdevDeviceMonitor: udev_monitor_new_from_netlink failed";
+ udev_unref(m_udev);
+ m_udev = nullptr;
+ return false;
+ }
+
+ udev_monitor_filter_add_match_subsystem_devtype(m_mon, "tty", nullptr);
+ udev_monitor_filter_add_match_subsystem_devtype(m_mon, "usb", nullptr);
+ udev_monitor_filter_add_match_subsystem_devtype(m_mon, "block", nullptr);
+
+ if (udev_monitor_enable_receiving(m_mon) != 0) {
+ LOG_ERROR() << "UdevDeviceMonitor: enable_receiving failed";
+ stop();
+ return false;
+ }
+
+ m_fd = udev_monitor_get_fd(m_mon);
+ if (m_fd < 0) {
+ LOG_ERROR() << "UdevDeviceMonitor: monitor fd invalid";
+ stop();
+ return false;
+ }
+
+ m_notifier = new QSocketNotifier(m_fd, QSocketNotifier::Read, this);
+ connect(m_notifier, &QSocketNotifier::activated, this, [this]() { onReadable(); });
+
+ LOG_INFO() << "UdevDeviceMonitor started fd=" << m_fd;
+ return true;
+}
+
+void UdevDeviceMonitor::stop()
+{
+ if (m_notifier) {
+ m_notifier->setEnabled(false);
+ m_notifier->deleteLater();
+ m_notifier = nullptr;
+ }
+ m_fd = -1;
+
+ if (m_mon) {
+ udev_monitor_unref(m_mon);
+ m_mon = nullptr;
+ }
+ if (m_udev) {
+ udev_unref(m_udev);
+ m_udev = nullptr;
+ }
+}
+
+void UdevDeviceMonitor::onReadable()
+{
+ if (!m_mon) {
+ return;
+ }
+
+ while (true) {
+ struct udev_device* dev = udev_monitor_receive_device(m_mon);
+ if (!dev) {
+ break;
+ }
+
+ const char* action = udev_device_get_action(dev);
+ DeviceEvent ev = toEvent(dev, action);
+
+ if (!shouldKeepEvent(ev)) {
+ udev_device_unref(dev);
+ continue;
+ }
+
+ emitEvent(ev);
+ udev_device_unref(dev);
+ }
+}
+
+std::vector UdevDeviceMonitor::snapshotExisting()
+{
+ std::vector out;
+ if (!m_udev) {
+ m_udev = udev_new();
+ if (!m_udev) {
+ LOG_ERROR() << "UdevDeviceMonitor: snapshot udev_new failed";
+ return out;
+ }
+ }
+
+ udev_enumerate* en = udev_enumerate_new(m_udev);
+ if (!en) {
+ return out;
+ }
+
+ udev_enumerate_add_match_subsystem(en, "tty");
+ udev_enumerate_add_match_subsystem(en, "usb");
+ udev_enumerate_add_match_subsystem(en, "block");
+ if (udev_enumerate_scan_devices(en) != 0) {
+ udev_enumerate_unref(en);
+ return out;
+ }
+
+ udev_list_entry* devices = udev_enumerate_get_list_entry(en);
+ udev_list_entry* entry = nullptr;
+ udev_list_entry_foreach(entry, devices)
+ {
+ const char* path = udev_list_entry_get_name(entry);
+ if (!path) {
+ continue;
+ }
+
+ udev_device* dev = udev_device_new_from_syspath(m_udev, path);
+ if (!dev) {
+ continue;
+ }
+
+ DeviceEvent ev = toEvent(dev, "add");
+ if (shouldKeepEvent(ev)) {
+ out.push_back(ev);
+ }
+
+ udev_device_unref(dev);
+ }
+
+ udev_enumerate_unref(en);
+ return out;
+}
+
+bool UdevDeviceMonitor::shouldKeepEvent(const DeviceEvent& ev) const
+{
+ if (ev.action == DeviceEventAction::Unknown) {
+ return false;
+ }
+
+ if (ev.subsystem == QStringLiteral("tty")) {
+ if (ev.devnode.isEmpty()) {
+ return false;
+ }
+ if (isConsoleLikeTty(ev.devnode)) {
+ return false;
+ }
+ if (!isWhitelistedSerialDevnode(ev.devnode)) {
+ return false;
+ }
+ }
+
+ if (ev.subsystem == QStringLiteral("usb")) {
+ const QString devtype = ev.props.value(QStringLiteral("DEVTYPE"));
+ if (!devtype.isEmpty() && devtype != QStringLiteral("usb_device")) {
+ return false;
+ }
+ if (ev.action == DeviceEventAction::Change) {
+ return false;
+ }
+ if (ev.devnode.isEmpty() && ev.vid.isEmpty() && ev.pid.isEmpty() && ev.serial.isEmpty()) {
+ return false;
+ }
+ if (ev.vid == QStringLiteral("1d6b")) {
+ return false;
+ }
+ const QString modelLower = ev.model.toLower();
+ if (modelLower.contains(QStringLiteral("hub")) || modelLower.contains(QStringLiteral("host_controller"))
+ || modelLower.contains(QStringLiteral("xhci"))) {
+ return false;
+ }
+ }
+
+ if (ev.subsystem == QStringLiteral("block")) {
+ if (ev.type != DeviceEventType::Storage) {
+ return false;
+ }
+ if (ev.action == DeviceEventAction::Change) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+QString UdevDeviceMonitor::getProp(struct udev_device* dev, const char* key)
+{
+ if (!dev || !key) {
+ return {};
+ }
+ const char* v = udev_device_get_property_value(dev, key);
+ return v ? QString::fromUtf8(v) : QString();
+}
+
+DeviceEvent UdevDeviceMonitor::toEvent(struct udev_device* dev, const char* action)
+{
+ DeviceEvent ev;
+ ev.timestamp = QDateTime::currentDateTimeUtc();
+
+ const char* subsys = udev_device_get_subsystem(dev);
+ if (subsys) {
+ ev.subsystem = QString::fromUtf8(subsys);
+ }
+
+ const char* devtype = udev_device_get_devtype(dev);
+ if (devtype) {
+ ev.props.insert(QStringLiteral("DEVTYPE"), QString::fromUtf8(devtype));
+ }
+
+ const char* syspath = udev_device_get_syspath(dev);
+ if (syspath) {
+ ev.props.insert(QStringLiteral("SYSPATH"), QString::fromUtf8(syspath));
+ }
+
+ const char* devnode = udev_device_get_devnode(dev);
+ if (devnode) {
+ ev.devnode = QString::fromUtf8(devnode);
+ }
+
+ const QString act = action ? QString::fromUtf8(action) : QString();
+ if (act == QStringLiteral("add")) {
+ ev.action = DeviceEventAction::Add;
+ } else if (act == QStringLiteral("remove")) {
+ ev.action = DeviceEventAction::Remove;
+ } else if (act == QStringLiteral("change")) {
+ ev.action = DeviceEventAction::Change;
+ } else {
+ ev.action = DeviceEventAction::Unknown;
+ }
+
+ if (ev.subsystem == QStringLiteral("tty")) {
+ ev.type = DeviceEventType::Serial;
+ } else if (ev.subsystem == QStringLiteral("usb")) {
+ ev.type = DeviceEventType::Usb;
+ } else if (ev.subsystem == QStringLiteral("block")) {
+ const QString idBus = getProp(dev, "ID_BUS");
+ if (idBus == QStringLiteral("usb")) {
+ ev.type = DeviceEventType::Storage;
+ } else {
+ struct udev_device* p = udev_device_get_parent_with_subsystem_devtype(dev, "usb", nullptr);
+ if (p) {
+ ev.type = DeviceEventType::Storage;
+ } else {
+ ev.type = DeviceEventType::Unknown;
+ }
+ }
+ } else {
+ ev.type = DeviceEventType::Unknown;
+ }
+
+ ev.vid = getProp(dev, "ID_VENDOR_ID");
+ ev.pid = getProp(dev, "ID_MODEL_ID");
+ ev.serial = getProp(dev, "ID_SERIAL_SHORT");
+ ev.model = getProp(dev, "ID_MODEL");
+ ev.vendor = getProp(dev, "ID_VENDOR");
+
+ if (ev.vid.isEmpty() && ev.subsystem == QStringLiteral("usb")) {
+ const char* a = udev_device_get_sysattr_value(dev, "idVendor");
+ if (a) {
+ ev.vid = QString::fromUtf8(a);
+ }
+ }
+ if (ev.pid.isEmpty() && ev.subsystem == QStringLiteral("usb")) {
+ const char* a = udev_device_get_sysattr_value(dev, "idProduct");
+ if (a) {
+ ev.pid = QString::fromUtf8(a);
+ }
+ }
+ if (ev.serial.isEmpty() && ev.subsystem == QStringLiteral("usb")) {
+ const char* a = udev_device_get_sysattr_value(dev, "serial");
+ if (a) {
+ ev.serial = QString::fromUtf8(a);
+ }
+ }
+
+ if (!ev.devnode.isEmpty()) {
+ ev.props.insert(QStringLiteral("DEVNAME"), ev.devnode);
+ }
+ if (!ev.subsystem.isEmpty()) {
+ ev.props.insert(QStringLiteral("SUBSYSTEM"), ev.subsystem);
+ }
+
+ return ev;
+}
+
+} // namespace softbus::device_bus::monitor
+
+#endif // __linux__
diff --git a/src/device_bus/monitor/UdevDeviceMonitor.h b/src/device_bus/monitor/UdevDeviceMonitor.h
new file mode 100644
index 0000000..624dfca
--- /dev/null
+++ b/src/device_bus/monitor/UdevDeviceMonitor.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "device_bus/monitor/IDeviceMonitor.h"
+
+#if defined(__linux__)
+
+#include
+
+#include
+
+namespace softbus::device_bus::monitor
+{
+
+class UdevDeviceMonitor final : public IDeviceMonitor
+{
+ Q_OBJECT
+public:
+ explicit UdevDeviceMonitor(QObject* parent = nullptr);
+ ~UdevDeviceMonitor() override;
+
+ bool start() override;
+ void stop() override;
+ std::vector snapshotExisting() override;
+
+private:
+ void onReadable();
+ bool shouldKeepEvent(const DeviceEvent& ev) const;
+ DeviceEvent toEvent(struct udev_device* dev, const char* action);
+ static QString getProp(struct udev_device* dev, const char* key);
+
+ struct udev* m_udev{nullptr};
+ struct udev_monitor* m_mon{nullptr};
+ int m_fd{-1};
+ QSocketNotifier* m_notifier{nullptr};
+};
+
+} // namespace softbus::device_bus::monitor
+
+#else
+
+namespace softbus::device_bus::monitor
+{
+
+class UdevDeviceMonitor final : public IDeviceMonitor
+{
+ Q_OBJECT
+public:
+ explicit UdevDeviceMonitor(QObject* parent = nullptr)
+ : IDeviceMonitor(parent)
+ {
+ }
+ ~UdevDeviceMonitor() override = default;
+
+ bool start() override { return false; }
+ void stop() override {}
+ std::vector snapshotExisting() override { return {}; }
+};
+
+} // namespace softbus::device_bus::monitor
+
+#endif
diff --git a/src/device_bus/tree/DeviceTreeModel.cpp b/src/device_bus/tree/DeviceTreeModel.cpp
new file mode 100644
index 0000000..4471c25
--- /dev/null
+++ b/src/device_bus/tree/DeviceTreeModel.cpp
@@ -0,0 +1,619 @@
+#include "device_bus/tree/DeviceTreeModel.h"
+
+#include
+
+#include
+#include
+#include
+
+#include "utils/logs/logging.h"
+
+namespace
+{
+
+void applyJsonPatchToDeviceTreeNode(DeviceTreeNode& node, const QJsonObject& patch)
+{
+ for (auto it = patch.constBegin(); it != patch.constEnd(); ++it) {
+ const QString k = it.key();
+ if (k == QStringLiteral("params") && it.value().isObject()) {
+ QJsonObject p = node.params;
+ const QJsonObject patchParams = it.value().toObject();
+ for (auto pit = patchParams.constBegin(); pit != patchParams.constEnd(); ++pit) {
+ const QString pk = pit.key();
+ if (pk == QStringLiteral("stableKey") || pk == QStringLiteral("online") || pk == QStringLiteral("lastSeenTs")) {
+ continue;
+ }
+ p.insert(pk, pit.value());
+ }
+ node.params = p;
+ } else if (k == QStringLiteral("name")) {
+ node.name = it.value().toString();
+ } else if (k == QStringLiteral("type")) {
+ node.type = it.value().toString();
+ } else if (k == QStringLiteral("endpoint")) {
+ node.endpoint = it.value().toString();
+ } else if (k == QStringLiteral("driver")) {
+ node.driver = it.value().toString();
+ } else if (k == QStringLiteral("capabilitiesRef")) {
+ node.capabilitiesRef = it.value().toString();
+ } else if (k == QStringLiteral("enabled")) {
+ node.enabled = it.value().toBool(node.enabled);
+ }
+ }
+}
+
+bool idAllowedForEdgeProposeNew(const QString& id, const QString& edgeId)
+{
+ if (id.isEmpty() || edgeId.isEmpty()) {
+ return false;
+ }
+ const QString rootId = QStringLiteral("edges/%1").arg(edgeId);
+ const QString prefix = rootId + QLatin1Char('/');
+ return (id == rootId) || id.startsWith(prefix);
+}
+
+bool childrenIdsAllowedForEdge(const QVector& children, const QString& edgeId)
+{
+ const QString rootId = QStringLiteral("edges/%1").arg(edgeId);
+ const QString prefix = rootId + QLatin1Char('/');
+ for (const QString& c : children) {
+ if (c.isEmpty()) {
+ return false;
+ }
+ if (!(c == rootId || c.startsWith(prefix))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+DeviceTreeNode deviceTreeNodeFromJsonObject(const QJsonObject& obj)
+{
+ DeviceTreeNode node;
+ node.id = obj.value(QStringLiteral("id")).toString();
+ node.name = obj.value(QStringLiteral("name")).toString();
+ node.type = obj.value(QStringLiteral("type")).toString();
+ node.endpoint = obj.value(QStringLiteral("endpoint")).toString();
+ node.driver = obj.value(QStringLiteral("driver")).toString();
+ node.capabilitiesRef = obj.value(QStringLiteral("capabilitiesRef")).toString();
+ node.params = obj.value(QStringLiteral("params")).toObject();
+ node.enabled = obj.value(QStringLiteral("enabled")).toBool(true);
+ const QJsonValue ch = obj.value(QStringLiteral("children"));
+ if (ch.isArray()) {
+ for (const QJsonValue& cv : ch.toArray()) {
+ if (cv.isString()) {
+ node.childrenIds.push_back(cv.toString());
+ }
+ }
+ }
+ return node;
+}
+
+void mergeUpsertJsonIntoNode(DeviceTreeNode& node, const QJsonObject& src)
+{
+ if (src.contains(QStringLiteral("name"))) {
+ node.name = src.value(QStringLiteral("name")).toString();
+ }
+ if (src.contains(QStringLiteral("type"))) {
+ node.type = src.value(QStringLiteral("type")).toString();
+ }
+ if (src.contains(QStringLiteral("endpoint"))) {
+ node.endpoint = src.value(QStringLiteral("endpoint")).toString();
+ }
+ if (src.contains(QStringLiteral("driver"))) {
+ node.driver = src.value(QStringLiteral("driver")).toString();
+ }
+ if (src.contains(QStringLiteral("capabilitiesRef"))) {
+ node.capabilitiesRef = src.value(QStringLiteral("capabilitiesRef")).toString();
+ }
+ if (src.contains(QStringLiteral("enabled"))) {
+ node.enabled = src.value(QStringLiteral("enabled")).toBool(node.enabled);
+ }
+ if (src.contains(QStringLiteral("params")) && src.value(QStringLiteral("params")).isObject()) {
+ QJsonObject p = node.params;
+ const QJsonObject patchParams = src.value(QStringLiteral("params")).toObject();
+ for (auto pit = patchParams.constBegin(); pit != patchParams.constEnd(); ++pit) {
+ p.insert(pit.key(), pit.value());
+ }
+ node.params = p;
+ }
+ if (src.contains(QStringLiteral("children")) && src.value(QStringLiteral("children")).isArray()) {
+ node.childrenIds.clear();
+ for (const QJsonValue& cv : src.value(QStringLiteral("children")).toArray()) {
+ if (cv.isString()) {
+ node.childrenIds.push_back(cv.toString());
+ }
+ }
+ }
+}
+
+} // namespace
+
+bool DeviceTreeModel::loadFromDaemonConfig(const QJsonObject& root, QString* errorMessage)
+{
+ m_nodes.clear();
+ m_indexById.clear();
+
+ if (!root.contains(QStringLiteral("device_tree"))) {
+ if (errorMessage) {
+ *errorMessage = QStringLiteral("daemon_config.json missing 'device_tree' field");
+ }
+ return false;
+ }
+
+ const QJsonValue treeVal = root.value(QStringLiteral("device_tree"));
+ if (!treeVal.isArray()) {
+ if (errorMessage) {
+ *errorMessage = QStringLiteral("'device_tree' must be an array");
+ }
+ return false;
+ }
+
+ const QJsonArray arr = treeVal.toArray();
+ m_nodes.reserve(arr.size());
+
+ for (const QJsonValue& v : arr) {
+ if (!v.isObject()) {
+ LOG_WARNING() << "DeviceTreeModel: skip non-object node in device_tree";
+ continue;
+ }
+ const QJsonObject obj = v.toObject();
+
+ DeviceTreeNode node;
+ node.id = obj.value(QStringLiteral("id")).toString();
+ node.name = obj.value(QStringLiteral("name")).toString();
+ node.type = obj.value(QStringLiteral("type")).toString();
+ node.endpoint = obj.value(QStringLiteral("endpoint")).toString();
+ node.driver = obj.value(QStringLiteral("driver")).toString();
+ node.capabilitiesRef = obj.value(QStringLiteral("capabilitiesRef")).toString();
+ node.params = obj.value(QStringLiteral("params")).toObject();
+ node.enabled = obj.value(QStringLiteral("enabled")).toBool(true);
+
+ // children: array of ids
+ const QJsonValue childrenVal = obj.value(QStringLiteral("children"));
+ if (childrenVal.isArray()) {
+ const QJsonArray childrenArr = childrenVal.toArray();
+ for (const QJsonValue& c : childrenArr) {
+ if (c.isString()) {
+ node.childrenIds.push_back(c.toString());
+ }
+ }
+ }
+
+ if (node.id.isEmpty()) {
+ LOG_WARNING() << "DeviceTreeModel: node without id, skipping";
+ continue;
+ }
+
+ const int index = m_nodes.size();
+ m_nodes.push_back(node);
+ m_indexById.insert(node.id, index);
+ }
+
+ LOG_INFO() << "DeviceTreeModel loaded" << m_nodes.size() << "nodes from daemon_config";
+ m_treeRevision.store(std::max(qint64(1),
+ root.value(QStringLiteral("device_tree_revision")).toVariant().toLongLong()),
+ std::memory_order_relaxed);
+ return true;
+}
+
+void DeviceTreeModel::rebuildIndex()
+{
+ m_indexById.clear();
+ for (int i = 0; i < m_nodes.size(); ++i) {
+ const auto& n = m_nodes.at(i);
+ if (!n.id.isEmpty()) {
+ m_indexById.insert(n.id, i);
+ }
+ }
+}
+
+namespace
+{
+
+QJsonObject deviceNodeToJsonObject(const DeviceTreeNode& n)
+{
+ QJsonObject obj;
+ obj.insert(QStringLiteral("id"), n.id);
+ if (!n.name.isEmpty()) {
+ obj.insert(QStringLiteral("name"), n.name);
+ }
+ if (!n.type.isEmpty()) {
+ obj.insert(QStringLiteral("type"), n.type);
+ }
+ if (!n.endpoint.isEmpty()) {
+ obj.insert(QStringLiteral("endpoint"), n.endpoint);
+ }
+ if (!n.driver.isEmpty()) {
+ obj.insert(QStringLiteral("driver"), n.driver);
+ }
+ if (!n.capabilitiesRef.isEmpty()) {
+ obj.insert(QStringLiteral("capabilitiesRef"), n.capabilitiesRef);
+ }
+ if (!n.params.isEmpty()) {
+ obj.insert(QStringLiteral("params"), n.params);
+ }
+ obj.insert(QStringLiteral("enabled"), n.enabled);
+
+ if (!n.childrenIds.isEmpty()) {
+ QJsonArray children;
+ for (const auto& c : n.childrenIds) {
+ children.append(c);
+ }
+ obj.insert(QStringLiteral("children"), children);
+ }
+ return obj;
+}
+
+} // namespace
+
+QJsonArray DeviceTreeModel::toDeviceTreeJson() const
+{
+ QJsonArray arr;
+
+ for (const auto& n : m_nodes) {
+ arr.append(deviceNodeToJsonObject(n));
+ }
+ return arr;
+}
+
+QJsonObject DeviceTreeModel::toDaemonConfigRoot() const
+{
+ QJsonObject root;
+ root.insert(QStringLiteral("device_tree"), toDeviceTreeJson());
+ return root;
+}
+
+const DeviceTreeNode* DeviceTreeModel::findById(const QString& id) const
+{
+ const int index = findIndexById(id);
+ if (index < 0 || index >= m_nodes.size()) {
+ return nullptr;
+ }
+ return &m_nodes.at(index);
+}
+
+int DeviceTreeModel::findIndexById(const QString& id) const
+{
+ const auto it = m_indexById.constFind(id);
+ if (it == m_indexById.constEnd()) {
+ return -1;
+ }
+ return it.value();
+}
+
+int DeviceTreeModel::findIndexByStableKey(const QString& stableKey) const
+{
+ if (stableKey.isEmpty()) {
+ return -1;
+ }
+ for (int i = 0; i < m_nodes.size(); ++i) {
+ const auto& n = m_nodes.at(i);
+ if (n.params.value(QStringLiteral("stableKey")).toString() == stableKey) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+int DeviceTreeModel::findIndexByEndpoint(const QString& endpoint) const
+{
+ if (endpoint.isEmpty()) {
+ return -1;
+ }
+ for (int i = 0; i < m_nodes.size(); ++i) {
+ const auto& n = m_nodes.at(i);
+ if (n.endpoint == endpoint) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+QString DeviceTreeModel::upsertNodeByStableKey(const QString& stableKey,
+ const QString& preferredId,
+ const QString& type,
+ const QString& endpoint,
+ const QString& name,
+ const QJsonObject& extraParams,
+ bool online,
+ const QString& lastSeenTs)
+{
+ int idx = findIndexByStableKey(stableKey);
+ if (idx < 0 && !preferredId.isEmpty()) {
+ idx = findIndexById(preferredId);
+ }
+
+ if (idx < 0) {
+ DeviceTreeNode node;
+ node.id = preferredId;
+ node.type = type;
+ node.endpoint = endpoint;
+ node.name = name;
+ node.enabled = true;
+ node.params = extraParams;
+ node.params.insert(QStringLiteral("stableKey"), stableKey);
+ node.params.insert(QStringLiteral("online"), online);
+ if (!lastSeenTs.isEmpty()) {
+ node.params.insert(QStringLiteral("lastSeenTs"), lastSeenTs);
+ }
+ m_nodes.push_back(node);
+ rebuildIndex();
+ return node.id;
+ }
+
+ auto& node = m_nodes[idx];
+ if (!type.isEmpty()) node.type = type;
+ if (!endpoint.isEmpty()) node.endpoint = endpoint;
+ if (!name.isEmpty()) node.name = name;
+ node.enabled = true;
+
+ for (auto it = extraParams.begin(); it != extraParams.end(); ++it) {
+ node.params.insert(it.key(), it.value());
+ }
+ node.params.insert(QStringLiteral("stableKey"), stableKey);
+ node.params.insert(QStringLiteral("online"), online);
+ if (!lastSeenTs.isEmpty()) {
+ node.params.insert(QStringLiteral("lastSeenTs"), lastSeenTs);
+ }
+ return node.id;
+}
+
+bool DeviceTreeModel::markOfflineByStableKey(const QString& stableKey, const QString& lastSeenTs)
+{
+ const int idx = findIndexByStableKey(stableKey);
+ if (idx < 0) {
+ return false;
+ }
+ auto& node = m_nodes[idx];
+ node.params.insert(QStringLiteral("online"), false);
+ if (!lastSeenTs.isEmpty()) {
+ node.params.insert(QStringLiteral("lastSeenTs"), lastSeenTs);
+ }
+ return true;
+}
+
+void DeviceTreeModel::bumpTreeRevision()
+{
+ m_treeRevision.fetch_add(1, std::memory_order_relaxed);
+}
+
+bool DeviceTreeModel::applyPatchToNodeById(const QString& id, const QJsonObject& patch)
+{
+ const int idx = findIndexById(id);
+ if (idx < 0) {
+ return false;
+ }
+ applyJsonPatchToDeviceTreeNode(m_nodes[idx], patch);
+ return true;
+}
+
+bool DeviceTreeModel::nodeBelongsToEdge(const DeviceTreeNode& node, const QString& edgeId)
+{
+ if (edgeId.isEmpty()) {
+ return false;
+ }
+ if (node.params.value(QStringLiteral("edgeId")).toString() == edgeId) {
+ return true;
+ }
+ const QString prefix = QStringLiteral("edges/%1/").arg(edgeId);
+ if (node.id == QStringLiteral("edges/%1").arg(edgeId) || node.id.startsWith(prefix)) {
+ return true;
+ }
+ return false;
+}
+
+QJsonArray DeviceTreeModel::exportSubtreeForEdge(const QString& edgeId, qint64* outRevisionAtExport) const
+{
+ QJsonArray arr;
+ if (edgeId.isEmpty()) {
+ return arr;
+ }
+ QMutexLocker locker(&m_writeMutex);
+ if (outRevisionAtExport) {
+ *outRevisionAtExport = m_treeRevision.load(std::memory_order_relaxed);
+ }
+ for (const auto& n : m_nodes) {
+ if (nodeBelongsToEdge(n, edgeId)) {
+ arr.append(deviceNodeToJsonObject(n));
+ }
+ }
+ return arr;
+}
+
+QVector DeviceTreeModel::downlinkEdgeIdsForNode(const DeviceTreeNode& node) const
+{
+ QVector out;
+ auto pushUnique = [&out](const QString& s) {
+ const QString t = s.trimmed();
+ if (t.isEmpty()) {
+ return;
+ }
+ for (const QString& x : out) {
+ if (x == t) {
+ return;
+ }
+ }
+ out.append(t);
+ };
+ pushUnique(node.params.value(QStringLiteral("edgeId")).toString());
+ static const QString pfx = QStringLiteral("edges/");
+ if (node.id.startsWith(pfx)) {
+ const QString rest = node.id.mid(pfx.size());
+ const int slash = rest.indexOf(QLatin1Char('/'));
+ pushUnique(slash < 0 ? rest : rest.left(slash));
+ }
+ return out;
+}
+
+DeviceTreeModel::EdgeProposeResult DeviceTreeModel::applyEdgePropose(const QString& edgeId,
+ qint64 baseRevision,
+ const QJsonArray& entries)
+{
+ EdgeProposeResult r;
+ QMutexLocker locker(&m_writeMutex);
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ if (entries.isEmpty()) {
+ r.ok = false;
+ r.code = QStringLiteral("INVALID_JSON");
+ r.message = QStringLiteral("entries must be non-empty");
+ return r;
+ }
+ if (baseRevision != m_treeRevision.load(std::memory_order_relaxed)) {
+ r.ok = false;
+ r.code = QStringLiteral("CONFLICT");
+ r.message = QStringLiteral("baseTreeRevision mismatch");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+
+ for (const QJsonValue& ev : entries) {
+ if (!ev.isObject()) {
+ r.ok = false;
+ r.code = QStringLiteral("INVALID_JSON");
+ r.message = QStringLiteral("entries must be objects");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ const QJsonObject eo = ev.toObject();
+ const QString id = eo.value(QStringLiteral("id")).toString();
+ const QString op = eo.value(QStringLiteral("op")).toString();
+
+ if (op.isEmpty() || op == QStringLiteral("patch")) {
+ const QJsonObject patch = eo.value(QStringLiteral("patch")).toObject();
+ if (id.isEmpty() || patch.isEmpty()) {
+ r.ok = false;
+ r.code = QStringLiteral("INVALID_JSON");
+ r.message = QStringLiteral("each patch entry needs id and patch");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ const DeviceTreeNode* node = findById(id);
+ if (!node) {
+ r.ok = false;
+ r.code = QStringLiteral("NODE_NOT_FOUND");
+ r.message = id;
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ if (!nodeBelongsToEdge(*node, edgeId)) {
+ r.ok = false;
+ r.code = QStringLiteral("FORBIDDEN_SCOPE");
+ r.message = id;
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ if (!applyPatchToNodeById(id, patch)) {
+ r.ok = false;
+ r.code = QStringLiteral("NODE_NOT_FOUND");
+ r.message = id;
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ } else if (op == QStringLiteral("upsert")) {
+ const QJsonObject nodeJo = eo.value(QStringLiteral("node")).toObject();
+ if (id.isEmpty() || nodeJo.isEmpty()) {
+ r.ok = false;
+ r.code = QStringLiteral("INVALID_JSON");
+ r.message = QStringLiteral("upsert entry needs id and non-empty node");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ if (nodeJo.contains(QStringLiteral("id"))) {
+ const QString nid = nodeJo.value(QStringLiteral("id")).toString();
+ if (!nid.isEmpty() && nid != id) {
+ r.ok = false;
+ r.code = QStringLiteral("INVALID_JSON");
+ r.message = QStringLiteral("node.id must match entry id");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ }
+ const int idx = findIndexById(id);
+ if (idx < 0) {
+ if (!idAllowedForEdgeProposeNew(id, edgeId)) {
+ r.ok = false;
+ r.code = QStringLiteral("FORBIDDEN_SCOPE");
+ r.message = id;
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ DeviceTreeNode n = deviceTreeNodeFromJsonObject(nodeJo);
+ if (n.id.isEmpty()) {
+ n.id = id;
+ }
+ if (n.id != id) {
+ r.ok = false;
+ r.code = QStringLiteral("INVALID_JSON");
+ r.message = QStringLiteral("upsert node id mismatch");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ if (!n.params.contains(QStringLiteral("edgeId"))) {
+ QJsonObject p = n.params;
+ p.insert(QStringLiteral("edgeId"), edgeId);
+ n.params = p;
+ }
+ if (!nodeBelongsToEdge(n, edgeId)) {
+ r.ok = false;
+ r.code = QStringLiteral("FORBIDDEN_SCOPE");
+ r.message = id;
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ if (!childrenIdsAllowedForEdge(n.childrenIds, edgeId)) {
+ r.ok = false;
+ r.code = QStringLiteral("FORBIDDEN_SCOPE");
+ r.message = QStringLiteral("children outside edge scope");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ m_nodes.push_back(n);
+ } else {
+ DeviceTreeNode& existing = m_nodes[idx];
+ if (!nodeBelongsToEdge(existing, edgeId)) {
+ r.ok = false;
+ r.code = QStringLiteral("FORBIDDEN_SCOPE");
+ r.message = id;
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ mergeUpsertJsonIntoNode(existing, nodeJo);
+ existing.id = id;
+ if (!existing.params.contains(QStringLiteral("edgeId"))) {
+ existing.params.insert(QStringLiteral("edgeId"), edgeId);
+ }
+ if (!nodeBelongsToEdge(existing, edgeId)) {
+ r.ok = false;
+ r.code = QStringLiteral("FORBIDDEN_SCOPE");
+ r.message = id;
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ if (!childrenIdsAllowedForEdge(existing.childrenIds, edgeId)) {
+ r.ok = false;
+ r.code = QStringLiteral("FORBIDDEN_SCOPE");
+ r.message = QStringLiteral("children outside edge scope");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ }
+ } else {
+ r.ok = false;
+ r.code = QStringLiteral("INVALID_JSON");
+ r.message = QStringLiteral("unknown op");
+ r.currentRevision = m_treeRevision.load(std::memory_order_relaxed);
+ return r;
+ }
+ rebuildIndex();
+ }
+
+ rebuildIndex();
+ bumpTreeRevision();
+ r.ok = true;
+ r.code = QStringLiteral("OK");
+ r.newRevision = m_treeRevision.load(std::memory_order_relaxed);
+ r.currentRevision = r.newRevision;
+ return r;
+}
+
diff --git a/src/device_bus/tree/DeviceTreeModel.h b/src/device_bus/tree/DeviceTreeModel.h
new file mode 100644
index 0000000..2c83fc2
--- /dev/null
+++ b/src/device_bus/tree/DeviceTreeModel.h
@@ -0,0 +1,89 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// 轻量级设备树模型:从 daemon_config.json 的 device_tree 字段加载
+
+struct DeviceTreeNode
+{
+ QString id;
+ QString name;
+ QString type;
+ QString endpoint;
+ QString driver;
+ QString capabilitiesRef;
+ QJsonObject params;
+ QVector childrenIds;
+ bool enabled{true};
+};
+
+class DeviceTreeModel
+{
+public:
+ // 从给定的 JSON 根对象(包含 device_tree 字段)加载
+ bool loadFromDaemonConfig(const QJsonObject& root, QString* errorMessage = nullptr);
+
+ const QVector& nodes() const { return m_nodes; }
+ QVector& nodesMutable() { return m_nodes; }
+
+ const DeviceTreeNode* findById(const QString& id) const;
+ int findIndexById(const QString& id) const;
+ int findIndexByEndpoint(const QString& endpoint) const;
+ int findIndexByStableKey(const QString& stableKey) const;
+
+ // Monitor/register path: upsert or mark online/offline
+ QString upsertNodeByStableKey(const QString& stableKey,
+ const QString& preferredId,
+ const QString& type,
+ const QString& endpoint,
+ const QString& name,
+ const QJsonObject& extraParams,
+ bool online,
+ const QString& lastSeenTs);
+ bool markOfflineByStableKey(const QString& stableKey, const QString& lastSeenTs);
+
+ // 序列化为 daemon_config.json 所需格式
+ QJsonArray toDeviceTreeJson() const;
+ QJsonObject toDaemonConfigRoot() const;
+
+ // 当外部直接修改 nodesMutable() 后,需要重建索引
+ void rebuildIndex();
+
+ // 与 D-Bus patch / 边缘 CTRL_TREE_PROPOSE 共用的写锁与 revision(见 docs/EDGE_UPLINK_PROTOCOL_v1.md)
+ QMutex& writeMutex() { return m_writeMutex; }
+ qint64 treeRevision() const { return m_treeRevision.load(std::memory_order_relaxed); }
+ void bumpTreeRevision();
+ bool applyPatchToNodeById(const QString& id, const QJsonObject& patch);
+
+ struct EdgeProposeResult
+ {
+ bool ok{false};
+ QString code; // OK, CONFLICT, FORBIDDEN_SCOPE, NODE_NOT_FOUND, INVALID_JSON
+ QString message;
+ qint64 newRevision{0};
+ qint64 currentRevision{0};
+ };
+ EdgeProposeResult applyEdgePropose(const QString& edgeId, qint64 baseRevision, const QJsonArray& entries);
+
+ /** 导出属于该 edge 的节点子集(与 applyEdgePropose 作用域规则一致),用于下行 TREE_FULL。
+ * 若传入 outRevisionAtExport,则写入导出时刻的 treeRevision(与 nodes 同一快照)。 */
+ QJsonArray exportSubtreeForEdge(const QString& edgeId, qint64* outRevisionAtExport = nullptr) const;
+
+ /** 节点若处于边缘作用域(params.edgeId 或 id 前缀 edges//...),返回应尝试下行 TREE_FULL 的 edgeId(去重)。 */
+ QVector downlinkEdgeIdsForNode(const DeviceTreeNode& node) const;
+
+private:
+ static bool nodeBelongsToEdge(const DeviceTreeNode& node, const QString& edgeId);
+
+ QVector m_nodes;
+ QHash m_indexById;
+ mutable QMutex m_writeMutex;
+ std::atomic m_treeRevision{1};
+};
+
diff --git a/src/devices/DeviceTypes.cpp b/src/devices/DeviceTypes.cpp
new file mode 100644
index 0000000..b1b9881
--- /dev/null
+++ b/src/devices/DeviceTypes.cpp
@@ -0,0 +1,66 @@
+#include "devices/DeviceTypes.h"
+
+namespace softbus::devices {
+
+DeviceKind kindFromType(const QString &typeStr) {
+ // 使用静态 QHash,仅在首次调用时初始化一次,后续调用直接极速查表
+ static const QHash typeMap = {
+ {QStringLiteral("serial"), DeviceKind::Serial},
+ {QStringLiteral("can"), DeviceKind::Can},
+ {QStringLiteral("ethernet"), DeviceKind::Network},
+ {QStringLiteral("network"), DeviceKind::Network},
+ {QStringLiteral("virtual"), DeviceKind::Virtual},
+ {QStringLiteral("distributed"), DeviceKind::Distributed}};
+
+ // value() 方法在找不到键时,会返回传入的第二个默认参数
+ return typeMap.value(typeStr.trimmed().toLower(), DeviceKind::Unknown);
+}
+
+DeviceKind inferDeviceKindFromEndpoint(const QString &endpoint) {
+ const QString ep = endpoint.trimmed();
+ // 使用结构体数组来存储设备类型和前缀的映射
+ struct Rule {
+ const QString prefix;
+ DeviceKind kind;
+ };
+
+ static const Rule rules[] = {
+ {QStringLiteral("/dev/tty"), DeviceKind::Serial},
+ {QStringLiteral("can"), DeviceKind::Can},
+ {QStringLiteral("tcp://"), DeviceKind::Tcp},
+ {QStringLiteral("udp://"), DeviceKind::Udp}
+ // 未来在这里添加其他设备类型
+ };
+
+ // 遍历规则表进行前缀匹配
+ for (const auto &rule : rules) {
+ if (ep.startsWith(rule.prefix)) {
+ return rule.kind;
+ }
+ }
+
+ return DeviceKind::Unknown;
+}
+
+QString driverKeyFor(DeviceKind kind) {
+ switch (kind) {
+ case DeviceKind::Serial:
+ return QStringLiteral("serial_qt");
+ case DeviceKind::Can:
+ return QStringLiteral("can_socketcan");
+ case DeviceKind::Tcp:
+ return QStringLiteral("tcp_qt");
+ case DeviceKind::Udp:
+ return QStringLiteral("udp_qt");
+ case DeviceKind::Network:
+ return QStringLiteral("network_qt");
+ case DeviceKind::Virtual:
+ return QStringLiteral("virtual_device");
+ case DeviceKind::Distributed:
+ return QStringLiteral("distributed_node");
+ default:
+ return QStringLiteral("unknown");
+ }
+}
+
+} // namespace softbus::devices
diff --git a/src/devices/DeviceTypes.h b/src/devices/DeviceTypes.h
new file mode 100644
index 0000000..908d0c9
--- /dev/null
+++ b/src/devices/DeviceTypes.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace softbus::devices
+{
+
+
+
+
+// 具体设备种类(用于选择 driver / 解析 endpoint)
+enum class DeviceKind {
+ Unknown,
+ Serial,
+ Can,
+ Tcp,
+ Udp,
+ Network, // 泛指 network(无法区分 tcp/udp 时)
+ Virtual,
+ Distributed,
+};
+
+inline QString deviceKindToString(DeviceKind kind)
+{
+ switch (kind) {
+ case DeviceKind::Serial:
+ return QStringLiteral("serial");
+ case DeviceKind::Can:
+ return QStringLiteral("can");
+ case DeviceKind::Tcp:
+ return QStringLiteral("tcp");
+ case DeviceKind::Udp:
+ return QStringLiteral("udp");
+ case DeviceKind::Network:
+ return QStringLiteral("network");
+ case DeviceKind::Virtual:
+ return QStringLiteral("virtual");
+ case DeviceKind::Distributed:
+ return QStringLiteral("distributed");
+ case DeviceKind::Unknown:
+ default:
+ return QStringLiteral("unknown");
+ }
+}
+
+struct DeviceInfo
+{
+ int id{0};
+ QString endpoint;
+ QString name;
+ QString type; // "serial" / "can" / "network" / ...
+ DeviceKind kind{DeviceKind::Unknown};
+ int address{0};
+ QString protocol;
+ QString protocolDetail;
+ QJsonObject properties;
+ QString status{QStringLiteral("online")};
+ bool isActive{true};
+ QDateTime lastSeen;
+
+ QJsonObject toJsonObject() const
+ {
+ QJsonObject obj;
+ obj[QStringLiteral("id")] = id;
+ obj[QStringLiteral("endpoint")] = endpoint;
+ obj[QStringLiteral("name")] = name;
+ obj[QStringLiteral("type")] = type;
+ obj[QStringLiteral("kind")] = static_cast(kind);
+ obj[QStringLiteral("kindName")] = deviceKindToString(kind);
+ obj[QStringLiteral("address")] = address;
+ obj[QStringLiteral("protocol")] = protocol;
+ obj[QStringLiteral("protocol_detail")] = protocolDetail;
+ obj[QStringLiteral("properties")] = properties;
+ obj[QStringLiteral("status")] = status;
+ obj[QStringLiteral("isActive")] = isActive;
+ if (lastSeen.isValid()) {
+ obj[QStringLiteral("lastSeen")] = lastSeen.toString(Qt::ISODate);
+ }
+ return obj;
+ }
+
+ QString toJson() const
+ {
+ const QJsonDocument doc(toJsonObject());
+ return QString::fromUtf8(doc.toJson(QJsonDocument::Compact));
+ }
+};
+
+struct BusMessage
+{
+ QString id;
+ QString source;
+ QString destination;
+ QJsonObject payload;
+ qint64 timestamp{0};
+};
+
+// 纯 helper:不做 IO,仅做字符串/endpoint 到 DeviceKind/driverKey 的映射
+DeviceKind kindFromType(const QString& typeStr);
+DeviceKind inferDeviceKindFromEndpoint(const QString& endpoint);
+QString driverKeyFor(DeviceKind kind);
+
+} // namespace softbus::devices
+
diff --git a/src/devices/base/IDevice.h b/src/devices/base/IDevice.h
new file mode 100644
index 0000000..d37f8ec
--- /dev/null
+++ b/src/devices/base/IDevice.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "devices/DeviceTypes.h"
+
+namespace softbus::devices
+{
+
+class IDevice
+{
+public:
+ virtual ~IDevice() = default;
+
+ virtual softbus::devices::DeviceInfo deviceInfo() const = 0;
+};
+
+} // namespace softbus::devices
+
diff --git a/src/devices/base/IPhysicalDevice.h b/src/devices/base/IPhysicalDevice.h
new file mode 100644
index 0000000..cd52e15
--- /dev/null
+++ b/src/devices/base/IPhysicalDevice.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "devices/DeviceTypes.h"
+#include "devices/base/IDevice.h"
+
+namespace softbus::devices::base
+{
+
+class IPhysicalDevice : public IDevice
+{
+public:
+ ~IPhysicalDevice() override = default;
+
+ virtual bool start() = 0;
+ virtual void stop() = 0;
+};
+
+} // namespace softbus::devices::base
+
diff --git a/src/devices/physical/CanDevice.cpp b/src/devices/physical/CanDevice.cpp
new file mode 100644
index 0000000..4f29077
--- /dev/null
+++ b/src/devices/physical/CanDevice.cpp
@@ -0,0 +1,147 @@
+#include "devices/physical/CanDevice.h"
+
+#include
+
+#include
+#include
+#include
+
+#include "core/memory/MemoryPool.h"
+#include "core/models/MessageRoutingUtils.h"
+#include "core/models/RawBusMessage.h"
+#include "message_bus/ingress/IIngressPort.h"
+#include "utils/logs/logging.h"
+
+namespace softbus::devices::physical
+{
+
+static QString socketCanInterfaceName(const QString& endpoint)
+{
+ QString ep = endpoint.trimmed();
+ if (ep.startsWith(QStringLiteral("can:"), Qt::CaseInsensitive)) {
+ return ep.mid(4);
+ }
+ return ep;
+}
+
+CanDevice::CanDevice(std::unique_ptr bus, const Config& config, const softbus::devices::DeviceInfo& info)
+ : m_bus(std::move(bus))
+ , m_config(config)
+ , m_info(info)
+{
+}
+
+CanDevice::~CanDevice()
+{
+ stop();
+}
+
+bool CanDevice::start()
+{
+ if (!m_bus) {
+ LOG_ERROR() << "CanDevice: null bus";
+ return false;
+ }
+ QObject::connect(m_bus.get(), &QCanBusDevice::framesReceived, m_bus.get(), [this]() { onFramesAvailable(); });
+ QObject::connect(m_bus.get(), &QCanBusDevice::errorOccurred, m_bus.get(), [this](QCanBusDevice::CanBusError) {
+ onError();
+ });
+
+ if (m_config.bitrate > 0) {
+ m_bus->setConfigurationParameter(QCanBusDevice::BitRateKey, m_config.bitrate);
+ }
+ LOG_INFO() << "CanDevice: connectDevice" << m_config.endpoint << "iface" << socketCanInterfaceName(m_config.endpoint);
+ return m_bus->connectDevice();
+}
+
+void CanDevice::stop()
+{
+ if (!m_bus) {
+ return;
+ }
+ QObject::disconnect(m_bus.get(), nullptr, m_bus.get(), nullptr);
+ if (m_bus->state() == QCanBusDevice::ConnectedState) {
+ m_bus->disconnectDevice();
+ }
+ m_bus.reset();
+}
+
+softbus::devices::DeviceInfo CanDevice::deviceInfo() const
+{
+ return m_info;
+}
+
+QString CanDevice::endpoint() const
+{
+ return m_config.endpoint;
+}
+
+softbus::devices::DeviceKind CanDevice::kind() const
+{
+ return softbus::devices::DeviceKind::Can;
+}
+
+void CanDevice::bindIngress(softbus::message_bus::ingress::IIngressPort* ingress,
+ std::shared_ptr pool)
+{
+ m_ingress = ingress;
+ m_pool = std::move(pool);
+}
+
+void CanDevice::onFramesAvailable()
+{
+ if (!m_ingress || !m_pool || !m_bus) {
+ return;
+ }
+ while (m_bus->framesAvailable()) {
+ const QCanBusFrame frame = m_bus->readFrame();
+ if (frame.frameType() != QCanBusFrame::DataFrame && frame.frameType() != QCanBusFrame::RemoteRequestFrame) {
+ continue;
+ }
+ const QByteArray busPayload = frame.payload();
+ auto block = m_pool->allocate(static_cast(busPayload.size()));
+ if (!block || !block->data || block->capacity < static_cast(busPayload.size())) {
+ m_ingress->reportError(m_config.endpoint, QStringLiteral("memory pool exhausted"));
+ return;
+ }
+ if (!busPayload.isEmpty()) {
+ std::memcpy(block->data, busPayload.constData(), static_cast(busPayload.size()));
+ }
+ block->size = static_cast(busPayload.size());
+
+ softbus::core::models::RawBusMessage ctx;
+ ctx.direction = softbus::core::models::BusDirection::Upstream;
+ ctx.endpointHash = softbus::core::models::makeEndpointHash(m_config.endpoint);
+ ctx.protocol = softbus::core::models::protocolTypeFromHint(m_info.protocol);
+ if (ctx.protocol == softbus::core::models::ProtocolType::UNKNOWN) {
+ ctx.protocol = softbus::core::models::ProtocolType::CAN_RAW;
+ }
+ ctx.header.can.cobId = static_cast(frame.frameId() & 0x1FFFFFFFU);
+ ctx.header.can.isExtended = frame.hasExtendedFrameFormat();
+ ctx.header.can.isRtr = (frame.frameType() == QCanBusFrame::RemoteRequestFrame);
+ ctx.logicalAddress = ctx.header.can.cobId & 0xFFU;
+ ctx.routingKey = softbus::core::models::makeRoutingKey(ctx.endpointHash, m_info.id);
+ ctx.timestampMs = QDateTime::currentMSecsSinceEpoch();
+ ctx.deviceId = m_info.id;
+ ctx.payload.block = std::move(block);
+ ctx.payload.length = static_cast(busPayload.size());
+ ctx.traceId = QStringLiteral("can:%1:%2:%3")
+ .arg(m_config.endpoint)
+ .arg(ctx.header.can.cobId)
+ .arg(ctx.timestampMs);
+
+ if (!m_ingress->push(std::move(ctx))) {
+ m_ingress->reportError(m_config.endpoint, QStringLiteral("ingress queue full"));
+ }
+ }
+}
+
+void CanDevice::onError()
+{
+ if (!m_ingress || !m_bus) {
+ return;
+ }
+ m_ingress->reportError(m_config.endpoint, m_bus->errorString());
+}
+
+} // namespace softbus::devices::physical
diff --git a/src/devices/physical/CanDevice.h b/src/devices/physical/CanDevice.h
new file mode 100644
index 0000000..270c849
--- /dev/null
+++ b/src/devices/physical/CanDevice.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include
+
+#include
+
+#include "devices/DeviceTypes.h"
+#include "devices/base/IPhysicalDevice.h"
+
+namespace softbus::message_bus::ingress { class IIngressPort; }
+namespace softbus::core::memory { class MemoryPool; }
+
+class QCanBusDevice;
+
+namespace softbus::devices::physical
+{
+
+/** SocketCAN 上行:读帧组装 `RawBusMessage`(`protocol: CAN_RAW`,`header.can` 填 CAN id)。 */
+class CanDevice : public base::IPhysicalDevice
+{
+public:
+ struct Config
+ {
+ QString endpoint; // 约定 `can:`,如 `can:vcan0`
+ int bitrate{500000};
+ };
+
+ CanDevice(std::unique_ptr bus, const Config& config, const softbus::devices::DeviceInfo& info);
+ ~CanDevice() override;
+
+ bool start() override;
+ void stop() override;
+
+ softbus::devices::DeviceInfo deviceInfo() const override;
+
+ QString endpoint() const;
+ softbus::devices::DeviceKind kind() const;
+ void bindIngress(softbus::message_bus::ingress::IIngressPort* ingress,
+ std::shared_ptr pool);
+
+private:
+ void onFramesAvailable();
+ void onError();
+
+private:
+ std::unique_ptr m_bus;
+ Config m_config;
+ softbus::devices::DeviceInfo m_info;
+ softbus::message_bus::ingress::IIngressPort* m_ingress{nullptr};
+ std::shared_ptr m_pool;
+};
+
+} // namespace softbus::devices::physical
diff --git a/src/devices/physical/SerialDevice.cpp b/src/devices/physical/SerialDevice.cpp
new file mode 100644
index 0000000..f9aec83
--- /dev/null
+++ b/src/devices/physical/SerialDevice.cpp
@@ -0,0 +1,145 @@
+#include "devices/physical/SerialDevice.h"
+
+#include
+#include
+#include
+
+#include
+
+#include "core/memory/MemoryPool.h"
+#include "core/models/MessageRoutingUtils.h"
+#include "core/models/RawBusMessage.h"
+#include "hardware/drivers/serial/SerialQtDriver.h"
+#include "message_bus/ingress/IIngressPort.h"
+#include "utils/logs/logging.h"
+
+using softbus::hardware::drivers::serial::SerialQtDriver;
+
+namespace softbus::devices::physical
+{
+
+SerialDevice::SerialDevice(std::unique_ptr driver,
+ const Config& config,
+ const softbus::devices::DeviceInfo& info)
+ : m_driver(std::move(driver))
+ , m_config(config)
+ , m_info(info)
+{
+}
+
+SerialDevice::~SerialDevice()
+{
+ stop();
+}
+
+bool SerialDevice::start()
+{
+ if (!m_driver) {
+ LOG_ERROR() << "SerialDevice: start called with null driver";
+ return false;
+ }
+
+ m_driver->SetReadCallback([this](const std::vector& data) { this->onRead(data); });
+ m_driver->SetErrorCallback([this](const std::string& error) { this->onError(error); });
+
+ LOG_INFO() << "SerialDevice: opening endpoint" << m_config.endpoint;
+ return m_driver->Open(m_config.endpoint.toStdString());
+}
+
+void SerialDevice::stop()
+{
+ if (!m_driver) {
+ return;
+ }
+
+ LOG_INFO() << "SerialDevice: closing endpoint" << m_config.endpoint;
+ m_driver->Close();
+}
+
+softbus::devices::DeviceInfo SerialDevice::deviceInfo() const
+{
+ return m_info;
+}
+
+QString SerialDevice::endpoint() const
+{
+ return m_config.endpoint;
+}
+
+softbus::devices::DeviceKind SerialDevice::kind() const
+{
+ return softbus::devices::DeviceKind::Serial;
+}
+
+void SerialDevice::bindIngress(softbus::message_bus::ingress::IIngressPort* ingress,
+ std::shared_ptr pool)
+{
+ m_ingress = ingress;
+ m_pool = std::move(pool);
+}
+
+// 串口写入数据回调 不定长数据
+bool SerialDevice::writePayload(const std::vector& data)
+{
+ if (!m_driver) {
+ return false;
+ }
+ return m_driver->Write(data);
+}
+// 串口写入数据回调 定长数据
+bool SerialDevice::writePayload(const std::uint8_t* data, std::size_t size)
+{
+ if (!m_driver) {
+ return false;
+ }
+ return m_driver->Write(data, size);
+}
+
+// 串口读取数据回调
+
+void SerialDevice::onRead(const std::vector& data)
+{
+ if (!m_ingress || !m_pool || data.empty()) {
+ return;
+ }
+
+ auto block = m_pool->allocate(data.size());
+ if (!block || !block->data || block->capacity < data.size()) {
+ m_ingress->reportError(m_config.endpoint, QStringLiteral("memory pool exhausted"));
+ return;
+ }
+
+ std::copy(data.begin(), data.end(), block->data);
+ block->size = data.size();
+ // @test code
+ LOG_INFO() << "SerialDevice: onRead data size" << data.size() << "endpoint" << m_config.endpoint;
+ // 将数据推送到 ingress 队列 包含端口名、协议、数据、时间戳、traceId
+ softbus::core::models::RawBusMessage ctx;
+ ctx.direction = softbus::core::models::BusDirection::Upstream;
+ ctx.endpointHash = softbus::core::models::makeEndpointHash(m_config.endpoint);
+ ctx.protocol = softbus::core::models::protocolTypeFromHint(m_info.protocol);
+ ctx.logicalAddress = 0;
+ ctx.routingKey = softbus::core::models::makeRoutingKey(ctx.endpointHash, m_info.id);
+ ctx.timestampMs = QDateTime::currentMSecsSinceEpoch();
+ ctx.deviceId = m_info.id;
+ ctx.payload.block = std::move(block);
+ ctx.payload.length = data.size();
+ ctx.traceId = QStringLiteral("serial:%1:%2")
+ .arg(m_config.endpoint)
+ .arg(ctx.timestampMs);
+
+ if (!m_ingress->push(std::move(ctx))) {
+ m_ingress->reportError(m_config.endpoint, QStringLiteral("ingress queue full"));
+ }
+}
+
+void SerialDevice::onError(const std::string& error)
+{
+ if (!m_ingress) {
+ return;
+ }
+ m_ingress->reportError(m_config.endpoint, QString::fromStdString(error));
+}
+
+} // namespace softbus::devices::physical
+
diff --git a/src/devices/physical/SerialDevice.h b/src/devices/physical/SerialDevice.h
new file mode 100644
index 0000000..52a1795
--- /dev/null
+++ b/src/devices/physical/SerialDevice.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "devices/DeviceTypes.h"
+#include "devices/base/IPhysicalDevice.h"
+namespace softbus::hardware::drivers::serial { class SerialQtDriver; }
+namespace softbus::message_bus::ingress { class IIngressPort; }
+namespace softbus::core::memory { class MemoryPool; }
+
+namespace softbus::devices::physical
+{
+
+class SerialDevice : public base::IPhysicalDevice
+{
+public:
+ struct Config
+ {
+ QString endpoint;
+ int baudRate;
+ int dataBits;
+ QString parity;
+ int stopBits;
+ QString flowControl;
+ };
+
+ SerialDevice(std::unique_ptr driver,
+ const Config& config,
+ const softbus::devices::DeviceInfo& info);
+
+ ~SerialDevice() override;
+
+ bool start() override;
+ void stop() override;
+
+ softbus::devices::DeviceInfo deviceInfo() const override;
+
+ QString endpoint() const;
+ softbus::devices::DeviceKind kind() const;
+ void bindIngress(softbus::message_bus::ingress::IIngressPort* ingress,
+ std::shared_ptr pool);
+ bool writePayload(const std::vector& data);
+ bool writePayload(const std::uint8_t* data, std::size_t size);
+
+private:
+ void onRead(const std::vector& data);
+ void onError(const std::string& error);
+
+private:
+ std::unique_ptr m_driver;
+ Config m_config;
+ softbus::devices::DeviceInfo m_info;
+ softbus::message_bus::ingress::IIngressPort* m_ingress{nullptr};
+ std::shared_ptr m_pool;
+};
+
+} // namespace softbus::devices::physical
+
diff --git a/src/fake_driver.cpp b/src/fake_driver.cpp
new file mode 100644
index 0000000..b4d850b
--- /dev/null
+++ b/src/fake_driver.cpp
@@ -0,0 +1,20 @@
+#include "fake_driver.hpp"
+
+FakeDriver::FakeDriver(QObject* parent)
+ : QObject(parent)
+{
+ connect(&m_timer, &QTimer::timeout, this, &FakeDriver::onTimeout);
+}
+
+void FakeDriver::start(int intervalMs, std::function onTick)
+{
+ m_onTick = std::move(onTick);
+ m_timer.start(intervalMs);
+}
+
+void FakeDriver::onTimeout()
+{
+ if (m_onTick) {
+ m_onTick();
+ }
+}
diff --git a/src/fake_driver.hpp b/src/fake_driver.hpp
new file mode 100644
index 0000000..06420d5
--- /dev/null
+++ b/src/fake_driver.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include
+
+#include
+#include
+
+class FakeDriver final : public QObject
+{
+ Q_OBJECT
+public:
+ explicit FakeDriver(QObject* parent = nullptr);
+
+ void start(int intervalMs, std::function onTick);
+
+private slots:
+ void onTimeout();
+
+private:
+ QTimer m_timer;
+ std::function m_onTick;
+};
diff --git a/src/hardware/drivers/serial/SerialCapabilities.h b/src/hardware/drivers/serial/SerialCapabilities.h
new file mode 100644
index 0000000..dd50eab
--- /dev/null
+++ b/src/hardware/drivers/serial/SerialCapabilities.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include
+#include
+
+namespace softbus::hardware::drivers::serial::capabilities
+{
+
+struct SerialDefaults
+{
+ int baudRate = 9600;
+ int dataBits = 8;
+ const char* parity = "none";
+ int stopBits = 1;
+ const char* flowControl = "none";
+};
+
+inline constexpr SerialDefaults kDefaults{};
+
+inline constexpr std::array kBaudRates{9600, 19200, 38400, 57600, 115200};
+inline constexpr std::array kDataBits{5, 6, 7, 8};
+inline constexpr std::array kParity{"none", "even", "odd"};
+inline constexpr std::array kStopBits{1, 2};
+inline constexpr std::array kFlowControl{"none", "hardware", "software"};
+
+inline bool isValidBaud(int v)
+{
+ for (int b : kBaudRates) {
+ if (b == v) return true;
+ }
+ return false;
+}
+
+inline bool isValidDataBits(int v)
+{
+ for (int b : kDataBits) {
+ if (b == v) return true;
+ }
+ return false;
+}
+
+inline bool isValidParity(const QString& s)
+{
+ for (const char* p : kParity) {
+ if (s == QLatin1String(p)) return true;
+ }
+ return false;
+}
+
+inline bool isValidStopBits(int v)
+{
+ for (int b : kStopBits) {
+ if (b == v) return true;
+ }
+ return false;
+}
+
+inline bool isValidFlowControl(const QString& s)
+{
+ for (const char* f : kFlowControl) {
+ if (s == QLatin1String(f)) return true;
+ }
+ return false;
+}
+
+} // namespace softbus::hardware::drivers::serial::capabilities
+
diff --git a/src/hardware/drivers/serial/SerialQtDriver.cpp b/src/hardware/drivers/serial/SerialQtDriver.cpp
new file mode 100644
index 0000000..58c6e0f
--- /dev/null
+++ b/src/hardware/drivers/serial/SerialQtDriver.cpp
@@ -0,0 +1,187 @@
+#include "hardware/drivers/serial/SerialQtDriver.h"
+
+#include
+#include
+#include
+
+#include "utils/logs/logging.h"
+
+namespace softbus::hardware::drivers::serial
+{
+
+SerialQtDriver::SerialQtDriver(QObject* parent)
+ : QObject(parent),
+ m_port(this)
+{
+ connect(&m_port, &QSerialPort::readyRead, this, &SerialQtDriver::handleReadyRead);
+ connect(&m_port, &QSerialPort::errorOccurred, this, &SerialQtDriver::handleError);
+}
+
+SerialQtDriver::~SerialQtDriver()
+{
+ Close();
+}
+
+bool SerialQtDriver::Open(const std::string& endpoint)
+{
+ const QString ep = QString::fromStdString(endpoint);
+ if (QThread::currentThread() == this->thread()) {
+ return openOnOwnerThread(ep);
+ }
+
+ bool ok = false;
+ QMetaObject::invokeMethod(
+ this,
+ [this, ep, &ok]() { ok = openOnOwnerThread(ep); },
+ Qt::BlockingQueuedConnection);
+ return ok;
+}
+
+void SerialQtDriver::Close()
+{
+ if (QThread::currentThread() == this->thread()) {
+ closeOnOwnerThread();
+ return;
+ }
+ QMetaObject::invokeMethod(this, [this]() { closeOnOwnerThread(); }, Qt::BlockingQueuedConnection);
+}
+
+bool SerialQtDriver::Write(const std::vector& data)
+{
+ return Write(data.data(), data.size());
+}
+
+bool SerialQtDriver::Write(const std::uint8_t* data, std::size_t size)
+{
+ if (!data || size == 0) {
+ return true;
+ }
+
+ const QByteArray bytes(reinterpret_cast(data), static_cast(size));
+ // QSerialPort 必须在其所属线程内访问;若从外部线程调用,切回 owner 线程执行。
+ if (QThread::currentThread() == this->thread()) {
+ return writeOnOwnerThread(bytes);
+ }
+
+ bool ok = false;
+ QMetaObject::invokeMethod(
+ this,
+ [this, bytes, &ok]() { ok = writeOnOwnerThread(bytes); },
+ Qt::BlockingQueuedConnection);
+ return ok;
+}
+
+void SerialQtDriver::SetReadCallback(ReadCallback cb)
+{
+ m_readCallback = std::move(cb);
+}
+
+void SerialQtDriver::SetErrorCallback(ErrorCallback cb)
+{
+ m_errorCallback = std::move(cb);
+}
+
+void SerialQtDriver::SetBaudRate(QSerialPort::BaudRate baud)
+{
+ m_port.setBaudRate(baud);
+}
+
+void SerialQtDriver::SetDataBits(QSerialPort::DataBits bits)
+{
+ m_port.setDataBits(bits);
+}
+
+void SerialQtDriver::SetParity(QSerialPort::Parity parity)
+{
+ m_port.setParity(parity);
+}
+
+void SerialQtDriver::SetStopBits(QSerialPort::StopBits stopBits)
+{
+ m_port.setStopBits(stopBits);
+}
+
+void SerialQtDriver::SetFlowControl(QSerialPort::FlowControl flow)
+{
+ m_port.setFlowControl(flow);
+}
+
+void SerialQtDriver::handleReadyRead()
+{
+ if (!m_readCallback) {
+ const QByteArray data = m_port.readAll();
+ LOG_DEBUG() << "SerialQtDriver: dropped" << data.size() << "bytes on" << m_port.portName()
+ << "because no read callback is set";
+ return;
+ }
+
+ const QByteArray data = m_port.readAll();
+ if (data.isEmpty()) {
+ return;
+ }
+
+ std::vector buffer(reinterpret_cast(data.constData()),
+ reinterpret_cast(data.constData()) + data.size());
+ m_readCallback(buffer);
+}
+
+void SerialQtDriver::handleError(QSerialPort::SerialPortError error)
+{
+ if (error == QSerialPort::NoError) {
+ return;
+ }
+
+ const QString msg =
+ QStringLiteral("SerialQtDriver error on %1: %2").arg(m_port.portName(), m_port.errorString());
+
+ LOG_WARNING() << msg;
+
+ if (m_errorCallback) {
+ m_errorCallback(msg.toStdString());
+ }
+}
+
+bool SerialQtDriver::writeOnOwnerThread(const QByteArray& bytes)
+{
+ if (!m_port.isOpen()) {
+ return false;
+ }
+ const qint64 written = m_port.write(bytes);
+ if (written != bytes.size()) {
+ LOG_WARNING() << "SerialQtDriver: partial write on" << m_port.portName() << "written" << written << "of"
+ << bytes.size();
+ return false;
+ }
+ return true;
+}
+
+bool SerialQtDriver::openOnOwnerThread(const QString& endpoint)
+{
+ if (m_port.isOpen()) {
+ return true;
+ }
+
+ m_port.setPortName(endpoint);
+ if (!m_port.open(QIODevice::ReadWrite)) {
+ if (m_errorCallback) {
+ m_errorCallback(m_port.errorString().toStdString());
+ }
+ LOG_WARNING() << "SerialQtDriver: failed to open port" << m_port.portName() << ":" << m_port.errorString();
+ return false;
+ }
+
+ LOG_DEBUG() << "SerialQtDriver: opened port" << m_port.portName();
+ return true;
+}
+
+void SerialQtDriver::closeOnOwnerThread()
+{
+ if (m_port.isOpen()) {
+ const QString name = m_port.portName();
+ m_port.close();
+ LOG_DEBUG() << "SerialQtDriver: closed port" << name;
+ }
+}
+
+} // namespace softbus::hardware::drivers::serial
+
diff --git a/src/hardware/drivers/serial/SerialQtDriver.h b/src/hardware/drivers/serial/SerialQtDriver.h
new file mode 100644
index 0000000..8db4c07
--- /dev/null
+++ b/src/hardware/drivers/serial/SerialQtDriver.h
@@ -0,0 +1,93 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "hardware/interfaces/IStreamDriver.h"
+
+// 这里的串口的唯一标识是串口的端口名,如 /dev/ttyUSB0, can0, tcp://...
+// 串口的endpoint是串口的端口名,如 /dev/ttyUSB0, can0, tcp://...
+// 使用说明:
+// 1. 创建一个SerialQtDriver对象
+// 2. 调用Open方法打开串口
+// 3. 调用Write方法写数据
+// 4. 调用SetReadCallback方法设置读回调
+// 5. 调用SetErrorCallback方法设置错误回调
+// 6. 调用Close方法关闭串口
+// 7. 调用SetBaudRate方法设置波特率
+// 8. 调用SetDataBits方法设置数据位
+// 9. 调用SetParity方法设置校验位
+// 10. 调用SetStopBits方法设置停止位
+// 11. 调用SetFlowControl方法设置流控制
+// eg:
+// SerialQtDriver driver;
+// driver.Open("/dev/ttyUSB0");
+// driver.Write({0x01, 0x02, 0x03});
+// driver.SetReadCallback([](const std::vector& data) {
+// std::cout << "read data: " << data << std::endl;
+// });
+// driver.SetErrorCallback([](const std::string& error) {
+// std::cout << "error: " << error << std::endl;
+// });
+// driver.Close();
+// 12. 调用SetBaudRate方法设置波特率
+// 13. 调用SetDataBits方法设置数据位
+// 14. 调用SetParity方法设置校验位
+// 15. 调用SetStopBits方法设置停止位
+// 16. 调用SetFlowControl方法设置流控制
+// eg:
+// driver.SetBaudRate(QSerialPort::Baud115200);
+// driver.SetDataBits(QSerialPort::Data8);
+// driver.SetParity(QSerialPort::NoParity);
+// driver.SetStopBits(QSerialPort::OneStop);
+// driver.SetFlowControl(QSerialPort::NoFlowControl);
+// driver.Close();
+
+namespace softbus::hardware::drivers::serial
+{
+
+// 基于 Qt 的串口驱动实现:只负责串口 open/read/write,不包含设备注册、数据库或协议逻辑
+class SerialQtDriver : public QObject, public softbus::hardware::interfaces::IStreamDriver
+{
+ Q_OBJECT
+
+public:
+ explicit SerialQtDriver(QObject* parent = nullptr);
+ ~SerialQtDriver() override;
+
+ bool Open(const std::string& endpoint) override;
+ void Close() override;
+
+ bool Write(const std::vector& data) override;
+ bool Write(const std::uint8_t* data, std::size_t size) override;
+
+ void SetReadCallback(ReadCallback cb) override;
+ void SetErrorCallback(ErrorCallback cb) override;
+
+ // 允许上层配置串口参数(最小集合)
+ void SetBaudRate(QSerialPort::BaudRate baud);
+ void SetDataBits(QSerialPort::DataBits bits);
+ void SetParity(QSerialPort::Parity parity);
+ void SetStopBits(QSerialPort::StopBits stopBits);
+ void SetFlowControl(QSerialPort::FlowControl flow);
+
+private slots:
+ void handleReadyRead();
+ void handleError(QSerialPort::SerialPortError error);
+
+private:
+ bool openOnOwnerThread(const QString& endpoint);
+ void closeOnOwnerThread();
+ bool writeOnOwnerThread(const QByteArray& bytes);
+
+private:
+ QSerialPort m_port;
+ ReadCallback m_readCallback;
+ ErrorCallback m_errorCallback;
+};
+
+} // namespace softbus::hardware::drivers::serial
+
diff --git a/src/hardware/interfaces/IDeviceWatcher.h b/src/hardware/interfaces/IDeviceWatcher.h
new file mode 100644
index 0000000..978759a
--- /dev/null
+++ b/src/hardware/interfaces/IDeviceWatcher.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#ifdef __cplusplus
+
+#include
+#include
+
+namespace softbus::hardware::interfaces
+{
+
+struct DiscoveredDevice
+{
+ std::string id; // 稳定 ID(如序列号、路径)
+ std::string endpoint; // 打开所需的 endpoint(如 /dev/ttyUSB0, can0, tcp://...)
+ std::string type; // 设备类型:serial / can / ethernet / usb 等
+};
+
+// 设备发现 / 监控接口:负责发现插拔事件,不负责创建设备或驱动实例
+class IDeviceWatcher
+{
+public:
+ using DeviceAttachedCallback = std::function;
+ using DeviceDetachedCallback = std::function;
+
+ virtual ~IDeviceWatcher() = default;
+
+ virtual bool Start() = 0;
+ virtual void Stop() = 0;
+
+ virtual void SetDeviceAttachedCallback(DeviceAttachedCallback cb) = 0;
+ virtual void SetDeviceDetachedCallback(DeviceDetachedCallback cb) = 0;
+};
+
+} // namespace softbus::hardware::interfaces
+
+#endif // __cplusplus
+
diff --git a/src/hardware/interfaces/IFrameDriver.h b/src/hardware/interfaces/IFrameDriver.h
new file mode 100644
index 0000000..91c28ec
--- /dev/null
+++ b/src/hardware/interfaces/IFrameDriver.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#ifdef __cplusplus
+
+#include
+#include
+#include
+#include
+
+namespace softbus::hardware::interfaces
+{
+
+struct Frame
+{
+ std::vector payload;
+ std::uint32_t id{0}; // 如 CAN ID
+ std::uint64_t timestamp{0};
+};
+
+// 面向帧设备(如 CAN 总线)的统一驱动接口
+class IFrameDriver
+{
+public:
+ using FrameCallback = std::function;
+ using ErrorCallback = std::function;
+
+ virtual ~IFrameDriver() = default;
+
+ virtual bool Open(const std::string& endpoint) = 0;
+ virtual void Close() = 0;
+
+ virtual bool WriteFrame(const Frame& frame) = 0;
+
+ virtual void SetFrameCallback(FrameCallback cb) = 0;
+ virtual void SetErrorCallback(ErrorCallback cb) = 0;
+};
+
+} // namespace softbus::hardware::interfaces
+
+#endif // __cplusplus
+
diff --git a/src/hardware/interfaces/IStreamDriver.h b/src/hardware/interfaces/IStreamDriver.h
new file mode 100644
index 0000000..1363783
--- /dev/null
+++ b/src/hardware/interfaces/IStreamDriver.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#ifdef __cplusplus
+
+#include
+#include
+#include
+#include
+
+namespace softbus::hardware::interfaces
+{
+
+// 面向字节流设备的统一驱动接口:串口 / TCP / UDP 等
+class IStreamDriver
+{
+public:
+ using ReadCallback = std::function&)>;
+ using ErrorCallback = std::function;
+
+ virtual ~IStreamDriver() = default;
+
+ virtual bool Open(const std::string& endpoint) = 0;
+ virtual void Close() = 0;
+
+ virtual bool Write(const std::vector& data) = 0;
+ virtual bool Write(const std::uint8_t* data, std::size_t size) = 0;
+
+ virtual void SetReadCallback(ReadCallback cb) = 0;
+ virtual void SetErrorCallback(ErrorCallback cb) = 0;
+};
+
+} // namespace softbus::hardware::interfaces
+
+#endif // __cplusplus
+
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..31ca86b
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,213 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "device_bus/EdgeDeviceBus.h"
+#include "device_bus/monitor/EdgeDiscoveryCoordinator.h"
+#include "message_bus/ingress/EdgeSession.h"
+#include "message_bus/ingress/EdgeTcpUplinkPort.h"
+#include "message_bus/ingress/EdgeUplinkWire.h"
+
+#include "fake_driver.hpp"
+#include "ops_ui_http.hpp"
+
+/** 无配置文件或与核心同目录运行时使用的默认值(可被 manifest 覆盖)。 */
+static QJsonObject defaultManifest()
+{
+ QJsonObject m;
+ m.insert(QStringLiteral("coreHost"), QStringLiteral("127.0.0.1"));
+ m.insert(QStringLiteral("corePort"), 18765);
+ m.insert(QStringLiteral("edgeId"), QStringLiteral("edge-dev-1"));
+ m.insert(QStringLiteral("authToken"), QStringLiteral(""));
+ m.insert(QStringLiteral("opsListenPort"), 0);
+ m.insert(QStringLiteral("localTreePath"), QStringLiteral("/tmp/edge_local_tree.json"));
+ m.insert(QStringLiteral("fakeIntervalMs"), 5000);
+ m.insert(QStringLiteral("fakeDeviceId"), 1);
+ m.insert(QStringLiteral("endpoint"), QStringLiteral("edge:fake"));
+ m.insert(QStringLiteral("protocol"), 0);
+ m.insert(QStringLiteral("edgeDiscoveryEnabled"), true);
+ return m;
+}
+
+/** 读取 manifest;缺失或非法时返回 defaultManifest(),并打日志。 */
+static QJsonObject loadManifestOrDefaults(const QString& path)
+{
+ QJsonObject out = defaultManifest();
+ QFile f(path);
+ if (!f.open(QIODevice::ReadOnly)) {
+ qWarning() << "edge manifest not found or unreadable, using defaults:" << path;
+ return out;
+ }
+ QJsonParseError pe;
+ const QJsonDocument d = QJsonDocument::fromJson(f.readAll(), &pe);
+ if (pe.error != QJsonParseError::NoError || !d.isObject()) {
+ qWarning() << "edge manifest JSON invalid, using defaults:" << pe.errorString() << "path" << path;
+ return out;
+ }
+ const QJsonObject fromFile = d.object();
+ for (auto it = fromFile.constBegin(); it != fromFile.constEnd(); ++it) {
+ out.insert(it.key(), it.value());
+ }
+ return out;
+}
+
+static void tryReloadTreeFromDisk(softbus::device_bus::EdgeDeviceBus& bus, const QString& localTreePath)
+{
+ QString err;
+ if (bus.loadLocalEdgeTreeFile(localTreePath, &err)) {
+ bus.reconcileSerialFromTree();
+ bus.reconcileCanFromTree();
+ qInfo() << "EdgeDeviceBus: loaded tree from" << localTreePath;
+ } else if (!err.isEmpty()) {
+ qWarning() << "EdgeDeviceBus: load failed" << err;
+ }
+}
+
+int main(int argc, char* argv[])
+{
+ QCoreApplication app(argc, argv);
+ const QString manifestPath = argc > 1 ? QString::fromLocal8Bit(argv[1])
+ : QStringLiteral("config/edge_manifest.json");
+ const QJsonObject m = loadManifestOrDefaults(manifestPath);
+
+ const QString coreHost = m.value(QStringLiteral("coreHost")).toString(QStringLiteral("127.0.0.1"));
+ const int corePort = m.value(QStringLiteral("corePort")).toInt(18765);
+ const QString edgeId = m.value(QStringLiteral("edgeId")).toString(QStringLiteral("edge-dev-1"));
+ const QString authToken = m.value(QStringLiteral("authToken")).toString();
+ const int opsPort = m.value(QStringLiteral("opsListenPort")).toInt(0);
+ const QString localTreePath = m.value(QStringLiteral("localTreePath")).toString(QStringLiteral("/tmp/edge_local_tree.json"));
+ const int fakeIntervalMs = m.value(QStringLiteral("fakeIntervalMs")).toInt(5000);
+ const int deviceId = m.value(QStringLiteral("fakeDeviceId")).toInt(1);
+ const QString endpoint = m.value(QStringLiteral("endpoint")).toString(QStringLiteral("edge:fake"));
+ const int protocol = m.value(QStringLiteral("protocol")).toInt(0);
+ const bool edgeDiscoveryEnabled = m.value(QStringLiteral("edgeDiscoveryEnabled")).toBool(true);
+
+ softbus::device_bus::EdgeDeviceBus edgeBus;
+ softbus::message_bus::ingress::EdgeSession session;
+
+ qint64 coreTreeRevision = 0;
+ softbus_edge::EdgeDiscoveryCoordinator discovery;
+ discovery.setEdgeSession(&session);
+ discovery.setEdgeId(edgeId);
+ discovery.setCoreTreeRevisionGetter([&coreTreeRevision]() { return coreTreeRevision; });
+
+ auto tcpUplink = std::make_unique(
+ [&session](const QByteArray& fr) { return session.writeSbupFrame(fr); });
+ edgeBus.setIngressPort(tcpUplink.get());
+
+ if (QFile::exists(localTreePath)) {
+ tryReloadTreeFromDisk(edgeBus, localTreePath);
+ }
+
+ QObject::connect(&session, &softbus::message_bus::ingress::EdgeSession::receivedTreeResult, &app,
+ [&](const QJsonObject& o) {
+ qInfo() << "TREE_RESULT" << o;
+ if (o.value(QStringLiteral("ok")).toBool()) {
+ if (o.contains(QStringLiteral("newTreeRevision"))) {
+ coreTreeRevision =
+ o.value(QStringLiteral("newTreeRevision")).toVariant().toLongLong();
+ } else if (o.contains(QStringLiteral("currentTreeRevision"))) {
+ coreTreeRevision =
+ o.value(QStringLiteral("currentTreeRevision")).toVariant().toLongLong();
+ }
+ } else if (o.value(QStringLiteral("code")).toString() == QStringLiteral("CONFLICT")) {
+ if (o.contains(QStringLiteral("currentTreeRevision"))) {
+ coreTreeRevision =
+ o.value(QStringLiteral("currentTreeRevision")).toVariant().toLongLong();
+ }
+ }
+ discovery.onTreeResultFromCore(o);
+ });
+ QObject::connect(&session, &softbus::message_bus::ingress::EdgeSession::receivedTreeFull, &app,
+ [&](const QJsonObject& envelope) {
+ QJsonObject doc;
+ doc.insert(QStringLiteral("treeRevision"), envelope.value(QStringLiteral("treeRevision")));
+ doc.insert(QStringLiteral("edgeId"), envelope.value(QStringLiteral("edgeId")));
+ doc.insert(QStringLiteral("tree"), envelope.value(QStringLiteral("tree")));
+ QSaveFile out(localTreePath);
+ if (!out.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+ qWarning() << "TREE_FULL: cannot write" << localTreePath;
+ return;
+ }
+ out.write(QJsonDocument(doc).toJson(QJsonDocument::Indented));
+ if (!out.commit()) {
+ qWarning() << "TREE_FULL: commit failed" << localTreePath;
+ return;
+ }
+ qInfo() << "TREE_FULL applied revision"
+ << doc.value(QStringLiteral("treeRevision")).toVariant().toLongLong() << "nodes"
+ << doc.value(QStringLiteral("tree")).toArray().size() << "->" << localTreePath;
+ tryReloadTreeFromDisk(edgeBus, localTreePath);
+ });
+
+ if (!session.connectToCore(coreHost, corePort)) {
+ qWarning() << "cannot reach core; start softbus_daemon with edgeIngress.enabled and matching port,"
+ << "or set coreHost/corePort in edge_manifest.json";
+ return 3;
+ }
+ if (!session.sendHello(edgeId, authToken, 0)) {
+ qWarning() << "hello send failed";
+ return 4;
+ }
+
+ if (edgeDiscoveryEnabled) {
+ (void)discovery.start();
+ }
+
+ OpsUiHttp ops;
+ if (opsPort > 0) {
+ if (!ops.start(static_cast(opsPort),
+ localTreePath,
+ edgeId,
+ [&coreTreeRevision]() { return coreTreeRevision; },
+ [&](const QJsonObject& body) {
+ if (!session.sendTreePropose(body)) {
+ qWarning() << "propose send failed";
+ }
+ },
+ [&discovery]() { return discovery.recentDiscoveriesJson(); })) {
+ qWarning() << "ops ui listen failed";
+ } else {
+ qInfo() << "ops UI http://127.0.0.1:" << opsPort
+ << " GET / GET /ui GET /discovered POST /propose POST /register_discovered";
+ }
+ }
+
+ FakeDriver fake;
+ if (fakeIntervalMs > 0) {
+ fake.start(fakeIntervalMs, [&]() {
+ const std::uint32_t epHash = static_cast(qHash(endpoint));
+ const std::uint64_t routing = (static_cast(epHash) << 32)
+ | static_cast(deviceId);
+ const QByteArray inner = softbus::message_bus::ingress::edge_uplink::encodeDataRawBusPayload(
+ deviceId,
+ epHash,
+ static_cast(protocol),
+ 0,
+ routing,
+ 0,
+ QDateTime::currentMSecsSinceEpoch(),
+ 1,
+ QStringLiteral("edge:fake"),
+ QByteArray(1, '\x01'));
+ if (inner.isEmpty()) {
+ qWarning() << "encode data failed";
+ return;
+ }
+ if (!session.sendDataRawBusInner(inner)) {
+ qWarning() << "send data failed";
+ }
+ });
+ }
+
+ QObject::connect(&app, &QCoreApplication::aboutToQuit, &discovery, [&discovery]() { discovery.stop(); });
+
+ (void)tcpUplink;
+ return app.exec();
+}
diff --git a/src/message_bus/ingress/EdgeSession.cpp b/src/message_bus/ingress/EdgeSession.cpp
new file mode 100644
index 0000000..e14dc04
--- /dev/null
+++ b/src/message_bus/ingress/EdgeSession.cpp
@@ -0,0 +1,91 @@
+#include "message_bus/ingress/EdgeSession.h"
+
+#include
+#include
+
+#include "message_bus/ingress/EdgeUplinkWire.h"
+
+using softbus::message_bus::ingress::edge_uplink::FrameKind;
+using softbus::message_bus::ingress::edge_uplink::encodeOuterFrame;
+using softbus::message_bus::ingress::edge_uplink::tryPopOneOuterFrame;
+
+namespace softbus::message_bus::ingress
+{
+
+EdgeSession::EdgeSession(QObject* parent)
+ : QObject(parent)
+{
+ connect(&m_sock, &QTcpSocket::readyRead, this, &EdgeSession::onSocketReadyRead);
+}
+
+bool EdgeSession::connectToCore(const QString& host, int port)
+{
+ m_sock.connectToHost(host, static_cast(port));
+ if (!m_sock.waitForConnected(5000)) {
+ qWarning() << "EdgeSession: TCP connect failed" << host << port << m_sock.errorString();
+ return false;
+ }
+ return true;
+}
+
+void EdgeSession::disconnectCore()
+{
+ m_sock.disconnectFromHost();
+}
+
+bool EdgeSession::sendHello(const QString& edgeId, const QString& authToken, qint64 currentTreeRevision)
+{
+ QJsonObject o;
+ o.insert(QStringLiteral("edgeId"), edgeId);
+ if (!authToken.isEmpty()) {
+ o.insert(QStringLiteral("authToken"), authToken);
+ }
+ o.insert(QStringLiteral("currentTreeRevision"), currentTreeRevision);
+ const QByteArray pl = QJsonDocument(o).toJson(QJsonDocument::Compact);
+ const QByteArray frame = encodeOuterFrame(FrameKind::Hello, pl);
+ return m_sock.write(frame) == frame.size();
+}
+
+bool EdgeSession::sendDataRawBusInner(const QByteArray& innerPayload)
+{
+ const QByteArray frame = encodeOuterFrame(FrameKind::DataRawBus, innerPayload);
+ return m_sock.write(frame) == frame.size();
+}
+
+bool EdgeSession::sendTreePropose(const QJsonObject& body)
+{
+ const QByteArray pl = QJsonDocument(body).toJson(QJsonDocument::Compact);
+ const QByteArray frame = encodeOuterFrame(FrameKind::TreePropose, pl);
+ return m_sock.write(frame) == frame.size();
+}
+
+bool EdgeSession::writeSbupFrame(const QByteArray& frame)
+{
+ return m_sock.write(frame) == frame.size();
+}
+
+void EdgeSession::onSocketReadyRead()
+{
+ m_inbuf.append(m_sock.readAll());
+ while (true) {
+ const auto popped = tryPopOneOuterFrame(m_inbuf, m_maxPayload);
+ if (!popped.has_value()) {
+ break;
+ }
+ if (popped->kind == FrameKind::TreeResult) {
+ QJsonParseError pe;
+ const QJsonDocument doc = QJsonDocument::fromJson(popped->payload, &pe);
+ if (pe.error == QJsonParseError::NoError && doc.isObject()) {
+ emit receivedTreeResult(doc.object());
+ }
+ } else if (popped->kind == FrameKind::TreeFull) {
+ QJsonParseError pe;
+ const QJsonDocument doc = QJsonDocument::fromJson(popped->payload, &pe);
+ if (pe.error == QJsonParseError::NoError && doc.isObject()) {
+ emit receivedTreeFull(doc.object());
+ }
+ }
+ }
+}
+
+} // namespace softbus::message_bus::ingress
diff --git a/src/message_bus/ingress/EdgeSession.h b/src/message_bus/ingress/EdgeSession.h
new file mode 100644
index 0000000..8e6793b
--- /dev/null
+++ b/src/message_bus/ingress/EdgeSession.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include
+#include