Foxglove工具折线图Panel源码分析
一、获取包信息,并缓存前置依赖项
- 使用时,需要先下载
@foxglove/rosbag和@foxglove/rosmsg库,这里默认已经做了解包或拖拽bag文件处理了,后面针对解包再做教程。
// packages\studio-base\src\players\IterablePlayer\BagIterableSource.ts
import { Bag, Filelike } from "@foxglove/rosbag";
import { parse as parseMessageDefinition } from "@foxglove/rosmsg";
import { BlobReader } from "@foxglove/rosbag/web";
// initialize() 函数
let fileLike: Filelike | undefined;
// 远程播包(类似websocket方案,我这里是直接拖包的方案,省略对应代码块)
if (this.#source.type === "remote") {
// ...
} else {
fileLike = new BlobReader(this.#source.file);
}
this.#bag = new Bag(fileLike, {
parse: false,
decompress: undefined // 这里是webassembly方案,我没有使用,忽略
});
await this.#bag.open();
// open 后this.#bag.connections 就会有.bag包信息的对应头信息
// 折线图所需topic信息
const topics = new Map<string, TopicWithDecodingInfo>();
// 折线图所需 datatypes 信息
const datatypes: RosDatatypes = new Map();
// 遍历
for (const [id, connection] of this.#bag.connections) {
const schemaName = connection.type;
// 保护,消息定义中一定需要定义好类型
if (!schemaName) {
continue;
}
const existingTopic = topics.get(connection.topic);
// 去重
if (!existingTopic) {
topics.set(connection.topic, {
name: connection.topic, // 消息主题
schemaName, // 消息接口名称
messageEncoding: "ros1", // foxglove自定义的encoding名称
schemaData: this.#textEncoder.encode(connection.messageDefinition), // 这里主要是拿消息定义,用来生成折线图选项路径,很重要,必要参数
schemaEncoding: "ros1msg", // foxglove自定义的schemaEncoding名称
});
}
// 解析topic消息定义
const messageDefinition = parse(TopicInfo.msg_def);
// 生成datatypes
const parsedDefinition = messageDefinition;
for (const definition of parsedDefinition) {
// 在解析的定义中,第一个定义(根)没有名称,因为它应该是
// 主题的数据类型。
// In parsed definitions, the first definition (root) does not have a name as is meant to
// be the datatype of the topic.
if (!definition.name) {
datatypes.set(schemaName, definition);
} else {
datatypes.set(definition.name, definition);
}
}
}
// initialize() 函数 返回值,最终存储到store里
// 这两个属性很重要,foxglove中很多业务都需要这两个属性的值进行处理
return {
topics: Array.from(topics.values()),
datatypes,
// ...其他属性
};
// initialize() 函数结束
二、根据topic消息定义生成下拉框路径树
- 想实现折线图Plot Panel功能,必须要知道x轴,y轴的参数、值是怎么来的,y轴一般是某个topic内某个属性数据的值,那么foxglove是怎么一层一层获取对应的路径的呢?
- 这个是实现折线图的必经之路,内容很枯燥,代码很繁琐,但是还是的看!
- 这会我已经不知道该从哪开始阅读代码了,这里教一个比较快定位代码的方案,那就是全局搜索附近的关键字!
2.1、先搜索“消息地址”
2.2、再搜索“messagePath”
-这个时候需要继续缩小搜索范围,因为我们找的是下拉框和Plot Panel,所以在F12找到对应的文件,然后在“messagePath”的地方打断点,了解业务流。
三、获取y轴的path
3.1、获取第一层路径
- 第一层
path比较简单,就是topicname的数组,我们通过第一步的topics的值即可拿到
const path1 = topics.map(topic => topic.topicName); // 获取path第一层
3.2、获取第二层路径
-
第二层例子,
/data_diagnose/diagnoser.header.frame_id, 获取/data_diagnose/diagnoser主题下的.header和.frame_id -
主函数
getAutoCompleteItemsByMessagePath通过这个函数来根据传入对应字段,返回对应的数据
/**
* 根据path返回topic的结构路径
* params.path 传undefined 或 空字符串,返回topicName列表
* params.path 符合topicname,返回该topic下的所有结构体数组
* params.path 传topic下的结构体路径字符串,返回undefined
*/
class Player {
public async getAutoCompleteItemsByMessagePath(params?: {
path: string | undefined;
type: 'filter' | 'select';
}) {
await this.preloadPromise; // 等待初始化完成
const _topics = [...this.plotTopics.values()];
if (params?.path === '' || params?.path === undefined) {
const topicsArray = _topics.map(({ name }) =>
quoteTopicNameIfNeeded(name)
);
return topicsArray;
} else {
if (this.plotTopics.get(params.path!)) {
const path = params.path! + '.';
const rosPath = parseMessagePath(path)!;
const structures = messagePathStructures(this.plotDataTypes);
const topic = (function () {
if (!rosPath) {
return undefined;
}
const { topicName } = rosPath;
return _topics.find(({ name }) => name === topicName);
})();
const structure =
topic?.schemaName != undefined
? structures[topic?.schemaName]
: undefined;
const autocompleteItems = filterMap(
messagePathsForStructure(structure, {
validTypes: plotableRosTypes,
noMultiSlices: undefined,
messagePath: rosPath.messagePath
}),
item => item.path
);
return autocompleteItems;
}
}
return undefined;
}
}
quoteTopicNameIfNeeded函数
// quoteTopicNameIfNeeded 函数
/** Wrap topic name in double quotes if it contains special characters */
export function quoteTopicNameIfNeeded(name: string): string {
// Pattern should match `slashID` in grammar.ne
if (name.match(/^[a-zA-Z0-9_/-]+$/)) {
return name;
}
return `"${name.replace(/[\\"]/g, char => `\\${char}`)}"`;
}
parseMessagePath函数
/** parseMessagePath 函数 */
import { Grammar, Parser } from 'nearley';
import grammar from './grammar.ne';
import { MessagePath } from './types';
const grammarObj = Grammar.fromCompiled(grammar);
const parseMessagePath = (path: string): MessagePath | undefined => {
// Need to create a new Parser object for every new string to parse (should be cheap).
const parser = new Parser(grammarObj);
try {
const result = parser.feed(path).results[0];
console.log(result);
return result;
} catch (_err) {
return undefined;
}
};
messagePathStructures函数
import _ from 'lodash';
import { type RosDatatypes } from 'typings/foxglove/RosDatatypes';
import { quoteFieldNameIfNeeded } from './parseMessagePath';
// 防止ts报错
type PrimitiveType = any;
type MessagePathStructureItem = any;
type MessagePathStructureItemMessage = any;
type Immutable = any;
type MessagePathPart = any;
// 如果将案例添加到联合中,则在此处强制转换为 _as_ PrimitiveType 会导致 TypeScript 错误
function isPrimitiveType(type: string): type is PrimitiveType {
// casting _as_ PrimitiveType here to have typescript error if add a case to the union
switch (type as PrimitiveType) {
case 'bool':
case 'int8':
case 'uint8':
case 'int16':
case 'uint16':
case 'int32':
case 'uint32':
case 'int64':
case 'uint64':
case 'float32':
case 'float64':
case 'string':
return true;
}
return false;
}
export function messagePathStructures(
datatypes: Immutable<RosDatatypes>
): Record<string, MessagePathStructureItemMessage> {
const structureFor = _.memoize(
(
datatype: string,
seenDatatypes: string[]
): MessagePathStructureItemMessage => {
const nextByName: Record<string, MessagePathStructureItem> = {};
const rosDatatype = datatypes.get(datatype);
if (!rosDatatype) {
// "time" and "duration" are considered "built-in" types in ROS
// If we can't find a datatype in our datatypes list we fall-back to our hard-coded versions
if (datatype === 'time' || datatype === 'duration') {
return {
structureType: 'message',
nextByName: {
sec: {
structureType: 'primitive',
primitiveType: 'uint32',
datatype: ''
},
nsec: {
structureType: 'primitive',
primitiveType: 'uint32',
datatype: ''
}
},
datatype
};
}
throw new Error(`datatype not found: "${datatype}"`);
}
for (const msgField of rosDatatype.definitions) {
if (msgField.isConstant === true) {
continue;
}
if (seenDatatypes.includes(msgField.type)) {
continue;
}
const next: MessagePathStructureItem = isPrimitiveType(msgField.type)
? {
structureType: 'primitive',
primitiveType: msgField.type,
datatype
}
: structureFor(msgField.type, [...seenDatatypes, msgField.type]);
if (msgField.isArray === true) {
nextByName[msgField.name] = {
structureType: 'array',
next,
datatype
};
} else {
nextByName[msgField.name] = next;
}
}
return { structureType: 'message', nextByName, datatype };
}
);
const structures: Record<string, MessagePathStructureItemMessage> = {};
for (const [datatype] of datatypes) {
structures[datatype] = structureFor(datatype, []);
}
return structures;
}