This commit is contained in:
flower_linux
2026-05-13 16:46:07 +08:00
commit 90ad86b4d0
52 changed files with 5251 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build
/.cache

89
CMakeLists.txt Normal file
View File

@@ -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()

308
CMakeLists.txt.user Normal file
View File

@@ -0,0 +1,308 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by QtCreator 17.0.1, 2026-05-13T14:08:44. -->
<qtcreator>
<data>
<variable>EnvironmentId</variable>
<value type="QByteArray">{a7c4c772-b559-4291-a2ea-267a2e71edb0}</value>
</data>
<data>
<variable>ProjectExplorer.Project.ActiveTarget</variable>
<value type="qlonglong">0</value>
</data>
<data>
<variable>ProjectExplorer.Project.EditorSettings</variable>
<valuemap type="QVariantMap">
<value type="bool" key="EditorConfiguration.AutoDetect">true</value>
<value type="bool" key="EditorConfiguration.AutoIndent">true</value>
<value type="bool" key="EditorConfiguration.CamelCaseNavigation">true</value>
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.0">
<value type="QString" key="language">Cpp</value>
<valuemap type="QVariantMap" key="value">
<value type="QByteArray" key="CurrentPreferences">CppGlobal</value>
</valuemap>
</valuemap>
<valuemap type="QVariantMap" key="EditorConfiguration.CodeStyle.1">
<value type="QString" key="language">QmlJS</value>
<valuemap type="QVariantMap" key="value">
<value type="QByteArray" key="CurrentPreferences">QmlJSGlobal</value>
</valuemap>
</valuemap>
<value type="qlonglong" key="EditorConfiguration.CodeStyle.Count">2</value>
<value type="QByteArray" key="EditorConfiguration.Codec">UTF-8</value>
<value type="bool" key="EditorConfiguration.ConstrainTooltips">false</value>
<value type="int" key="EditorConfiguration.IndentSize">4</value>
<value type="bool" key="EditorConfiguration.KeyboardTooltips">false</value>
<value type="int" key="EditorConfiguration.LineEndingBehavior">0</value>
<value type="int" key="EditorConfiguration.MarginColumn">80</value>
<value type="bool" key="EditorConfiguration.MouseHiding">true</value>
<value type="bool" key="EditorConfiguration.MouseNavigation">true</value>
<value type="int" key="EditorConfiguration.PaddingMode">1</value>
<value type="int" key="EditorConfiguration.PreferAfterWhitespaceComments">0</value>
<value type="bool" key="EditorConfiguration.PreferSingleLineComments">false</value>
<value type="bool" key="EditorConfiguration.ScrollWheelZooming">true</value>
<value type="bool" key="EditorConfiguration.ShowMargin">false</value>
<value type="int" key="EditorConfiguration.SmartBackspaceBehavior">2</value>
<value type="bool" key="EditorConfiguration.SmartSelectionChanging">true</value>
<value type="bool" key="EditorConfiguration.SpacesForTabs">true</value>
<value type="int" key="EditorConfiguration.TabKeyBehavior">0</value>
<value type="int" key="EditorConfiguration.TabSize">8</value>
<value type="bool" key="EditorConfiguration.UseGlobal">true</value>
<value type="bool" key="EditorConfiguration.UseIndenter">false</value>
<value type="int" key="EditorConfiguration.Utf8BomBehavior">1</value>
<value type="bool" key="EditorConfiguration.addFinalNewLine">true</value>
<value type="bool" key="EditorConfiguration.cleanIndentation">true</value>
<value type="bool" key="EditorConfiguration.cleanWhitespace">true</value>
<value type="QString" key="EditorConfiguration.ignoreFileTypes">*.md, *.MD, Makefile</value>
<value type="bool" key="EditorConfiguration.inEntireDocument">false</value>
<value type="bool" key="EditorConfiguration.skipTrailingWhitespace">true</value>
<value type="bool" key="EditorConfiguration.tintMarginArea">true</value>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.PluginSettings</variable>
<valuemap type="QVariantMap">
<valuemap type="QVariantMap" key="AutoTest.ActiveFrameworks">
<value type="bool" key="AutoTest.Framework.Boost">true</value>
<value type="bool" key="AutoTest.Framework.CTest">false</value>
<value type="bool" key="AutoTest.Framework.Catch">true</value>
<value type="bool" key="AutoTest.Framework.GTest">true</value>
<value type="bool" key="AutoTest.Framework.QtQuickTest">true</value>
<value type="bool" key="AutoTest.Framework.QtTest">true</value>
</valuemap>
<value type="bool" key="AutoTest.ApplyFilter">false</value>
<valuemap type="QVariantMap" key="AutoTest.CheckStates"/>
<valuelist type="QVariantList" key="AutoTest.PathFilters"/>
<value type="int" key="AutoTest.RunAfterBuild">0</value>
<value type="bool" key="AutoTest.UseGlobal">true</value>
<valuemap type="QVariantMap" key="ClangTools">
<value type="bool" key="ClangTools.AnalyzeOpenFiles">true</value>
<value type="bool" key="ClangTools.BuildBeforeAnalysis">true</value>
<value type="QString" key="ClangTools.DiagnosticConfig">Builtin.DefaultTidyAndClazy</value>
<value type="int" key="ClangTools.ParallelJobs">8</value>
<value type="bool" key="ClangTools.PreferConfigFile">true</value>
<valuelist type="QVariantList" key="ClangTools.SelectedDirs"/>
<valuelist type="QVariantList" key="ClangTools.SelectedFiles"/>
<valuelist type="QVariantList" key="ClangTools.SuppressedDiagnostics"/>
<value type="bool" key="ClangTools.UseGlobalSettings">true</value>
</valuemap>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.Target.0</variable>
<valuemap type="QVariantMap">
<value type="QString" key="DeviceType">Desktop</value>
<value type="bool" key="HasPerBcDcs">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">Qt 6.9.2</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Qt 6.9.2</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">{ac766883-1e6c-4832-9072-86a8b976b89e}</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveBuildConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.BuildConfiguration.0">
<value type="QString" key="CMake.Build.Type">Build</value>
<value type="int" key="CMake.Configure.BaseEnvironment">2</value>
<value type="bool" key="CMake.Configure.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMake.Configure.UserEnvironmentChanges"/>
<value type="QString" key="CMake.Initial.Parameters">-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</value>
<value type="QString" key="CMake.Source.Directory">/home/dt/myCode/bus/soft_bus/edge_agent</value>
<value type="QString" key="ProjectExplorer.BuildConfiguration.BuildDirectory">/home/dt/myCode/bus/soft_bus/edge_agent/build</value>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">all</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">构建</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">构建</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">构建</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Build</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString">clean</value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">构建</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.MakeStep</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">1</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">清除</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">清除</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Clean</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">2</value>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.CustomParsers"/>
<value type="bool" key="ProjectExplorer.BuildConfiguration.ParseStandardOutput">false</value>
<valuelist type="QVariantList" key="ProjectExplorer.BuildConfiguration.UserEnvironmentChanges"/>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">构建 (imported)</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">CMakeProjectManager.CMakeBuildConfiguration</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveDeployConfiguration">0</value>
<value type="qlonglong" key="ProjectExplorer.Target.ActiveRunConfiguration">0</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString"></value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Install Application Manager package</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName"></value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.CustomExecutableRunConfiguration</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey"></value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.BuildConfigurationCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">0</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.DefaultDeployConfiguration</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.DeployConfiguration.1">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildConfiguration.BuildStepList.0">
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.0">
<value type="QString" key="CMakeProjectManager.MakeStep.BuildPreset"></value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.BuildTargets">
<value type="QString"></value>
</valuelist>
<value type="bool" key="CMakeProjectManager.MakeStep.ClearSystemEnvironment">false</value>
<valuelist type="QVariantList" key="CMakeProjectManager.MakeStep.UserEnvironmentChanges"/>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.CMakePackageStep</value>
</valuemap>
<valuemap type="QVariantMap" key="ProjectExplorer.BuildStepList.Step.1">
<value type="QString" key="ApplicationManagerPlugin.Deploy.InstallPackageStep.Arguments">install-package --acknowledge</value>
<value type="bool" key="ProjectExplorer.BuildStep.Enabled">true</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">Install Application Manager package</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.InstallPackageStep</value>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedFiles"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedHosts"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedRemotePaths"/>
<valuelist type="QVariantList" key="ProjectExplorer.RunConfiguration.LastDeployedSysroots"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedLocalTimes"/>
<valuelist type="QVariantList" key="RemoteLinux.LastDeployedRemoteTimes"/>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.BuildStepList.StepsCount">2</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DefaultDisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName">部署</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.BuildSteps.Deploy</value>
</valuemap>
<value type="int" key="ProjectExplorer.BuildConfiguration.BuildStepListCount">1</value>
<valuemap type="QVariantMap" key="ProjectExplorer.DeployConfiguration.CustomData"/>
<value type="bool" key="ProjectExplorer.DeployConfiguration.CustomDataEnabled">false</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ApplicationManagerPlugin.Deploy.Configuration</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.DeployConfigurationCount">2</value>
<valuemap type="QVariantMap" key="ProjectExplorer.Target.RunConfiguration.0">
<value type="bool" key="Analyzer.Perf.Settings.UseGlobalSettings">true</value>
<value type="bool" key="Analyzer.QmlProfiler.Settings.UseGlobalSettings">true</value>
<value type="int" key="Analyzer.Valgrind.Callgrind.CostFormat">0</value>
<value type="bool" key="Analyzer.Valgrind.Settings.UseGlobalSettings">true</value>
<valuelist type="QVariantList" key="CustomOutputParsers"/>
<value type="int" key="PE.EnvironmentAspect.Base">2</value>
<valuelist type="QVariantList" key="PE.EnvironmentAspect.Changes"/>
<value type="bool" key="PE.EnvironmentAspect.PrintOnRun">false</value>
<value type="QString" key="PerfRecordArgsId">-e cpu-cycles --call-graph dwarf,4096 -F 250</value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.DisplayName"></value>
<value type="QString" key="ProjectExplorer.ProjectConfiguration.Id">ProjectExplorer.CustomExecutableRunConfiguration</value>
<value type="QString" key="ProjectExplorer.RunConfiguration.BuildKey"></value>
<value type="bool" key="ProjectExplorer.RunConfiguration.Customized">false</value>
<value type="bool" key="RunConfiguration.UseCppDebuggerAuto">true</value>
<value type="bool" key="RunConfiguration.UseQmlDebuggerAuto">true</value>
</valuemap>
<value type="qlonglong" key="ProjectExplorer.Target.RunConfigurationCount">1</value>
</valuemap>
</data>
<data>
<variable>ProjectExplorer.Project.TargetCount</variable>
<value type="qlonglong">1</value>
</data>
<data>
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>
<value type="int">22</value>
</data>
<data>
<variable>Version</variable>
<value type="int">22</value>
</data>
</qtcreator>

85
README.md Normal file
View File

@@ -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/<edgeId>/discovered/<stableKey>`)、`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/<edgeId>/discovered/<stableKey>`remove 时 **`patch`** `params.online: false`
3. 核心 `applyEdgePropose` 成功后再次 **`TREE_FULL`**;边缘 reload 树文件后对 **Serial/CAN** 执行 reconcile**raw 仅上行**。

View File

@@ -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
}

View File

@@ -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_PROPOSEupsert/patch需 Linux libudev"
}
}
}

View File

@@ -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` 行为对齐的裁剪版)。

View File

@@ -0,0 +1,98 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <queue>
#include <vector>
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<PoolBlock>;
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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::uint8_t> m_storage;
mutable std::mutex m_mutex;
std::queue<std::size_t> m_freeSlots;
std::vector<bool> m_inUse;
};
} // namespace softbus::core::memory

View File

@@ -0,0 +1,59 @@
#pragma once
#include <cstdint>
#include <QString>
#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<std::uint64_t>(endpointHash) << 32) | static_cast<std::uint32_t>(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

View File

@@ -0,0 +1,35 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <QMetaType>
#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)

View File

@@ -0,0 +1,80 @@
#pragma once
#include <cstdint>
#include <QMetaType>
#include <QString>
#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<std::uint16_t>(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)

View File

@@ -0,0 +1,222 @@
#include "device_bus/EdgeDeviceBus.h"
#include <QFile>
#include <QIODevice>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSet>
#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<DeviceTreeModel>())
, m_pool(std::make_shared<softbus::core::memory::MemoryPool>())
{
}
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<QString> 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<QString> 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

View File

@@ -0,0 +1,61 @@
#pragma once
#include <memory>
#include <QHash>
#include <QSharedPointer>
#include <QString>
#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<softbus::core::memory::MemoryPool> memoryPool() const { return m_pool; }
private:
void shutdownAllSerial();
#if SOFTBUS_HAVE_CAN
void shutdownAllCan();
#endif
std::shared_ptr<DeviceTreeModel> m_tree;
std::shared_ptr<softbus::core::memory::MemoryPool> m_pool;
softbus::message_bus::ingress::IIngressPort* m_ingress{nullptr};
QHash<QString, QSharedPointer<softbus::devices::physical::SerialDevice>> m_serialByEndpoint;
#if SOFTBUS_HAVE_CAN
QHash<QString, QSharedPointer<softbus::devices::physical::CanDevice>> m_canByEndpoint;
#endif
};
} // namespace softbus::device_bus

View File

@@ -0,0 +1,105 @@
#include "device_bus/manager/CanDeviceManager.h"
#include <memory>
#include <QCanBus>
#include <QCanBusDevice>
#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<void()>& 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<int>(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<QCanBusDevice> 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<softbus::devices::physical::CanDevice> 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

View File

@@ -0,0 +1,31 @@
#pragma once
#include <functional>
#include <memory>
#include <QHash>
#include <QJsonObject>
#include <QSharedPointer>
#include <QString>
#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<void()> dirtyCb;
softbus::message_bus::ingress::IIngressPort* ingressPort{nullptr};
std::shared_ptr<softbus::core::memory::MemoryPool> memoryPool;
QHash<QString, QSharedPointer<softbus::devices::physical::CanDevice>>* canDevicesByEndpoint{nullptr};
};
bool initializeCanDeviceWithDeps(const QString& endpoint, const QJsonObject& params, const CanInitDeps& deps);
} // namespace softbus::device_bus::manager

View File

@@ -0,0 +1,152 @@
#include "device_bus/manager/SerialDeviceManager.h"
#include <QJsonDocument>
#include <QSerialPort>
#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<void()>& 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<SerialQtDriver>(nullptr);
driver->SetBaudRate(static_cast<QSerialPort::BaudRate>(baud));
driver->SetDataBits(static_cast<QSerialPort::DataBits>(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<softbus::devices::physical::SerialDevice> 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

View File

@@ -0,0 +1,32 @@
#pragma once
// 与 softbus_daemon 同源布局;边缘侧去掉 Registry / Egress / Pipeline仅保留串口 + IIngressPort。
#include <functional>
#include <memory>
#include <QHash>
#include <QJsonObject>
#include <QSharedPointer>
#include <QString>
#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<void()> dirtyCb;
softbus::message_bus::ingress::IIngressPort* ingressPort{nullptr};
std::shared_ptr<softbus::core::memory::MemoryPool> memoryPool;
QHash<QString, QSharedPointer<softbus::devices::physical::SerialDevice>>* serialDevicesByEndpoint{nullptr};
};
bool initializeSerialDeviceWithDeps(const QString& endpoint, const QJsonObject& params, const SerialInitDeps& deps);
} // namespace softbus::device_bus::manager

View File

@@ -0,0 +1,50 @@
#pragma once
#include <QDateTime>
#include <QMap>
#include <QString>
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<QString, QString> props;
};
} // namespace softbus::device_bus::monitor

View File

@@ -0,0 +1,286 @@
#include "device_bus/monitor/EdgeDiscoveryCoordinator.h"
#include <QDateTime>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMutexLocker>
#include <QTimer>
#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<qint64()> 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<softbus::device_bus::monitor::UdevDeviceMonitor>(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

View File

@@ -0,0 +1,81 @@
#pragma once
#include <functional>
#include <memory>
#include <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QMutex>
#include <QObject>
#include <QString>
#include <QVector>
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<qint64()> 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<qint64()> m_getCoreRevision;
std::unique_ptr<softbus::device_bus::monitor::UdevDeviceMonitor> m_udev;
struct PendingUpsert
{
QString stableKey;
QJsonObject node;
};
QHash<QString, PendingUpsert> m_pendingUpsert;
QVector<QJsonObject> m_pendingPatchEntries;
QHash<QString, QString> m_devnodeToStableKey;
mutable QMutex m_recentMutex;
QVector<QJsonObject> m_recent;
qint64 m_lastKnownCoreRevision{0};
QTimer* m_debounceTimer{nullptr};
bool m_proposeInFlight{false};
};
} // namespace softbus_edge

View File

@@ -0,0 +1,47 @@
#pragma once
#include <functional>
#include <vector>
#include <QObject>
#include "device_bus/monitor/DeviceEvent.h"
namespace softbus::device_bus::monitor
{
class IDeviceMonitor : public QObject
{
Q_OBJECT
public:
using EventCallback = std::function<void(const DeviceEvent&)>;
explicit IDeviceMonitor(QObject* parent = nullptr)
: QObject(parent)
{
}
~IDeviceMonitor() override = default;
virtual bool start() = 0;
virtual void stop() = 0;
virtual std::vector<DeviceEvent> 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

View File

@@ -0,0 +1,362 @@
#include "device_bus/monitor/UdevDeviceMonitor.h"
#if defined(__linux__)
#include <unistd.h>
#include <vector>
#include <QFileInfo>
#include <QRegularExpression>
#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<DeviceEvent> UdevDeviceMonitor::snapshotExisting()
{
std::vector<DeviceEvent> 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__

View File

@@ -0,0 +1,61 @@
#pragma once
#include "device_bus/monitor/IDeviceMonitor.h"
#if defined(__linux__)
#include <libudev.h>
#include <QSocketNotifier>
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<DeviceEvent> 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<DeviceEvent> snapshotExisting() override { return {}; }
};
} // namespace softbus::device_bus::monitor
#endif

View File

@@ -0,0 +1,619 @@
#include "device_bus/tree/DeviceTreeModel.h"
#include <algorithm>
#include <QJsonArray>
#include <QJsonObject>
#include <QMutexLocker>
#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<QString>& 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<QString> DeviceTreeModel::downlinkEdgeIdsForNode(const DeviceTreeNode& node) const
{
QVector<QString> 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;
}

View File

@@ -0,0 +1,89 @@
#pragma once
#include <QJsonObject>
#include <QJsonArray>
#include <QVector>
#include <QString>
#include <QHash>
#include <atomic>
#include <QMutex>
// 轻量级设备树模型:从 daemon_config.json 的 device_tree 字段加载
struct DeviceTreeNode
{
QString id;
QString name;
QString type;
QString endpoint;
QString driver;
QString capabilitiesRef;
QJsonObject params;
QVector<QString> childrenIds;
bool enabled{true};
};
class DeviceTreeModel
{
public:
// 从给定的 JSON 根对象(包含 device_tree 字段)加载
bool loadFromDaemonConfig(const QJsonObject& root, QString* errorMessage = nullptr);
const QVector<DeviceTreeNode>& nodes() const { return m_nodes; }
QVector<DeviceTreeNode>& 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/<edgeId>/...),返回应尝试下行 TREE_FULL 的 edgeId去重。 */
QVector<QString> downlinkEdgeIdsForNode(const DeviceTreeNode& node) const;
private:
static bool nodeBelongsToEdge(const DeviceTreeNode& node, const QString& edgeId);
QVector<DeviceTreeNode> m_nodes;
QHash<QString, int> m_indexById;
mutable QMutex m_writeMutex;
std::atomic<qint64> m_treeRevision{1};
};

View File

@@ -0,0 +1,66 @@
#include "devices/DeviceTypes.h"
namespace softbus::devices {
DeviceKind kindFromType(const QString &typeStr) {
// 使用静态 QHash仅在首次调用时初始化一次后续调用直接极速查表
static const QHash<QString, DeviceKind> 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

107
src/devices/DeviceTypes.h Normal file
View File

@@ -0,0 +1,107 @@
#pragma once
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>
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<int>(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

View File

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

View File

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

View File

@@ -0,0 +1,147 @@
#include "devices/physical/CanDevice.h"
#include <cstring>
#include <QCanBusDevice>
#include <QCanBusFrame>
#include <QDateTime>
#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<QCanBusDevice> 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<softbus::core::memory::MemoryPool> 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<std::size_t>(busPayload.size()));
if (!block || !block->data || block->capacity < static_cast<std::size_t>(busPayload.size())) {
m_ingress->reportError(m_config.endpoint, QStringLiteral("memory pool exhausted"));
return;
}
if (!busPayload.isEmpty()) {
std::memcpy(block->data, busPayload.constData(), static_cast<std::size_t>(busPayload.size()));
}
block->size = static_cast<std::size_t>(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<std::uint32_t>(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<std::size_t>(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

View File

@@ -0,0 +1,53 @@
#pragma once
#include <memory>
#include <QString>
#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:<interface>`,如 `can:vcan0`
int bitrate{500000};
};
CanDevice(std::unique_ptr<QCanBusDevice> 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<softbus::core::memory::MemoryPool> pool);
private:
void onFramesAvailable();
void onError();
private:
std::unique_ptr<QCanBusDevice> m_bus;
Config m_config;
softbus::devices::DeviceInfo m_info;
softbus::message_bus::ingress::IIngressPort* m_ingress{nullptr};
std::shared_ptr<softbus::core::memory::MemoryPool> m_pool;
};
} // namespace softbus::devices::physical

View File

@@ -0,0 +1,145 @@
#include "devices/physical/SerialDevice.h"
#include <algorithm>
#include <memory>
#include <string>
#include <QDateTime>
#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<SerialQtDriver> 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<std::uint8_t>& 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<softbus::core::memory::MemoryPool> pool)
{
m_ingress = ingress;
m_pool = std::move(pool);
}
// 串口写入数据回调 不定长数据
bool SerialDevice::writePayload(const std::vector<std::uint8_t>& 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<std::uint8_t>& 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

View File

@@ -0,0 +1,61 @@
#pragma once
#include <QString>
#include <cstddef>
#include <memory>
#include <vector>
#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<softbus::hardware::drivers::serial::SerialQtDriver> 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<softbus::core::memory::MemoryPool> pool);
bool writePayload(const std::vector<std::uint8_t>& data);
bool writePayload(const std::uint8_t* data, std::size_t size);
private:
void onRead(const std::vector<std::uint8_t>& data);
void onError(const std::string& error);
private:
std::unique_ptr<softbus::hardware::drivers::serial::SerialQtDriver> m_driver;
Config m_config;
softbus::devices::DeviceInfo m_info;
softbus::message_bus::ingress::IIngressPort* m_ingress{nullptr};
std::shared_ptr<softbus::core::memory::MemoryPool> m_pool;
};
} // namespace softbus::devices::physical

20
src/fake_driver.cpp Normal file
View File

@@ -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<void()> onTick)
{
m_onTick = std::move(onTick);
m_timer.start(intervalMs);
}
void FakeDriver::onTimeout()
{
if (m_onTick) {
m_onTick();
}
}

22
src/fake_driver.hpp Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <functional>
#include <QObject>
#include <QTimer>
class FakeDriver final : public QObject
{
Q_OBJECT
public:
explicit FakeDriver(QObject* parent = nullptr);
void start(int intervalMs, std::function<void()> onTick);
private slots:
void onTimeout();
private:
QTimer m_timer;
std::function<void()> m_onTick;
};

View File

@@ -0,0 +1,67 @@
#pragma once
#include <array>
#include <QString>
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<int, 5> kBaudRates{9600, 19200, 38400, 57600, 115200};
inline constexpr std::array<int, 4> kDataBits{5, 6, 7, 8};
inline constexpr std::array<const char*, 3> kParity{"none", "even", "odd"};
inline constexpr std::array<int, 2> kStopBits{1, 2};
inline constexpr std::array<const char*, 3> 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

View File

@@ -0,0 +1,187 @@
#include "hardware/drivers/serial/SerialQtDriver.h"
#include <QByteArray>
#include <QMetaObject>
#include <QThread>
#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<std::uint8_t>& 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<const char*>(data), static_cast<int>(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<std::uint8_t> buffer(reinterpret_cast<const std::uint8_t*>(data.constData()),
reinterpret_cast<const std::uint8_t*>(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

View File

@@ -0,0 +1,93 @@
#pragma once
#include <QObject>
#include <QSerialPort>
#include <QString>
#include <cstddef>
#include <QByteArray>
#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<std::uint8_t>& 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<std::uint8_t>& 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

View File

@@ -0,0 +1,37 @@
#pragma once
#ifdef __cplusplus
#include <functional>
#include <string>
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<void(const DiscoveredDevice&)>;
using DeviceDetachedCallback = std::function<void(const DiscoveredDevice&)>;
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

View File

@@ -0,0 +1,41 @@
#pragma once
#ifdef __cplusplus
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace softbus::hardware::interfaces
{
struct Frame
{
std::vector<std::uint8_t> payload;
std::uint32_t id{0}; // 如 CAN ID
std::uint64_t timestamp{0};
};
// 面向帧设备(如 CAN 总线)的统一驱动接口
class IFrameDriver
{
public:
using FrameCallback = std::function<void(const Frame&)>;
using ErrorCallback = std::function<void(const std::string& message)>;
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

View File

@@ -0,0 +1,35 @@
#pragma once
#ifdef __cplusplus
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace softbus::hardware::interfaces
{
// 面向字节流设备的统一驱动接口:串口 / TCP / UDP 等
class IStreamDriver
{
public:
using ReadCallback = std::function<void(const std::vector<std::uint8_t>&)>;
using ErrorCallback = std::function<void(const std::string& message)>;
virtual ~IStreamDriver() = default;
virtual bool Open(const std::string& endpoint) = 0;
virtual void Close() = 0;
virtual bool Write(const std::vector<std::uint8_t>& 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

213
src/main.cpp Normal file
View File

@@ -0,0 +1,213 @@
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QSaveFile>
#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<softbus::message_bus::ingress::EdgeTcpUplinkPort>(
[&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<quint16>(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<std::uint32_t>(qHash(endpoint));
const std::uint64_t routing = (static_cast<std::uint64_t>(epHash) << 32)
| static_cast<std::uint32_t>(deviceId);
const QByteArray inner = softbus::message_bus::ingress::edge_uplink::encodeDataRawBusPayload(
deviceId,
epHash,
static_cast<std::uint16_t>(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();
}

View File

@@ -0,0 +1,91 @@
#include "message_bus/ingress/EdgeSession.h"
#include <QJsonDocument>
#include <QJsonParseError>
#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<quint16>(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

View File

@@ -0,0 +1,41 @@
#pragma once
#include <QByteArray>
#include <QJsonObject>
#include <QObject>
#include <QTcpSocket>
namespace softbus::message_bus::ingress
{
class EdgeSession final : public QObject
{
Q_OBJECT
public:
explicit EdgeSession(QObject* parent = nullptr);
bool connectToCore(const QString& host, int port);
void disconnectCore();
bool sendHello(const QString& edgeId, const QString& authToken, qint64 currentTreeRevision);
bool sendDataRawBusInner(const QByteArray& innerPayload);
bool sendTreePropose(const QJsonObject& body);
/** 已带 SBUP 外层的任意帧(如完整 `DATA_RAWBUS`)。 */
bool writeSbupFrame(const QByteArray& frame);
void setMaxPayloadBytes(int m) { m_maxPayload = m; }
signals:
void receivedTreeResult(const QJsonObject& obj);
void receivedTreeFull(const QJsonObject& obj);
private slots:
void onSocketReadyRead();
private:
QTcpSocket m_sock;
QByteArray m_inbuf;
int m_maxPayload{4 * 1024 * 1024};
};
} // namespace softbus::message_bus::ingress

View File

@@ -0,0 +1,53 @@
#include "message_bus/ingress/EdgeTcpUplinkPort.h"
#include <cstring>
#include <QByteArray>
#include <QDebug>
#include "message_bus/ingress/EdgeUplinkWire.h"
namespace softbus::message_bus::ingress
{
EdgeTcpUplinkPort::EdgeTcpUplinkPort(FrameWriter writer)
: m_writer(std::move(writer))
{
}
bool EdgeTcpUplinkPort::push(softbus::core::models::RawBusMessage ctx)
{
if (!m_writer || !ctx.payload.valid()) {
return false;
}
std::uint64_t protocolHeaderRaw = 0;
std::memcpy(&protocolHeaderRaw, &ctx.header, sizeof(protocolHeaderRaw));
const auto* bytes = ctx.payload.bytes();
const QByteArray busPayload(reinterpret_cast<const char*>(bytes), static_cast<int>(ctx.payload.length));
const QByteArray inner = edge_uplink::encodeDataRawBusPayload(ctx.deviceId,
ctx.endpointHash,
static_cast<std::uint16_t>(ctx.protocol),
ctx.logicalAddress,
ctx.routingKey,
protocolHeaderRaw,
ctx.timestampMs,
static_cast<std::int8_t>(ctx.direction),
ctx.traceId.isEmpty() ? QStringLiteral("edge")
: ctx.traceId,
busPayload);
if (inner.isEmpty()) {
return false;
}
const QByteArray outer =
edge_uplink::encodeOuterFrame(edge_uplink::FrameKind::DataRawBus, inner);
return m_writer(outer);
}
void EdgeTcpUplinkPort::reportError(const QString& endpoint, const QString& error)
{
qWarning() << "EdgeTcpUplinkPort" << endpoint << error;
}
} // namespace softbus::message_bus::ingress

View File

@@ -0,0 +1,28 @@
#pragma once
#include <functional>
#include <QByteArray>
#include <QString>
#include "message_bus/ingress/IIngressPort.h"
namespace softbus::message_bus::ingress
{
/** 将 `RawBusMessage` 编码为 SBUP `DATA_RAWBUS` 帧并写入 TCP边缘 → 核心)。 */
class EdgeTcpUplinkPort final : public IIngressPort
{
public:
using FrameWriter = std::function<bool(const QByteArray& sbupFrame)>;
explicit EdgeTcpUplinkPort(FrameWriter writer);
bool push(softbus::core::models::RawBusMessage ctx) override;
void reportError(const QString& endpoint, const QString& error) override;
private:
FrameWriter m_writer;
};
} // namespace softbus::message_bus::ingress

View File

@@ -0,0 +1,178 @@
#include "message_bus/ingress/EdgeUplinkWire.h"
#include <cstring>
#include <QtEndian>
#include "utils/logs/logging.h"
namespace softbus::message_bus::ingress::edge_uplink
{
static std::uint16_t readU16BE(const unsigned char* p)
{
return qFromBigEndian<quint16>(reinterpret_cast<const quint8*>(p));
}
static std::uint32_t readU32BE(const unsigned char* p)
{
return qFromBigEndian<quint32>(reinterpret_cast<const quint8*>(p));
}
static std::int32_t readI32BE(const unsigned char* p)
{
return static_cast<std::int32_t>(readU32BE(p));
}
static std::int64_t readI64BE(const unsigned char* p)
{
return qFromBigEndian<qint64>(reinterpret_cast<const quint8*>(p));
}
static std::uint64_t readU64BE(const unsigned char* p)
{
return qFromBigEndian<quint64>(reinterpret_cast<const quint8*>(p));
}
QByteArray encodeOuterFrame(FrameKind kind, const QByteArray& payload)
{
QByteArray out;
out.resize(static_cast<int>(kOuterHeaderBytes + payload.size()));
unsigned char* d = reinterpret_cast<unsigned char*>(out.data());
d[0] = static_cast<unsigned char>(kMagic0);
d[1] = static_cast<unsigned char>(kMagic1);
d[2] = static_cast<unsigned char>(kMagic2);
d[3] = static_cast<unsigned char>(kMagic3);
qToBigEndian(kWireVersion, d + 4);
qToBigEndian(static_cast<std::uint16_t>(kind), d + 6);
qToBigEndian(static_cast<std::uint32_t>(0), d + 8);
qToBigEndian(static_cast<std::uint32_t>(payload.size()), d + 12);
if (!payload.isEmpty()) {
std::memcpy(d + kOuterHeaderBytes, payload.constData(), static_cast<std::size_t>(payload.size()));
}
return out;
}
QByteArray encodeDataRawBusPayload(int deviceId,
std::uint32_t endpointHash,
std::uint16_t protocol,
std::uint32_t logicalAddress,
std::uint64_t routingKey,
std::uint64_t protocolHeaderRaw,
qint64 timestampMs,
std::int8_t direction,
const QString& traceId,
const QByteArray& busPayload)
{
const QByteArray tr = traceId.toUtf8();
if (tr.size() > kMaxTraceLen) {
return {};
}
QByteArray inner;
inner.resize(static_cast<int>(kDataRawBusFixedBytes + tr.size() + busPayload.size()));
unsigned char* w = reinterpret_cast<unsigned char*>(inner.data());
qToBigEndian(static_cast<quint16>(tr.size()), w + 0);
qToBigEndian(static_cast<qint32>(deviceId), reinterpret_cast<uchar*>(w + 2));
qToBigEndian(endpointHash, w + 6);
qToBigEndian(protocol, w + 10);
qToBigEndian(static_cast<quint16>(0), w + 12);
qToBigEndian(logicalAddress, w + 14);
qToBigEndian(routingKey, w + 18);
qToBigEndian(protocolHeaderRaw, w + 26);
qToBigEndian(timestampMs, reinterpret_cast<uchar*>(w + 34));
w[42] = static_cast<unsigned char>(direction);
std::memset(w + 43, 0, 5);
if (!tr.isEmpty()) {
std::memcpy(w + 48, tr.constData(), static_cast<std::size_t>(tr.size()));
}
if (!busPayload.isEmpty()) {
std::memcpy(w + 48 + tr.size(), busPayload.constData(), static_cast<std::size_t>(busPayload.size()));
}
return inner;
}
std::optional<OuterFramePop> tryPopOneOuterFrame(QByteArray& buffer, int maxPayloadBytes)
{
if (buffer.size() < static_cast<int>(kOuterHeaderBytes)) {
return std::nullopt;
}
const unsigned char* d = reinterpret_cast<const unsigned char*>(buffer.constData());
if (d[0] != static_cast<unsigned char>(kMagic0) || d[1] != static_cast<unsigned char>(kMagic1)
|| d[2] != static_cast<unsigned char>(kMagic2) || d[3] != static_cast<unsigned char>(kMagic3)) {
LOG_WARNING() << "EdgeUplinkWire: bad magic, dropping byte";
buffer.remove(0, 1);
return std::nullopt;
}
const std::uint16_t ver = readU16BE(d + 4);
if (ver != kWireVersion) {
LOG_WARNING() << "EdgeUplinkWire: unsupported wire_version" << ver;
buffer.clear();
return std::nullopt;
}
const std::uint32_t payloadLen = readU32BE(d + 12);
if (maxPayloadBytes > 0 && payloadLen > static_cast<std::uint32_t>(maxPayloadBytes)) {
LOG_WARNING() << "EdgeUplinkWire: payload_len exceeds max" << payloadLen << maxPayloadBytes;
buffer.clear();
return std::nullopt;
}
const int total = static_cast<int>(kOuterHeaderBytes) + static_cast<int>(payloadLen);
if (payloadLen > 64u * 1024u * 1024u) {
LOG_WARNING() << "EdgeUplinkWire: absurd payload_len" << payloadLen;
buffer.clear();
return std::nullopt;
}
if (buffer.size() < total) {
return std::nullopt;
}
OuterFramePop fr;
fr.kind = static_cast<FrameKind>(readU16BE(d + 6));
fr.payload = QByteArray(buffer.constData() + static_cast<int>(kOuterHeaderBytes), static_cast<int>(payloadLen));
fr.consumedBytes = total;
buffer.remove(0, total);
return fr;
}
bool decodeDataRawBusPayload(const QByteArray& payload,
softbus::core::models::RawBusMessage& ctx,
QByteArray& busPayloadOut,
QString* errorMessage)
{
if (payload.size() < static_cast<int>(kDataRawBusFixedBytes)) {
if (errorMessage) {
*errorMessage = QStringLiteral("data_rawbus_too_short");
}
return false;
}
const unsigned char* p = reinterpret_cast<const unsigned char*>(payload.constData());
const std::uint16_t traceLen = readU16BE(p + 0);
if (traceLen > static_cast<std::uint16_t>(kMaxTraceLen)) {
if (errorMessage) {
*errorMessage = QStringLiteral("trace_too_long");
}
return false;
}
const int need = static_cast<int>(kDataRawBusFixedBytes) + static_cast<int>(traceLen);
if (payload.size() < need) {
if (errorMessage) {
*errorMessage = QStringLiteral("data_rawbus_truncated_trace");
}
return false;
}
ctx = {};
ctx.deviceId = readI32BE(p + 2);
ctx.endpointHash = readU32BE(p + 6);
ctx.protocol = static_cast<softbus::core::models::ProtocolType>(readU16BE(p + 10));
ctx.logicalAddress = readU32BE(p + 14);
ctx.routingKey = readU64BE(p + 18);
std::memcpy(&ctx.header, p + 26, 8);
ctx.timestampMs = readI64BE(p + 34);
ctx.direction = static_cast<softbus::core::models::BusDirection>(static_cast<signed char>(p[42]));
if (traceLen > 0) {
ctx.traceId = QString::fromUtf8(reinterpret_cast<const char*>(p + 48), static_cast<int>(traceLen));
}
busPayloadOut = payload.mid(need);
ctx.payload = {};
return true;
}
} // namespace softbus::message_bus::ingress::edge_uplink

View File

@@ -0,0 +1,65 @@
#pragma once
#include <cstdint>
#include <optional>
#include <QByteArray>
#include <QString>
#include "core/models/RawBusMessage.h"
namespace softbus::message_bus::ingress::edge_uplink
{
inline constexpr char kMagic0 = 'S';
inline constexpr char kMagic1 = 'B';
inline constexpr char kMagic2 = 'U';
inline constexpr char kMagic3 = 'P';
inline constexpr std::uint16_t kWireVersion = 1;
enum class FrameKind : std::uint16_t
{
Hello = 1,
Ack = 2,
DataRawBus = 3,
TreeFull = 4,
TreePatch = 5,
TreePropose = 6,
TreeResult = 7,
Report = 8,
};
inline constexpr std::size_t kOuterHeaderBytes = 16;
inline constexpr std::size_t kDataRawBusFixedBytes = 48;
inline constexpr int kMaxTraceLen = 512;
struct OuterFramePop
{
FrameKind kind{FrameKind::Hello};
QByteArray payload;
int consumedBytes{0};
};
std::optional<OuterFramePop> tryPopOneOuterFrame(QByteArray& buffer, int maxPayloadBytes = 4 * 1024 * 1024);
QByteArray encodeOuterFrame(FrameKind kind, const QByteArray& payload);
/** 与核心 `DATA_RAWBUS` 内层二进制布局一致(边缘上行编码)。 */
QByteArray encodeDataRawBusPayload(int deviceId,
std::uint32_t endpointHash,
std::uint16_t protocol,
std::uint32_t logicalAddress,
std::uint64_t routingKey,
std::uint64_t protocolHeaderRaw,
qint64 timestampMs,
std::int8_t direction,
const QString& traceId,
const QByteArray& busPayload);
// 填充除 payload.block 外的标量与 tracebusPayload 为总线原始字节,由调用方 allocate 后写入 ctx.payload
bool decodeDataRawBusPayload(const QByteArray& payload,
softbus::core::models::RawBusMessage& ctx,
QByteArray& busPayloadOut,
QString* errorMessage);
} // namespace softbus::message_bus::ingress::edge_uplink

View File

@@ -0,0 +1,19 @@
#pragma once
#include <QString>
#include "core/models/RawBusMessage.h"
namespace softbus::message_bus::ingress
{
class IIngressPort
{
public:
virtual ~IIngressPort() = default;
virtual bool push(softbus::core::models::RawBusMessage ctx) = 0;
virtual void reportError(const QString& endpoint, const QString& error) = 0;
};
} // namespace softbus::message_bus::ingress

374
src/ops_ui_http.cpp Normal file
View File

@@ -0,0 +1,374 @@
#include "ops_ui_http.hpp"
#include <QFile>
#include <QHostAddress>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QTcpServer>
#include <QTcpSocket>
#include <QUrlQuery>
namespace
{
QString htmlEscape(const QString& s)
{
QString o;
o.reserve(s.size() * 2);
for (QChar c : s) {
switch (c.unicode()) {
case '&':
o += QStringLiteral("&amp;");
break;
case '<':
o += QStringLiteral("&lt;");
break;
case '>':
o += QStringLiteral("&gt;");
break;
case '"':
o += QStringLiteral("&quot;");
break;
default:
o += c;
break;
}
}
return o;
}
QByteArray buildUiPage(const QString& edgeId, const QJsonArray& discovered)
{
QString options = QStringLiteral("<option value=\"\">— 请选择已发现端口 —</option>\n");
for (const QJsonValue& v : discovered) {
if (!v.isObject()) {
continue;
}
const QJsonObject o = v.toObject();
const QString nid = o.value(QStringLiteral("nodeId")).toString();
const QString dev = o.value(QStringLiteral("devnode")).toString();
if (nid.isEmpty() || dev.isEmpty()) {
continue;
}
options += QStringLiteral("<option value=\"%1\">%2 — %3</option>\n")
.arg(htmlEscape(nid), htmlEscape(dev), htmlEscape(nid));
}
if (discovered.isEmpty()) {
options = QStringLiteral("<option value=\"\">(暂无发现记录,可先插拔串口或关闭后再试)</option>\n");
}
const QString html = QStringLiteral(
"<!doctype html><html lang=zh-CN><head><meta charset=utf-8>"
"<meta name=viewport content=\"width=device-width,initial-scale=1\">"
"<title>边缘运维 — 发现端口注册</title>"
"<style>body{font-family:system-ui,sans-serif;max-width:42rem;margin:1rem auto;padding:0 .75rem}"
"label{display:block;margin:.6rem 0 .2rem}input,select,button{width:100%;box-sizing:border-box;padding:.4rem}"
"button{margin-top:1rem;cursor:pointer}code{background:#f4f4f4;padding:.1rem .3rem}</style></head><body>"
"<h1>发现端口 → 核心注册</h1>"
"<p>边缘 ID<code>%1</code>。从 udev 最近发现列表中选端口,填写协议字符串(与设备树 <code>params.protocol</code> 一致,如 <code>modbus_rtu</code>),提交后将向核心发送一条 <code>TREE_PROPOSE</code><code>op: upsert</code>)。需已连接核心且收到有效 <code>treeRevision</code>。</p>"
"<form method=post action=/register_discovered enctype=application/x-www-form-urlencoded>"
"<label for=nodeId>已发现节点</label><select id=nodeId name=nodeId required>%2</select>"
"<label for=devnode>设备节点(可选,未在下拉匹配时填写 /dev/tty…</label>"
"<input id=devnode name=devnode type=text placeholder=/dev/ttyUSB0>"
"<label for=protocol>协议占位params.protocol</label>"
"<input id=protocol name=protocol type=text value=modbus_rtu required>"
"<label for=baudRate>波特率</label>"
"<input id=baudRate name=baudRate type=number value=115200 min=300 step=1>"
"<label for=name>显示名称(可选)</label>"
"<input id=name name=name type=text placeholder=\"例如 电表\">"
"<button type=submit>发送 upsert PROPOSE</button></form>"
"<p><a href=/>本地树 JSON</a> · <a href=/discovered>发现 JSON</a> · <code>POST /propose</code> 仍可提交完整 JSON。</p>"
"</body></html>")
.arg(htmlEscape(edgeId), options);
return html.toUtf8();
}
QString devnodeForNodeId(const QJsonArray& discovered, const QString& nodeId)
{
for (const QJsonValue& v : discovered) {
if (!v.isObject()) {
continue;
}
const QJsonObject o = v.toObject();
if (o.value(QStringLiteral("nodeId")).toString() == nodeId) {
return o.value(QStringLiteral("devnode")).toString();
}
}
return {};
}
QString stableKeyFromNodeId(const QString& nodeId)
{
const QString prefix = QStringLiteral("/discovered/");
const int i = nodeId.indexOf(prefix);
if (i < 0) {
return {};
}
return nodeId.mid(i + prefix.size());
}
} // namespace
OpsUiHttp::OpsUiHttp(QObject* parent)
: QObject(parent)
{
}
OpsUiHttp::~OpsUiHttp()
{
if (m_tcp) {
m_tcp->close();
m_tcp->deleteLater();
m_tcp = nullptr;
}
}
bool OpsUiHttp::start(quint16 port,
const QString& localTreePath,
const QString& edgeId,
const std::function<qint64()>& getBaseTreeRevision,
const std::function<void(const QJsonObject&)>& onPropose,
const std::function<QJsonArray()>& getDiscovered)
{
m_treePath = localTreePath;
m_edgeId = edgeId;
m_getBaseTreeRevision = getBaseTreeRevision;
m_onPropose = onPropose;
m_getDiscovered = getDiscovered;
m_tcp = new QTcpServer(this);
connect(m_tcp, &QTcpServer::newConnection, this, &OpsUiHttp::onNewConnection);
if (!m_tcp->listen(QHostAddress::LocalHost, port)) {
return false;
}
return true;
}
void OpsUiHttp::onNewConnection()
{
while (QTcpSocket* sock = m_tcp->nextPendingConnection()) {
QByteArray acc;
if (!sock->waitForReadyRead(8000)) {
sock->deleteLater();
continue;
}
acc += sock->readAll();
while (!acc.contains("\r\n\r\n") && sock->waitForReadyRead(500)) {
acc += sock->readAll();
}
handleSocket(sock, acc);
sock->deleteLater();
}
}
static void writeHttp(QTcpSocket* sock, int code, const QByteArray& body, const char* contentType)
{
QByteArray statusLine = QByteArrayLiteral("HTTP/1.1 ");
statusLine += QByteArray::number(code);
if (code == 200) {
statusLine += QByteArrayLiteral(" OK");
} else if (code == 400) {
statusLine += QByteArrayLiteral(" Bad Request");
} else if (code == 404) {
statusLine += QByteArrayLiteral(" Not Found");
} else if (code == 503) {
statusLine += QByteArrayLiteral(" Service Unavailable");
} else {
statusLine += QByteArrayLiteral(" Error");
}
statusLine += QByteArrayLiteral("\r\nContent-Type: ");
statusLine += QByteArray(contentType);
statusLine += QByteArrayLiteral("\r\nContent-Length: ");
statusLine += QByteArray::number(body.size());
statusLine += QByteArrayLiteral("\r\nConnection: close\r\n\r\n");
(void)sock->write(statusLine);
(void)sock->write(body);
sock->disconnectFromHost();
}
void OpsUiHttp::handleSocket(QTcpSocket* sock, const QByteArray& data)
{
const int sep = data.indexOf("\r\n\r\n");
if (sep < 0) {
writeHttp(sock, 400, QByteArrayLiteral("bad request"), "text/plain; charset=utf-8");
return;
}
const QByteArray headers = data.left(sep);
const QByteArray body = data.mid(sep + 4);
const int firstLineEnd = headers.indexOf("\r\n");
if (firstLineEnd < 0) {
writeHttp(sock, 400, QByteArrayLiteral("bad request line"), "text/plain; charset=utf-8");
return;
}
const QByteArray reqLine = headers.left(firstLineEnd);
const QList<QByteArray> parts = reqLine.split(' ');
if (parts.size() < 2) {
writeHttp(sock, 400, QByteArrayLiteral("bad request line"), "text/plain; charset=utf-8");
return;
}
const QByteArray method = parts[0];
const QByteArray path = parts[1];
if (method == "GET" && path == "/") {
QFile f(m_treePath);
if (!f.open(QIODevice::ReadOnly)) {
const QByteArray html =
QByteArrayLiteral("<!doctype html><meta charset=utf-8><title>edge</title>"
"<p>无本地树文件。</p><p><a href=/ui>打开配置页</a> · <a href=/discovered>发现 JSON</a></p>");
writeHttp(sock, 200, html, "text/html; charset=utf-8");
return;
}
const QByteArray fileBody = f.readAll();
writeHttp(sock, 200, fileBody, "application/json; charset=utf-8");
return;
}
if (method == "GET" && path == "/discovered") {
QJsonArray arr;
if (m_getDiscovered) {
arr = m_getDiscovered();
}
const QByteArray out =
QJsonDocument(QJsonObject{{QStringLiteral("items"), arr}}).toJson(QJsonDocument::Indented);
writeHttp(sock, 200, out, "application/json; charset=utf-8");
return;
}
if (method == "GET" && path == "/ui") {
QJsonArray arr;
if (m_getDiscovered) {
arr = m_getDiscovered();
}
writeHttp(sock, 200, buildUiPage(m_edgeId, arr), "text/html; charset=utf-8");
return;
}
if (method == "POST" && path == "/propose") {
QJsonParseError pe;
const QJsonDocument doc = QJsonDocument::fromJson(body, &pe);
if (pe.error != QJsonParseError::NoError || !doc.isObject()) {
writeHttp(sock, 400, QByteArrayLiteral("invalid json"), "text/plain; charset=utf-8");
return;
}
if (m_onPropose) {
m_onPropose(doc.object());
}
const QJsonObject ok{{QStringLiteral("accepted"), true}};
writeHttp(sock, 200, QJsonDocument(ok).toJson(QJsonDocument::Compact), "application/json; charset=utf-8");
return;
}
if (method == "POST" && path == "/register_discovered") {
QString nodeId;
QString devnodeIn;
QString protocolStr = QStringLiteral("modbus_rtu");
int baudRate = 115200;
QString nameOpt;
const QByteArray trimmed = body.trimmed();
if (trimmed.startsWith('{')) {
QJsonParseError pe;
const QJsonDocument doc = QJsonDocument::fromJson(body, &pe);
if (pe.error != QJsonParseError::NoError || !doc.isObject()) {
writeHttp(sock, 400, QByteArrayLiteral("invalid json"), "text/plain; charset=utf-8");
return;
}
const QJsonObject o = doc.object();
nodeId = o.value(QStringLiteral("nodeId")).toString();
devnodeIn = o.value(QStringLiteral("devnode")).toString();
protocolStr = o.value(QStringLiteral("protocol")).toString(protocolStr);
baudRate = o.value(QStringLiteral("baudRate")).toInt(baudRate);
nameOpt = o.value(QStringLiteral("name")).toString();
} else {
QUrlQuery q(QString::fromUtf8(body));
nodeId = q.queryItemValue(QStringLiteral("nodeId"));
devnodeIn = q.queryItemValue(QStringLiteral("devnode"));
protocolStr = q.queryItemValue(QStringLiteral("protocol"), QUrl::FullyDecoded);
if (protocolStr.isEmpty()) {
protocolStr = QStringLiteral("modbus_rtu");
}
bool okBr = false;
baudRate = q.queryItemValue(QStringLiteral("baudRate")).toInt(&okBr);
if (!okBr || baudRate <= 0) {
baudRate = 115200;
}
nameOpt = q.queryItemValue(QStringLiteral("name"), QUrl::FullyDecoded);
}
if (nodeId.isEmpty()) {
writeHttp(sock, 400, QByteArrayLiteral("missing nodeId"), "text/plain; charset=utf-8");
return;
}
QJsonArray discovered;
if (m_getDiscovered) {
discovered = m_getDiscovered();
}
QString devnode = devnodeIn.trimmed();
if (devnode.isEmpty()) {
devnode = devnodeForNodeId(discovered, nodeId);
}
if (devnode.isEmpty()) {
writeHttp(sock, 400, QByteArrayLiteral("missing devnode (not in /discovered; fill devnode field)"),
"text/plain; charset=utf-8");
return;
}
const qint64 baseRev = m_getBaseTreeRevision ? m_getBaseTreeRevision() : 0;
if (baseRev <= 0) {
const QJsonObject err{{QStringLiteral("error"), QStringLiteral("baseTreeRevision unavailable")}};
writeHttp(sock, 503, QJsonDocument(err).toJson(QJsonDocument::Compact), "application/json; charset=utf-8");
return;
}
const QString sk = stableKeyFromNodeId(nodeId);
QJsonObject params;
params.insert(QStringLiteral("edgeId"), m_edgeId);
if (!sk.isEmpty()) {
params.insert(QStringLiteral("stableKey"), sk);
}
params.insert(QStringLiteral("online"), true);
params.insert(QStringLiteral("discoveredVia"), QStringLiteral("ops_ui"));
params.insert(QStringLiteral("protocol"), protocolStr);
params.insert(QStringLiteral("baudRate"), baudRate);
QJsonObject node;
node.insert(QStringLiteral("id"), nodeId);
node.insert(QStringLiteral("name"),
nameOpt.isEmpty() ? QStringLiteral("serial %1").arg(devnode) : nameOpt);
node.insert(QStringLiteral("type"), QStringLiteral("serial"));
node.insert(QStringLiteral("endpoint"), devnode);
node.insert(QStringLiteral("enabled"), true);
node.insert(QStringLiteral("params"), params);
QJsonObject entry;
entry.insert(QStringLiteral("op"), QStringLiteral("upsert"));
entry.insert(QStringLiteral("id"), nodeId);
entry.insert(QStringLiteral("node"), node);
QJsonObject propose;
propose.insert(QStringLiteral("edgeId"), m_edgeId);
propose.insert(QStringLiteral("baseTreeRevision"), baseRev);
propose.insert(QStringLiteral("entries"), QJsonArray{entry});
if (m_onPropose) {
m_onPropose(propose);
}
const bool formSubmit = !trimmed.startsWith('{');
if (formSubmit) {
const QByteArray html =
QByteArrayLiteral("<!doctype html><meta charset=utf-8><title>已提交</title>"
"<p>已向核心发送 upsert <code>TREE_PROPOSE</code>。请查看边缘日志与核心 <code>TREE_RESULT</code>。</p>"
"<p><a href=/ui>返回配置页</a> · <a href=/discovered>发现 JSON</a></p>");
writeHttp(sock, 200, html, "text/html; charset=utf-8");
return;
}
const QJsonObject ok{{QStringLiteral("accepted"), true}, {QStringLiteral("sentNodeId"), nodeId}};
writeHttp(sock, 200, QJsonDocument(ok).toJson(QJsonDocument::Compact), "application/json; charset=utf-8");
return;
}
writeHttp(sock, 404, QByteArrayLiteral("not found"), "text/plain; charset=utf-8");
}

39
src/ops_ui_http.hpp Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <functional>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QString>
class QTcpServer;
class QTcpSocket;
class OpsUiHttp final : public QObject
{
Q_OBJECT
public:
explicit OpsUiHttp(QObject* parent = nullptr);
~OpsUiHttp() override;
bool start(quint16 port,
const QString& localTreePath,
const QString& edgeId,
const std::function<qint64()>& getBaseTreeRevision,
const std::function<void(const QJsonObject&)>& onPropose,
const std::function<QJsonArray()>& getDiscovered = {});
private slots:
void onNewConnection();
private:
void handleSocket(QTcpSocket* sock, const QByteArray& data);
QTcpServer* m_tcp{nullptr};
QString m_treePath;
QString m_edgeId;
std::function<qint64()> m_getBaseTreeRevision;
std::function<void(const QJsonObject&)> m_onPropose;
std::function<QJsonArray()> m_getDiscovered;
};

36
src/utils/logs/logging.h Normal file
View File

@@ -0,0 +1,36 @@
#ifndef LOGGING_H
#define LOGGING_H
#include <QDebug>
#include <QLoggingCategory>
#include <QString>
#include <cstring>
/**
* @file logging.h
* @brief 日志宏定义,提供带文件名和函数名的日志输出,并异步存储到数据库
*/
// 获取文件名(不含路径)
#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__)
// 构建来源字符串的辅助宏
// #define __LOG_SOURCE__ (QString(__FILENAME__) + "." + QString(__FUNCTION__))
// 日志宏,格式:[文件名.函数名] 消息
#define LOG_DEBUG() \
qDebug() << "[" << __FILENAME__ << "/" << __FUNCTION__ << "]"
#define LOG_ERROR() \
qCritical() << "[" << __FILENAME__ << "/" << __FUNCTION__ << "]"
#define LOG_INFO() \
qInfo() << "[" << __FILENAME__ << "/" << __FUNCTION__ << "]"
#define LOG_WARNING() \
qWarning() << "[" << __FILENAME__ << "/" << __FUNCTION__ << "]"
#endif // LOGGING_H