整体分层架构
┌─────────────────────────────────────────────────────────────────┐
│ 应用层 (Application) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ WebSocket │ │ HTTP Server │ │ File Server │ │
│ │ Service │ │ │ │ │ │
│ └──────┬───────┘ └──────────────┘ └──────────────┘ │
└─────────┼────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 输出层 (Output Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ XVIZFormatWriter │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ JSON │ │ Binary │ │ Protobuf │ │ │
│ │ │ Format │ │ GLB │ │ Format │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────┼────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 构建层 (Builder Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Builder Service │ │
│ │ │ │
│ │ ┌───────────────────────┐ ┌───────────────────────┐ │ │
│ │ │ XVIZMetadataBuilder │ │ XVIZBuilder │ │ │
│ │ │ (初始化时调用一次) │ │ (每帧都调用) │ │ │
│ │ │ │ │ │ │ │
│ │ │ - stream() │ │ - pose() │ │ │
│ │ │ - category() │ │ - primitive() │ │ │
│ │ │ - type() │ │ - polyline() │ │ │
│ │ │ - streamStyle() │ │ - polygon() │ │ │
│ │ │ - getMetadata() │ │ - points() │ │ │
│ │ │ │ │ - getMessage() │ │ │
│ │ └───────────────────────┘ └───────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────┼────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 转换层 (Transform Layer) │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Dispatcher │ │
│ │ - 收集所有 Parser 的输出 │ │
│ │ - 按 streamName 分组 │ │
│ │ - 调用 Builder Service │ │
│ └────────────┬───────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Parser (插件化) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Lane │ │ Obstacle │ │ Point │ ... │ │
│ │ │ Parser │ │ Parser │ │ Cloud │ │ │
│ │ └──────────┘ └──────────┘ │ Parser │ │ │
│ │ └──────────┘ │ │
│ │ 输入:中间格式 │ │
│ │ 输出:XVIZ 数据结构 │ │
│ │ 作用:转换为符合 XVIZ 协议的数据 │ │
│ └────────────┬──────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Formatter (插件化) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Lane │ │ Obstacle │ │ Pose │ ... │ │
│ │ │ Formatter│ │ Formatter│ │ Formatter│ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ 输入:原始 ROS 消息 │ │
│ │ 输出:中间格式 (normalPrivateData, latched...) │ │
│ │ 作用:业务逻辑转换、坐标变换、数据聚合 │ │
│ └────────────┬──────────────────────────────────────────┘ │
└───────────────┼──────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 预处理层 (Preprocess Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Loader / Event Emitter │ │
│ │ │ │
│ │ - subscribe(topic, callback) │ │
│ │ - preformat(data) │ │
│ │ - emit(topic, data) │ │
│ │ - Worker 线程通信 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────┼────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 数据源层 (Data Source Layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Reader / Parser │ │
│ │ │ │
│ │ ┌────────────┐ │ │
│ │ │ ROS Bag │ │ │
│ │ │ Reader │ - readAt(timestamp) │ │
│ │ │ │ - 按 topic 过滤 │ │
│ │ │ │ - 时间同步 │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ 输入:bag 文件路径 │ │
│ │ 输出:原始 ROS 消息 (topic, message, timestamp) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
数据流转图
ROS Bag ↓ Reader/Loader (读取原始消息) ↓ Preformat (预处理) ↓ Loader Subscribe (订阅) ↓ 【Formatter】← 第一次转换 输入:ROS 原始消息 输出:中间格式(你们公司自定义) ↓ 【Parser】← 第二次转换 输入:Formatter 的中间格式 输出:符合 XVIZ 的数据结构 ↓ Dispatch (派发) ↓ Builder Service ← 使用 XVIZBuilder 将 Parser 的输出添加到 XVIZBuilder ↓ XVIZFormatWriter 写入文件或发送到前端
┌─────────────┐
│ ROS Bag │
│ File │
└──────┬──────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Reader Layer │
│ ┌────────────────────────────────────────────────┐ │
│ │ reader.readAt(timestamp) │ │
│ │ ↓ │ │
│ │ { topic, message_type, message, timestamp } │ │
│ └────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Preprocess Layer │
│ ┌────────────────────────────────────────────────┐ │
│ │ preformat(data) │ │
│ │ ↓ │ │
│ │ topicEventEmitter.emit(topic, data) │ │
│ │ ↓ │ │
│ │ loader.subscribe(topic, callback) │ │
│ │ ↓ │ │
│ │ worker.postMessage({ rawData, ... }) │ │
│ └────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Transform Layer (Worker 线程) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Formatter │ │
│ │ 输入: { topic, message } │ │
│ │ 处理: 业务逻辑转换 │ │
│ │ 输出: { │ │
│ │ normalPrivateData: {...}, │ │
│ │ latchedPrivateData: {...}, │ │
│ │ latchedSharedData: {...} │ │
│ │ } │ │
│ └────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Parser │ │
│ │ 输入: privateData (Formatter 的输出) │ │
│ │ 处理: 转换为 XVIZ 数据结构 │ │
│ │ 输出: { │ │
│ │ polylines: [{ │ │
│ │ vertices: [...], │ │
│ │ base: { object_id, style } │ │
│ │ }] │ │
│ │ } │ │
│ └────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Dispatcher │ │
│ │ dispatch(builderData, pluginConfig) │ │
│ │ ↓ │ │
│ │ builderDatas[streamName] = { │ │
│ │ builderData, │ │
│ │ pluginConfig │ │
│ │ } │ │
│ └────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Builder Layer │
│ ┌────────────────────────────────────────────────┐ │
│ │ builderService.onBuildMessage({ │ │
│ │ buildMessage, // Parser 的输出 │ │
│ │ pluginConfig, │ │
│ │ frameTimestamp, │ │
│ │ frameBuilder // XVIZBuilder 实例 │ │
│ │ }) │ │
│ │ ↓ │ │
│ │ frameBuilder │ │
│ │ .primitive(streamName) │ │
│ │ .polyline(vertices) │ │
│ │ .id(object_id) │ │
│ │ .style(style) │ │
│ │ ↓ │ │
│ │ frameBuilder.getMessage() │ │
│ └────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Output Layer │
│ ┌────────────────────────────────────────────────┐ │
│ │ XVIZFormatWriter │ │
│ │ ↓ │ │
│ │ writer.writeMetadata(metadata) │ │
│ │ writer.writeMessage(xvizMessage) │ │
│ │ ↓ │ │
│ │ 序列化为 JSON / Binary / Protobuf │ │
│ └────────────────────────────────────────────────┘ │
└───────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌────────────────────────────────────────────────┐ │
│ │ WebSocket / HTTP / File │ │
│ │ ↓ │ │
│ │ 发送到前端 / 写入文件 │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
插件化架构
┌─────────────────────────────────────────────────────────┐
│ Plugin Manager │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Plugin A │ │ Plugin B │ │ Plugin C │ ... │
│ │ (Lane) │ │(Obstacle)│ │ (Pose) │ │
│ │ │ │ │ │ │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │Format│ │ │ │Format│ │ │ │Format│ │ │
│ │ │-ter │ │ │ │-ter │ │ │ │-ter │ │ │
│ │ └──┬───┘ │ │ └──┬───┘ │ │ └──┬───┘ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ ▼ │ │ ▼ │ │ ▼ │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │Parser│ │ │ │Parser│ │ │ │Parser│ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ │ │ │ │ │ │ │
│ │ Config: │ │ Config: │ │ Config: │ │
│ │ - topic │ │ - topic │ │ - topic │ │
│ │ - stream │ │ - stream │ │ - stream │ │
│ │ - style │ │ - style │ │ - style │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
完整架构流程
┌─────────────────────────────────────────────────────────────┐
│ 第一步:Reader/Loader │
│ 读取 bag 文件,获取原始 ROS 消息 │
└──────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ reader.readAt() 按时间戳读取 │
│ messageQueue[topic].push({ │
│ topic, message_type, │
│ message, timestamp │
│ }) │
└──────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第二步:Preformat + Event Emitter │
│ 预处理消息并通过事件发射 │
└──────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ Object.keys(messageQueue) │
│ .forEach((topic) => { │
│ const topicEventData = │
│ preformat(data) │
│ this.topicEventEmitter │
│ .emit(topic, topicEventData)│
│ }) │
└──────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第三步:Loader Subscribe │
│ 订阅 topic,获取 rawData │
└──────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ this.loader.subscribe(topic, │
│ (rawData) => { │
│ this.worker.postMessage({ │
│ rawData, │
│ privateData, │
│ sharedData │
│ }) │
│ }) │
└──────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第四步:Formatter(在 Worker 中) │
│ 将 rawData 格式化为中间数据格式 │
└──────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ plugins.forEach( │
│ (plugin) => plugin.activate() │
│ ) │
│ │
│ const { topic, message } = │
│ rawData │
│ │
│ this.formatters[topic]({ │
│ topic, message, │
│ lastLatchedPrivateData │
│ }) │
│ │
│ // 输出: │
│ { │
│ normalPrivateData, │
│ latchedPrivateData, │
│ latchedSharedData │
│ } │
└──────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第五步:Parser(在 Worker 中) │
│ 解析中间数据,输出符合 XVIZ 格式的数据 │
└──────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ await Promise.all( │
│ plugins.map( │
│ (plugin) => plugin.parse() │
│ ) │
│ ) │
│ │
│ // Parser 输出符合 XVIZ 的数据 │
│ dispatch(builderData, │
│ pluginConfig) │
└──────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第六步:Dispatch + Builder Service │
│ 使用 XVIZBuilder 构建最终 XVIZ 消息 │
└──────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ builderDatas[streamName] = { │
│ builderData, │
│ pluginConfig │
│ } │
│ │
│ builderService.onBuildMessage({ │
│ buildMessage: builderData, │
│ pluginConfig, │
│ frameTimestamp, │
│ frameBuilder ← XVIZBuilder! │
│ }) │
└──────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 第七步:XVIZFormatWriter │
│ 写入文件或发送到前端 │
└──────────────────┬──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ const sink = new FileSink(path) │
│ let writer = new XVIZFormatWriter│
│ (sink, { │
│ format: XVIZ_FORMAT.BINARY_PBE│
│ }) │
│ │
│ writer.writeMetadata(metadata) │
│ writer.writeMessage(...) │
└──────────────────────────────────┘
关键设计模式
1. 管道模式 (Pipeline Pattern)
Reader → Preprocess → Formatter → Parser → Builder → Writer
2. 插件模式 (Plugin Pattern)
pluginManager.register('lane', {
formatter: laneFormatter,
parser: laneParser,
config: { ... }
})
3. 适配器模式 (Adapter Pattern)
ROS 格式 → [Formatter 适配] → 中间格式
中间格式 → [Parser 适配] → XVIZ 格式
4. 发布-订阅模式 (Pub-Sub Pattern)
loader.subscribe(topic, callback)
eventEmitter.emit(topic, data)
5. 建造者模式 (Builder Pattern)
XVIZBuilder
.pose('/pose')
.primitive('/lanes')
.getMessage()