# softbus_edge **路径:** `src/softbus_edge` **构建目标:** `softbus_edge`(EXECUTABLE) **层级:** L3 可执行层 ## 1. 概述 `softbus_edge` 是边缘代理进程,运行在靠近现场设备的一侧,负责: - **物理域采集(ingress)**:按 `physical_*.json` 轮询 Modbus / CANopen / 裸以太网设备,产出 `RawPacket` - **上行(egress/uplink)**:经 TCP 将 `RawPacket`、心跳、注册信息发送给 `softbus_daemon` - **下行(egress)**:接收 daemon 下发的 `ExecuteCommand`,写回物理设备(Modbus 写寄存器等) edge **不**加载逻辑点表、**不**操作 OPC UA、**不**运行 `protocol_modbus` 插件。协议解析与 OPC 绑定均在 daemon 侧完成。 | 模式 | 说明 | |------|------| | **正常模式** | `CompositeIngress`(Modbus + CANopen + Eth),读真实硬件 | | **Mock 模式** | `--mock`,无硬件,模拟 Modbus 帧上行 | | **独立模式** | daemon 未启动时 edge 仍可运行,后台重连 uplink | --- ## 2. 与 daemon 的职责边界(双域) ``` ┌─────────────────────────────────────────────────────────────────┐ │ edge(物理域) daemon(逻辑域) │ ├─────────────────────────────────────────────────────────────────┤ │ config/edge/ config/daemon/ │ │ edge.json daemon_profile.json │ │ physical_channels.json logical_points.json │ │ physical_devices.json bindings_global.json │ │ bindings_local.json(可选) topic_rule.json │ │ │ │ 读硬件 → RawPacket RawPacket → 插件解析 │ │ 写硬件 ← physicalPointId → bindings → OPC UA 节点 │ └─────────────────────────────────────────────────────────────────┘ ``` | 配置 | edge 是否加载 | 用途 | |------|---------------|------| | `physical_channels.json` | 是 | 串口 / TCP 通道参数 | | `physical_devices.json` | 是 | 从站、寄存器、物理点 ID | | `bindings_local.json` | 可选 | 下行写命令的**本地备用**解析(正常联调几乎不用) | | `logical_points.json` | **否** | daemon 专用 | | `bindings_global.json` | **否** | daemon 专用(上行绑定 + 下行路由) | edge 上行时**不做** binding 初筛:所有已配置物理点都会采集并上报;是否绑定 OPC 由 daemon 的 `bindings_global.json` 决定(未绑定点进入 `edge/unbound/raw/...`)。 --- ## 3. 配置加载流程 ### 3.1 入口文件 默认配置:`config/edge/edge.json`。子路径**相对 `edge.json` 所在目录**解析。 ```json { "edge": { "edgeId": "edge01", "deviceId": "EdgeDevice_01", "daemonHost": "127.0.0.1", "daemonPort": 9000, "heartbeatPeriodMs": 1000, "uplinkReconnectPeriodMs": 5000, "physicalChannels": "physical_channels.json", "physicalDevices": "physical_devices.json", "bindingsLocal": "bindings_local.json" } } ``` | 字段 | 说明 | |------|------| | `edgeId` | 边缘实例 ID,uplink 注册与 daemon 路由用 | | `deviceId` | 逻辑设备名(ACK 等场景) | | `daemonHost` / `daemonPort` | 核心 uplink 地址 | | `heartbeatPeriodMs` | 心跳周期(仅 uplink 已连接时发送) | | `uplinkReconnectPeriodMs` | daemon 不可达时的重连间隔 | | `physicalChannels` / `physicalDevices` | 物理层配置 | | `bindingsLocal` | 可选;空则跳过 | | `bindingsPatch` | 可选;热加载绑定补丁 | ### 3.2 启动时加载顺序(`EdgeRuntime::init`) ``` 1. EdgeConfig.loadFromFile(--config) 2. PhysicalRegistry.loadChannels(physical_channels.json) 3. PhysicalRegistry.loadDevices(physical_devices.json, edgeId) → 物理点 ID 自动加前缀:edge01/phy_modbus_s1_r40001 4. [可选] BindingRegistry.loadLocal(bindings_local.json) 5. UplinkClient + CommandExecutor 初始化 6. DeviceFactory.createIngress → CompositeIngress(按 protocol 创建子 ingress) 7. 各 ChannelManager.openChannel(libmodbus / SocketCAN / asio) 8. uplink.connect();成功则 sendEdgeRegister(edgeId) → 失败仅 WARN,edge 独立运行并后台重连 ``` ### 3.3 物理通道(`physical_channels.json`) ```json { "channels": [ { "channelId": "serial_0", "protocol": "modbus", "transport": "rtu", "port": "/dev/ttyS3", "baudRate": 9600, "parity": "N", "dataBits": 8, "stopBits": 1, "pollIntervalMs": 500 } ] } ``` | 字段 | 说明 | |------|------| | `transport` | `rtu`(串口)或 `tcp`(Modbus TCP,需 `host` + `tcpPort`) | | `pollIntervalMs` | 通道默认轮询周期(点可覆盖) | | `interface` / `bitrate` | CANopen 通道:`can0`、`500000` | | `host` / `tcpPort` | Modbus TCP 或裸以太网 | CANopen 通道示例见 `config/edge/physical_channels_can.json`。 ### 3.4 物理设备与点(`physical_devices.json`) ```json { "devices": [ { "physicalDeviceId": "phy_modbus_slave_1", "channelId": "serial_0", "protocol": "modbus", "slaveId": 1, "points": [ { "physicalPointId": "phy_modbus_s1_r40001", "register": 40001, "function": 3, "scale": 0.1, "signalType": "AI", "writable": false, "pollIntervalMs": 1000, "reportOnChange": true, "deadband": 0.5 } ] } ] } ``` **点级可选字段:** | 字段 | 说明 | |------|------| | `pollIntervalMs` | 覆盖通道默认周期 | | `reportOnChange` | `true` 时启用变化上报 | | `deadband` | 工程值死区(配合 `reportOnChange`) | | `objectIndex` / `subIndex` | CANopen 对象字典索引 | | `pdoType` | `tpdo` / `sdo` / `rpdo` | | `cobId` | PDO COB-ID 覆盖 | | `nodeId` / `edsPath` | 设备级 CANopen 字段 | 运行时 `physicalPointId` 会变为 `{edgeId}/{physicalPointId}`,作为 `RawPacket.sourceId` 上报。 CANopen 示例见 `config/edge/physical_devices_can.json` 与 `config/edge/samples/minimal.eds`。 ### 3.5 热加载 | 信号 | 行为 | |------|------| | `SIGHUP` | `reloadPhysical()`:重载 channels + devices,重建 Modbus 连接 | | `SIGHUP` | `reloadBindingsPatch()`:若配置了 `bindingsPatch` 则增量应用 | | 文件变更 | `ConfigWatcher` 监视 `physical_channels`、`physical_devices`、`bindingsPatch` | --- ## 4. 组件架构 ### 4.0 目录结构 ``` include/softbus_edge/ ingress/ IIngressPort, CompositeIngress modbus/ ModbusIngress, MockModbusIngress can/ CanIngress eth/ EthIngress driver/ modbus/ ModbusChannelManager (libmodbus) can/ CanChannelManager (SocketCAN) eth/ EthChannelManager (asio) egress/ modbus/ ModbusEgress can/ CanEgress (SDO 写) eth/ EthEgress scheduler/ ModbusPollScheduler, PollPlanner, ChangeFilter canopen/ NmtManager, SdoClient, PdoMapper config/ runtime/ factory/ discovery/ ``` ``` main.cpp └── EdgeRuntime ├── PhysicalRegistry ├── CompositeIngress │ ├── ModbusIngress + ModbusPollScheduler / PollPlanner / ChangeFilter │ ├── CanIngress + NmtManager / PdoMapper / SdoClient │ └── EthIngress ├── ModbusEgress / CanEgress / EthEgress ├── CommandExecutor ← 按 protocol 分发写 └── UplinkClient ``` ### 4.1 IIngressPort — 入站抽象 | 方法 | 说明 | |------|------| | `open()` | 打开采集通道 | | `close()` | 关闭采集通道 | | `poll()` | 轮询,返回 `vector` | **ModbusIngress**:`PollPlanner` 调度到期点 → `ModbusPollScheduler` 合并相邻寄存器批量读 → `ChangeFilter` 变化过滤 → `RawPacket`。 **CanIngress**:启动时 NMT Start + 加载 EDS;TPDO 监听 + SDO 慢点轮询;`protocolTag: "canopen"`。 **EthIngress**:裸 TCP/UDP 收字节;`protocolTag: "raw_eth"`。 **MockModbusIngress**:`--mock` 模式,无硬件模拟帧。 ### 4.2 ModbusChannelManager — 硬件 I/O - 构建时链接 **libmodbus**(系统包或 `third_party/libmodbus` vendor) - RTU:`modbus_new_rtu` + `modbus_read_registers` / `modbus_write_register` - TCP:`modbus_new_tcp`(`transport: tcp` 时) - 无 libmodbus 时退化为模拟读写(编译宏 `SOFTBUS_HAS_LIBMODBUS=0`) > edge **不使用** daemon 侧的 `protocol_modbus.so` 插件;插件仅用于 daemon 解析上行 RawPacket。 ### 4.3 UplinkClient — 上行/下行 | 方法 | 说明 | |------|------| | `connect()` / `disconnect()` | TCP 连接 daemon | | `sendEdgeRegister(edgeId)` | 注册边缘实例 | | `sendHeartbeat(deviceId)` | 心跳 | | `sendRawPacket(RawPacket)` | 上行原始包 | | `sendAck(SoftbusAck)` | 写命令 ACK | | `poll()` | 接收下行 `ExecuteCommand` | | `setCommandHandler(handler)` | 注册命令回调 | 协议详见 [UPLINK_WIRE_PROTOCOL.md](UPLINK_WIRE_PROTOCOL.md)。 ### 4.4 CommandExecutor — 下行写 daemon 经 `bindings_global` 在命令中注入 `physicalPointId` + `edgeId`,edge 按协议写: ``` modbus → ModbusEgress canopen → CanEgress (SDO download) raw → EthEgress ``` 仅当命令**缺少** `physicalPointId` 时,才用 `bindings_local.json` 做 `logicalPointId → physicalPointId` 备用解析。 --- ## 5. 数据流 ### 5.1 上行(采集 → OPC) ```mermaid sequenceDiagram participant HW as 现场设备 participant Edge as softbus_edge participant Daemon as softbus_daemon participant OPC as OPC UA HW->>Edge: Modbus RTU/TCP 读寄存器 Edge->>Edge: ModbusIngress.poll → RawPacket Note over Edge: sourceId = edge01/phy_xxx
protocolTag = modbus Edge->>Daemon: TCP rawPacket + edgeRegister/heartbeat Daemon->>Daemon: protocol_modbus 解析帧 Daemon->>Daemon: bindings_global → logicalPointId Daemon->>Daemon: logical_points → objectRef + scale Daemon->>OPC: UaModelBinder 写节点值 ``` 逐步说明: 1. **edge**:`ModbusIngress` 读硬件 → `RawPacket{ sourceId=physicalPointId, payload=Modbus帧 }` 2. **edge**:`UplinkClient.sendRawPacket` → TCP JSON 行 3. **daemon**:`TransportBusBridge` 发布到总线 `edge/raw/{physicalPointId}` 4. **daemon**:`PipelineEngine` + `protocol_modbus` 插件解析 → 寄存器值 5. **daemon**:`bindings_global` 查绑定 → `logical_points` 得 OPC 路径 → scale → 写 OPC UA 未绑定物理点:daemon 发布到 `edge/unbound/raw/{physicalPointId}`,不写 OPC。 ### 5.2 下行(OPC 写 → 设备) ```mermaid sequenceDiagram participant OPC as OPC UA 客户端 participant Daemon as softbus_daemon participant Edge as softbus_edge participant HW as 现场设备 OPC->>Daemon: ExecuteCommand (objectRef + value) Daemon->>Daemon: bindings_global → physicalPointId + edgeId Daemon->>Edge: TCP executeCommand Edge->>Edge: CommandExecutor → ModbusEgress Edge->>HW: libmodbus 写寄存器 Edge->>Daemon: TCP ack ``` ### 5.3 主循环(`main.cpp`) ```cpp while (!shouldStop) { ConfigWatcher.poll(); if (SIGHUP) { reloadPhysical(); reloadBindingsPatch(); } runtime.tick(); // uplink.poll + ingress.poll + sendRawPacket if (uplinkConnected && heartbeatDue) sendHeartbeat(edgeId); sleep(50ms); } ``` --- ## 6. CLI 参数 | 参数 | 默认值 | 说明 | |------|--------|------| | `--config ` | `config/edge/edge.json` | 边缘入口配置 | | `--mock` | 关闭 | 使用 MockModbusIngress,跳过 physical 配置与 libmodbus | | `--object-ref ` | `DemoSite/.../MotorTemperature` | Mock 模式下的模拟 sourceId | > 不再使用 `--daemon host:port`;uplink 地址由 `edge.json` 的 `daemonHost` / `daemonPort` 指定。 --- ## 7. 依赖 | 依赖 | 用途 | |------|------| | `softbus_core` | RawPacket、Logger、PlatformSignal、ConfigWatcher | | `softbus_registry` | BindingRegistry(仅 local 可选) | | `softbus_transport` | TcpUplinkClient、UplinkWire | | `softbus_api` | ExecuteCommand、SoftbusAck | | **libmodbus** | Modbus RTU/TCP 硬件 I/O | | **SocketCAN** | Linux `linux/can.h`(`SOFTBUS_HAS_SOCKETCAN`) | | **softbus_canopen_common** | EDS 解析、对象字典 | | asio | Uplink + EthChannel | --- ## 8. 构建与运行 ### 8.1 libmodbus 构建时自动选择:系统 `libmodbus-dev` 优先,否则编译 `third_party/libmodbus` 静态库进 edge。 ```bash cmake -B build cmake --build build --target softbus_edge ``` 串口权限:`sudo usermod -aG dialout $USER` ### 8.2 运行示例 ```bash # 1. 先启动 daemon ./build/bin/softbus_daemon \ --config config/daemon/daemon_profile.json \ --logical-points config/daemon/logical_points.json \ --bindings-global config/daemon/bindings_global.json # 2. 启动 edge(真实 Modbus) ./build/bin/softbus_edge --config config/edge/edge.json # 3. 无硬件 / 无 daemon 调试 ./build/bin/softbus_edge --config config/edge/edge.json --mock # 4. 热加载物理配置 kill -HUP $(pidof softbus_edge) ``` 更多配置说明见 [edge_device_setup.md](edge_device_setup.md)。 --- ## 9. 多协议与物理层优化(已实现) ### 9.1 多 Ingress 并存 `physical_channels.json` 中可混合 `protocol: modbus | canopen | raw`,`DeviceFactory` 为每种协议创建子 ingress,由 `CompositeIngress` 统一 `poll()`。 ### 9.2 Modbus 采集优化 | 组件 | 行为 | |------|------| | **ModbusPollScheduler** | 同 (channel, slave, function) 下合并相邻寄存器,单次最多 125 寄存器 | | **PollPlanner** | 每点独立 `pollIntervalMs`(缺省继承通道) | | **ChangeFilter** | `reportOnChange` + `deadband` 物理层过滤 | ### 9.3 CANopen(edge 已实现) | 组件 | 说明 | |------|------| | **CanChannelManager** | SocketCAN 收发(无头文件时模拟降级) | | **NmtManager** | 启动时 NMT Start Remote Node | | **EdsDcfLoader** | `softbus_canopen_common` 解析 EDS | | **PdoMapper** | TPDO 订阅表 + 帧解包 | | **SdoClient** | SDO upload(慢点)/ download(下行写) | | **daemon** | `protocol_canopen.so` 解析仍占位 | CAN 前置:`sudo ip link set can0 up type can bitrate 500000` ### 9.4 裸以太网(骨架) `protocol: "raw"` + `transport: tcp|udp` → `EthIngress` 原样上行;`EthEgress` 支持原始字节写。 Modbus TCP 仍走 `protocol: modbus` + `transport: tcp`。 ### 9.5 待增强 | 项 | 说明 | |----|------| | **DeviceDiscovery** | 总线扫描、动态写入 `physical_devices` | | **daemon CANopen 解析** | `CanopenParser` 完整实现 | | **Eth 帧解析** | 自定义帧格式插件化 | --- ## 10. 约束(CONSTRAINTS) - edge **不得**直接操作 OPC UA 或加载 `protocol_*.so` 解析插件 - edge **不得**加载 `logical_points.json` / `bindings_global.json` - ingress 实现须遵循 `IIngressPort`,便于接入 CANopen/Ethernet driver - 上行统一为 `RawPacket`;`sourceId` 必须是 **physicalPointId**(含 edgeId 前缀) - daemon 不可达时 edge **必须**能独立启动并采集(uplink 后台重连) --- ## 11. 相关文档 | 文档 | 内容 | |------|------| | [edge/tcp_rtu_callstack.md](edge/tcp_rtu_callstack.md) | Modbus TCP 点完整调用栈(含 RTU-over-TCP) | | [edge_device_setup.md](edge_device_setup.md) | 配置目录、双域、libmodbus、Mock | | [UPLINK_WIRE_PROTOCOL.md](UPLINK_WIRE_PROTOCOL.md) | TCP JSON 线协议 | | [softbus_daemon.md](softbus_daemon.md) | daemon 侧 Pipeline 与 OPC | | [plugins.md](plugins.md) | protocol 插件(daemon 解析用) |