# 数据流转 ## 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::` 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`。 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`。 - 样本内容:`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 主链路;该点位在发现列表中自动消失(查询时过滤已映射键)。