9.9 KiB
9.9 KiB
数据流转
1. 概念约定
- 物理设备:真实硬件端口(当前主要是串口),由
SerialQtDriver直接读写。 - 设备实例:
SerialDevice,负责单设备生命周期、驱动回调、上下行数据接入。 - 原始总线消息:
RawBusMessage,包含endpointHash、protocol、routingKey、header、payload、traceId、direction等基础字段。 - 流水线上下文:
PipelineContext,继承RawBusMessage,增加frameKind、frameSeq等管道字段。 - 业务消息:经过协议解析和映射后得到
DOMMessage(统一语义数据)。
2. 上行主链路(串口 -> 消息总线)
2.1 设备初始化
SerialDeviceManager 完成以下动作:
- 根据配置创建并配置
SerialQtDriver(波特率、校验、停止位、流控)。 - 创建
SerialDevice并注入DeviceInfo(含协议提示,如modbus_rtu)。 - 通过
bindIngress(...)绑定:IIngressPort(当前实现为DriverIngressAdapter)MemoryPool(用于 payload 内存块分配)
- 调用
start()打开端口并注册读回调/错误回调。
2.2 读回调进入 ingress
当串口收到字节流时,SerialDevice::onRead(...) 执行:
- 从
MemoryPool分配内存块并拷贝原始字节。 - 构造
RawBusMessage:direction = UpstreamendpointHash = makeEndpointHash(串口名)protocol = protocolTypeFromHint(DeviceInfo.protocol)routingKey = makeRoutingKey(endpointHash, deviceId)header = ProtocolHeader{}(由后续 framer/parser 按协议填充 OOB 元数据)payload = 原始字节数据(长度由payload.length表示,不再维护payloadSize)traceId = serial:<endpoint>:<timestampMs>
- 调用
m_ingress->push(std::move(ctx))进入统一入口。 - 若内存池不足或 ingress 队列满,通过
reportError(...)记录错误。
2.3 ingress 适配为 PipelineContext
DriverIngressAdapter::push(...) 负责轻量转换:
- 新建
PipelineContext。 pipelineCtx.raw() = std::move(ctx)将原始消息搬运进管道上下文。- 调用
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(...) 的选择策略:
- 按
protocol(ProtocolType)从插件管理器选择协议分帧插件。 - 按结构化键
SessionKey{endpointHash, pluginId}复用 framer/parser/mapper session(保持协议状态机连续性)。 - 若没有可用插件,降级到
PassthroughFramer(透传/整包策略)。 - framer session 创建接口已升级为
createFramerSession(uint32_t endpointHash, ...),不再传递 endpoint 字符串。
输出:frameKind = CompleteFrame 的帧批次。
3.3 Stage 2/3/4:解析 -> 过滤 -> 校验(当前返回语义)
runStages234(...) 核心流程:
- Stage2 Parser
- 选择
IProtocolPlugin,复用 parser session。 parse(rawCtx, envelope)产出协议封装ProtocolEnvelope。- 选择 mapper 插件并映射为
std::vector<DOMMessage>。
- 选择
- Stage3 Filter
- 对
DOMMessage执行过滤规则(ProtocolParseFilter::process)。
- 对
- Stage4 Validator
- 最低校验当前为
messageId非空。
- 最低校验当前为
- Transform/Enrich
- 仅对通过 filter+validator 的
DOMMessage应用动态规则(scale/offset/unit)。
- 仅对通过 filter+validator 的
- 规则作用域按
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. 错误与背压点
主要失败点及表现(按当前实现):
- 内存池分配失败:
SerialDevice::onRead直接reportError(memory pool exhausted)。 - 入口队列满:
enqueueIngress返回 false,计入 drop。 - 分帧/解析/映射插件缺失或失败:
stage2Parser返回空,runStages234=false,该帧不进入 egress。 - 过滤/校验失败:对应
DOMMessage被跳过(不做 enrich),但该PipelineContext仍可能继续进入 egress(只要 Stage2 非空)。 - 重排深度超限:
mergeDrop增加并告警。
5. 当前链路时序(上行)
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. 维护建议
traceId建议在全链路透传到最终发布侧,便于定位帧级问题。- 为 Stage2/Stage3/Stage4 增加细粒度指标(成功率、耗时、drop 原因)。
- 文档后续可新增“协议插件开发约定”章节(framer/parser/mapper 三类插件接口对照)。
8. 无规则冷启动与发现池(新增)
8.1 设计目标
- 默认保持极致性能:未命中映射规则的寄存器不会产出 DOMMessage。
- 仅在配置页面开启时进入“嗅探模式(Sniffer Mode)”,将未命中点位以低成本写入内存发现池。
- 发现池只维护“最近值”,不做全量历史存储,避免高频总线拖垮系统。
8.2 Mapper 行为(Modbus RTU)
ModbusRtuProtocolMapperPlugin + PipelineEngine 内 IMappingLookup 组合行为:
- 对
ReadHoldingRegistersRequest记录unitId -> startAddress,用于后续响应地址回填。 - 对
ReadHoldingRegistersResponse遍历寄存器并构造compositeKey:compositeKey = endpointHash + protocol + slaveId + registerAddress(位拼接)。
- 解析优先级(统一语义):
MappingRegistry(运行时绑定)优先;未命中则查ProfileRegistry(device_profiles.json,按runtimeDeviceId与modbus_rtu协议匹配,支持*通配设备)。 - 若上述之一命中:
- 生成轻量
DOMMessage:messageId/pointId来自绑定或 profile,metadataId来自 profile(若有),value为工程值;静态元数据不随每条样本重复序列化,由metadataId在控制面/Registry 解析。
- 生成轻量
- 若仍未命中:
- 若存在
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 双向匹配工作流
- Vue 打开配置页 -> 调用
discovery/start。 - 左侧“发现列表”轮询
discovery/samples。 - 用户执行 Bottom-Up(从样本创建 DOM 绑定)或 Top-Down(将样本拖到 DOM 占位符)。
- 前端调用
mapping/bind,规则即时生效。 - 下一帧同
compositeKey数据在 mapper 命中,进入 DOM 主链路;该点位在发现列表中自动消失(查询时过滤已映射键)。