207 lines
9.9 KiB
Markdown
207 lines
9.9 KiB
Markdown
# 数据流转
|
||
|
||
## 1. 概念约定
|
||
|
||
- **物理设备**:真实硬件端口(当前主要是串口),由 `SerialQtDriver` 直接读写。
|
||
- **设备实例**:`SerialDevice`,负责单设备生命周期、驱动回调、上下行数据接入。
|
||
- **原始总线消息**:`RawBusMessage`,包含 `endpointHash`、`protocol`、`routingKey`、`header`、`payload`、`traceId`、`direction` 等基础字段。
|
||
- **流水线上下文**:`PipelineContext`,继承 `RawBusMessage`,增加 `frameKind`、`frameSeq` 等管道字段。
|
||
- **业务消息**:经过协议解析和映射后得到 `DOMMessage`(统一语义数据)。
|
||
|
||
---
|
||
|
||
## 2. 上行主链路(串口 -> 消息总线)
|
||
|
||
### 2.1 设备初始化
|
||
|
||
`SerialDeviceManager` 完成以下动作:
|
||
|
||
1. 根据配置创建并配置 `SerialQtDriver`(波特率、校验、停止位、流控)。
|
||
2. 创建 `SerialDevice` 并注入 `DeviceInfo`(含协议提示,如 `modbus_rtu`)。
|
||
3. 通过 `bindIngress(...)` 绑定:
|
||
- `IIngressPort`(当前实现为 `DriverIngressAdapter`)
|
||
- `MemoryPool`(用于 payload 内存块分配)
|
||
4. 调用 `start()` 打开端口并注册读回调/错误回调。
|
||
|
||
### 2.2 读回调进入 ingress
|
||
|
||
当串口收到字节流时,`SerialDevice::onRead(...)` 执行:
|
||
|
||
1. 从 `MemoryPool` 分配内存块并拷贝原始字节。
|
||
2. 构造 `RawBusMessage`:
|
||
- `direction = Upstream`
|
||
- `endpointHash = makeEndpointHash(串口名)`
|
||
- `protocol = protocolTypeFromHint(DeviceInfo.protocol)`
|
||
- `routingKey = makeRoutingKey(endpointHash, deviceId)`
|
||
- `header = ProtocolHeader{}`(由后续 framer/parser 按协议填充 OOB 元数据)
|
||
- `payload = 原始字节数据`(长度由 `payload.length` 表示,不再维护 `payloadSize`)
|
||
- `traceId = serial:<endpoint>:<timestampMs>`
|
||
3. 调用 `m_ingress->push(std::move(ctx))` 进入统一入口。
|
||
4. 若内存池不足或 ingress 队列满,通过 `reportError(...)` 记录错误。
|
||
|
||
### 2.3 ingress 适配为 PipelineContext
|
||
|
||
`DriverIngressAdapter::push(...)` 负责轻量转换:
|
||
|
||
1. 新建 `PipelineContext`。
|
||
2. `pipelineCtx.raw() = std::move(ctx)` 将原始消息搬运进管道上下文。
|
||
3. 调用 `PipelineEngine::enqueueIngress(...)` 入总线处理队列。
|
||
|
||
---
|
||
|
||
## 3. PipelineEngine 内部流转
|
||
|
||
### 3.1 入口分流
|
||
|
||
`enqueueIngress(...)` 分两种模式:
|
||
|
||
- **并行模式(parallelPipeline=true)**
|
||
- 按 `endpointHash` 创建/复用独立 `EndpointFramingState`(每端点一个分帧线程 + chunk 队列)。
|
||
- chunk 先进入端点队列,端点线程负责分帧并赋值 `frameSeq`。
|
||
- 完整帧进入 `m_framedQ`,由解析线程池处理。
|
||
- **单线程模式(parallelPipeline=false)**
|
||
- 直接进入 `m_ingressQ`,由单 worker 串行处理。
|
||
|
||
### 3.2 Stage 1:分帧(Framer)
|
||
|
||
`dispatchFramer(...)` 的选择策略:
|
||
|
||
1. 按 `protocol`(`ProtocolType`)从插件管理器选择协议分帧插件。
|
||
2. 按结构化键 `SessionKey{endpointHash, pluginId}` 复用 framer/parser/mapper session(保持协议状态机连续性)。
|
||
3. 若没有可用插件,降级到 `PassthroughFramer`(透传/整包策略)。
|
||
4. framer session 创建接口已升级为 `createFramerSession(uint32_t endpointHash, ...)`,不再传递 endpoint 字符串。
|
||
|
||
输出:`frameKind = CompleteFrame` 的帧批次。
|
||
|
||
### 3.3 Stage 2/3/4:解析 -> 过滤 -> 校验(当前返回语义)
|
||
|
||
`runStages234(...)` 核心流程:
|
||
|
||
1. **Stage2 Parser**
|
||
- 选择 `IProtocolPlugin`,复用 parser session。
|
||
- `parse(rawCtx, envelope)` 产出协议封装 `ProtocolEnvelope`。
|
||
- 选择 mapper 插件并映射为 `std::vector<DOMMessage>`。
|
||
2. **Stage3 Filter**
|
||
- 对 `DOMMessage` 执行过滤规则(`ProtocolParseFilter::process`)。
|
||
3. **Stage4 Validator**
|
||
- 最低校验当前为 `messageId` 非空。
|
||
4. **Transform/Enrich**
|
||
- 仅对通过 filter+validator 的 `DOMMessage` 应用动态规则(scale/offset/unit)。
|
||
- 规则作用域按 `endpointHash/deviceId/stableKey` 匹配,不再按 endpoint 字符串匹配。
|
||
- 写入 trace,补充富化字段。
|
||
|
||
说明(与代码一致):
|
||
- `runStages234` 在 `stage2Parser` 返回空时才返回 `false`。
|
||
- 只要 `stage2Parser` 有消息,哪怕全部在 Stage3/Stage4 被跳过,当前实现仍返回 `true`。
|
||
- `EnrichedMessage` 在内部生成,但尚未进入独立发布/持久化通道。
|
||
|
||
### 3.4 出口与有序保证
|
||
|
||
- 未开启有序出口:解析成功后直接进入 `m_egressQ`。
|
||
- 开启 `orderedEgress`:
|
||
- 解析线程先写入 `m_mergeQ`。
|
||
- `mergerLoop` 按 `endpointHash(映射键) + frameSeq` 重排。
|
||
- 使用 `nextExpected` 保证同端点有序输出。
|
||
- 超过 `maxReorderDepth` 的乱序缓存上限会丢弃并计数。
|
||
|
||
---
|
||
|
||
## 4. 错误与背压点
|
||
|
||
主要失败点及表现(按当前实现):
|
||
|
||
1. **内存池分配失败**:`SerialDevice::onRead` 直接 `reportError(memory pool exhausted)`。
|
||
2. **入口队列满**:`enqueueIngress` 返回 false,计入 drop。
|
||
3. **分帧/解析/映射插件缺失或失败**:`stage2Parser` 返回空,`runStages234=false`,该帧不进入 egress。
|
||
4. **过滤/校验失败**:对应 `DOMMessage` 被跳过(不做 enrich),但该 `PipelineContext` 仍可能继续进入 egress(只要 Stage2 非空)。
|
||
5. **重排深度超限**:`mergeDrop` 增加并告警。
|
||
|
||
---
|
||
|
||
## 5. 当前链路时序(上行)
|
||
|
||
```text
|
||
SerialQtDriver(read callback)
|
||
-> SerialDevice::onRead
|
||
-> MemoryPool::allocate + RawBusMessage
|
||
-> IIngressPort::push (DriverIngressAdapter)
|
||
-> PipelineEngine::enqueueIngress
|
||
-> [Framer] dispatchFramer/feedChunk
|
||
-> [Stage2] parse + map -> DOMMessage
|
||
-> [Stage3] filter
|
||
-> [Stage4] validate
|
||
-> transform/enrich
|
||
-> egressQ (有序模式下先 merge 再 egress)
|
||
-> DeviceBusManager::processEgressLoop
|
||
-> EgressRouter::send
|
||
-> SerialDevice::writePayload
|
||
```
|
||
|
||
---
|
||
|
||
## 6. egress 到设备(现状)
|
||
|
||
- `DeviceBusManager` 已启动 `processEgressLoop`,持续从 `PipelineEngine::tryPopEgress` 取数据并调用 `EgressRouter::send`。
|
||
- `EgressRouter` 已实现基于 `endpointHash` 的 `SerialDevice` 查找和 `writePayload(...)` 发送。
|
||
- 当前关键缺口:`SerialDeviceManager::initializeSerialDeviceWithDeps` 中 `bindSerialDevice(...)` 仍被注释,导致路由表通常为空,`send` 多数返回失败。
|
||
- 因此当前链路是“egress 框架已存在,但默认没有绑定到串口实例”。
|
||
|
||
---
|
||
|
||
## 7. 维护建议
|
||
|
||
1. `traceId` 建议在全链路透传到最终发布侧,便于定位帧级问题。
|
||
2. 为 Stage2/Stage3/Stage4 增加细粒度指标(成功率、耗时、drop 原因)。
|
||
3. 文档后续可新增“协议插件开发约定”章节(framer/parser/mapper 三类插件接口对照)。
|
||
|
||
---
|
||
|
||
## 8. 无规则冷启动与发现池(新增)
|
||
|
||
### 8.1 设计目标
|
||
|
||
- 默认保持极致性能:**未命中映射规则的寄存器不会产出 DOMMessage**。
|
||
- 仅在配置页面开启时进入“嗅探模式(Sniffer Mode)”,将未命中点位以低成本写入内存发现池。
|
||
- 发现池只维护“最近值”,不做全量历史存储,避免高频总线拖垮系统。
|
||
|
||
### 8.2 Mapper 行为(Modbus RTU)
|
||
|
||
`ModbusRtuProtocolMapperPlugin` + `PipelineEngine` 内 `IMappingLookup` 组合行为:
|
||
|
||
1. 对 `ReadHoldingRegistersRequest` 记录 `unitId -> startAddress`,用于后续响应地址回填。
|
||
2. 对 `ReadHoldingRegistersResponse` 遍历寄存器并构造 `compositeKey`:
|
||
- `compositeKey = endpointHash + protocol + slaveId + registerAddress`(位拼接)。
|
||
3. **解析优先级(统一语义)**:`MappingRegistry`(运行时绑定)优先;未命中则查 `ProfileRegistry`(`device_profiles.json`,按 `runtimeDeviceId` 与 `modbus_rtu` 协议匹配,支持 `*` 通配设备)。
|
||
4. 若上述之一命中:
|
||
- 生成轻量 `DOMMessage`:`messageId`/`pointId` 来自绑定或 profile,`metadataId` 来自 profile(若有),`value` 为工程值;静态元数据不随每条样本重复序列化,由 `metadataId` 在控制面/Registry 解析。
|
||
5. 若仍未命中:
|
||
- 若存在 `discoverySink`,始终上报 `DiscoveryRecord`(发现池内部仍受嗅探开关与限频约束);
|
||
- **仅当嗅探开启**时,主链路额外产出 `modbus.unmapped:*` 的 `DOMMessage` 供规则调试;嗅探关闭时不产出未映射 DOM,减少总线冗载。
|
||
|
||
### 8.3 DiscoveryPool 约束
|
||
|
||
- 数据结构:`unordered_map<uint64_t, DiscoverySample>`。
|
||
- 样本内容:`compositeKey/endpointHash/protocol/slaveId/registerAddress/rawValue/firstSeenMs/lastSeenMs/hitCount`。
|
||
- 性能保护:
|
||
- `maxEntries` 限制最大条目数(超限按最旧 `lastSeenMs` 淘汰);
|
||
- `minSampleIntervalMs` 做按键限频,降低写放大;
|
||
- 支持 `ttlMs` 自动关闭嗅探,防止遗留长期开启。
|
||
|
||
### 8.4 REST API(配置工作台)
|
||
|
||
- `POST /api/v1/discovery/start`:开启嗅探(可传 `ttlMs`)。
|
||
- `POST /api/v1/discovery/stop`:关闭嗅探。
|
||
- `GET /api/v1/discovery/status`:查看嗅探状态与池容量信息。
|
||
- `GET /api/v1/discovery/samples`:拉取发现样本(支持 `offset/limit/endpointHash/protocol`)。
|
||
- `POST /api/v1/discovery/clear`:清空发现池。
|
||
- `POST /api/v1/mapping/bind`:新增或更新绑定规则(`compositeKey -> domPath + scale/offset/unit`)。
|
||
- `POST /api/v1/mapping/unbind`:删除绑定规则。
|
||
- `GET /api/v1/mapping/list`:查看当前绑定规则。
|
||
|
||
### 8.5 双向匹配工作流
|
||
|
||
1. Vue 打开配置页 -> 调用 `discovery/start`。
|
||
2. 左侧“发现列表”轮询 `discovery/samples`。
|
||
3. 用户执行 Bottom-Up(从样本创建 DOM 绑定)或 Top-Down(将样本拖到 DOM 占位符)。
|
||
4. 前端调用 `mapping/bind`,规则即时生效。
|
||
5. 下一帧同 `compositeKey` 数据在 mapper 命中,进入 DOM 主链路;该点位在发现列表中自动消失(查询时过滤已映射键)。 |