Foxglove工具折线图Panel源码分析

332 阅读3分钟

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消息定义生成下拉框路径树

image.png

  • 想实现折线图Plot Panel功能,必须要知道x轴,y轴的参数、值是怎么来的,y轴一般是某个topic内某个属性数据的值,那么foxglove是怎么一层一层获取对应的路径的呢?
  • 这个是实现折线图的必经之路,内容很枯燥,代码很繁琐,但是还是的看!
  • 这会我已经不知道该从哪开始阅读代码了,这里教一个比较快定位代码的方案,那就是全局搜索附近的关键字!

2.1、先搜索“消息地址”

image.png

2.2、再搜索“messagePath”

image.png

-这个时候需要继续缩小搜索范围,因为我们找的是下拉框Plot Panel,所以在F12找到对应的文件,然后在“messagePath”的地方打断点,了解业务流。

三、获取y轴的path

3.1、获取第一层路径

image.png

  • 第一层path比较简单,就是topicname的数组,我们通过第一步的topics的值即可拿到

const path1 = topics.map(topic => topic.topicName); // 获取path第一层

3.2、获取第二层路径

image.png

  • 第二层例子,/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;
}