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

发现端口 → 核心注册

" + "

边缘 ID:%1。从 udev 最近发现列表中选端口,填写协议字符串(与设备树 params.protocol 一致,如 modbus_rtu),提交后将向核心发送一条 TREE_PROPOSEop: upsert)。需已连接核心且收到有效 treeRevision

" + "
" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
" + "

本地树 JSON · 发现 JSON · POST /propose 仍可提交完整 JSON。

" + "") + .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& getBaseTreeRevision, + const std::function& onPropose, + const std::function& 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 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("edge" + "

无本地树文件。

打开配置页 · 发现 JSON

"); + 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("已提交" + "

已向核心发送 upsert TREE_PROPOSE。请查看边缘日志与核心 TREE_RESULT

" + "

返回配置页 · 发现 JSON

"); + 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"); +} diff --git a/src/ops_ui_http.hpp b/src/ops_ui_http.hpp new file mode 100644 index 0000000..2187126 --- /dev/null +++ b/src/ops_ui_http.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include +#include +#include +#include + +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& getBaseTreeRevision, + const std::function& onPropose, + const std::function& 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 m_getBaseTreeRevision; + std::function m_onPropose; + std::function m_getDiscovered; +}; diff --git a/src/utils/logs/logging.h b/src/utils/logs/logging.h new file mode 100644 index 0000000..2c8ef09 --- /dev/null +++ b/src/utils/logs/logging.h @@ -0,0 +1,36 @@ +#ifndef LOGGING_H +#define LOGGING_H + +#include +#include +#include +#include + + +/** + * @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 +