gici-open学习日记(1):主函数以及读取配置文件

gici-open——主函数以及读取配置文件

  • 主函数
  • `NodeOptionHandle`类
    • 构造函数
    • `NodeBase`类
    • `StreamerNodeBase`、`FormatorNodeBase`和`EstimatorNodeBase`类
  • `NodeHandle`类
    • 构造函数
  • 总结

主函数

gici-open的命令输入非常简单,只需要一个配置文件。函数一进来就是用yaml来加载配置文件

yaml_node = YAML::LoadFile(config_file_path);

之后就是初始化glog日志库,如果配置文件中有设置logging节点,并且值是true的话就初始化glog

yaml_node["logging"].IsDefined() && 
option_tools::safeGet(yaml_node["logging"], "enable", &enable_logging) && 
enable_logging == true

其中涉及到了safeGet这个函数,这个函数就是来判断配置文件的node里有没有你要的那个关键字,有的话再把对应的值返回。后边读各种节点都用到了这个

/* 判断node里有没有key,有的话返回到value里 */
template<typename ValueType> bool safeGet(const YAML::Node& node, const std::string& key, ValueType* value)

接下来的一个函数看起来是对两种Linux异常的情况处理,参考了一下这个SIGINT SIGPIPE SIGTERM SIGSEGV SIG_IGN产生原因及处理,总结下来加自己的理解就是

  • SIGPIPE信号:尝试向一个已经关闭的socket写入数据时
  • SIGSEGV信号:向不存在的内存写数据,就是经典的段错误
initializeSignalHandles(); /* 主要用来正确处理SIGPIPE信号和SIGSEGV信号的异常情况 */

接下来就是来对读取到的yaml_node处理,用智能指针初始化了一个NodeOptionHandle类,作者给的注释是Mainly used for organizing the relationship between nodes.,也就是管理各个node之间的关系

NodeOptionHandlePtr node_option_handle = std::make_shared<NodeOptionHandle>(yaml_node);

然后就进一步的初始化各个节点,以及完成了子线程的创建

std::unique_ptr<NodeHandle> node_handle =           /* 这里进来读配置文件 */
    std::make_unique<NodeHandle>(node_option_handle); /* 同时,这里会创建streamer和estimate的线程 */

接下来就是把读取到的配置文件信息又cout了一下,然后涉及到了SpinControl这个类,spin应该就是自旋锁(自旋等待),以下内容来自ChatGPT

自旋锁是一种线程同步的机制,用于保护共享资源的访问。当一个线程尝试获取自旋锁时,如果锁已被其他线程占用,则该线程会一直在自旋锁上自旋等待,不会进行阻塞。它会不断地检查锁是否可用,直到获取到锁为止。这种自旋等待的方式避免了线程切换的开销,适用于对共享资源的竞争情况短暂而频繁的场景。

所以这里也就解释了我上一篇博客中有人提出的问题了,gici-open在主函数里没有给出任何终止程序的代码,所以哪怕是运行结束了,也是在一直等待,需要自己手动退出

主函数整体写得非常简洁明了,接下来仔细看一下其中涉及到得的两个比较关键的类

NodeOptionHandle

这个类本身是用来管理各个node之间的关系的,里面包括了一个基类NodeBase以及继承自这个基类的几个子类,当然也有对应的成员变量

bool valid;                                     // 配置文件是否有效
YAML::Node replay_options;                      // replay节点
std::vector<NodeBasePtr> nodes;
std::vector<StreamerNodeBasePtr> streamers;     // streamers节点
std::vector<FormatorNodeBasePtr> formators;     // 对应的formators
std::vector<EstimatorNodeBasePtr> estimators;   // estimators节点
std::map<std::string, NodeBasePtr> tag_to_node; // 把tag和nide对应起来的变量

构造函数

它的构造函数就是对读进来的全部内容进行进一步的细分,比如对stream节点下的内容读取,以其中的streamers为例

  if (yaml_node["stream"].IsDefined()) 
  {
    const YAML::Node& stream_node = yaml_node["stream"];

    // Load streamers
    if (stream_node["streamers"].IsDefined()) {     // 有没有stream
      const YAML::Node& streamer_nodes = stream_node["streamers"];  // 把里面的streamers提出来
      for (size_t i = 0; i < streamer_nodes.size(); i++) {
        const YAML::Node& streamer_node = streamer_nodes[i]["streamer"];
        StreamerNodeBasePtr streamer = 
          std::make_shared<StreamerNodeBase>(streamer_node);  // 把每一个streamers再初始化成对应的StreamerNodeBase类
        streamers.push_back(streamer);  // 然后存在成员变量vector
        nodes.push_back(std::static_pointer_cast<NodeBase>(streamer));
        tag_to_node.insert(std::make_pair(nodes.back()->tag, nodes.back()));  // 再把node和tag对应起来
      }
    }
    ......
  }

然后来判断是否全部节点都是有效的

if (!checkAllNodeOptions()) { valid = false; return; }

接下来就是建立input_tagoutput_tag之间的联系

    for (auto& input_tag : nodes[i]->input_tags) {  // 第i个node的input_tag
      for (size_t j = 0; j < nodes.size(); j++) {   // 然后遍历所有的node
        if (nodes[j]->tag != input_tag) continue;   // 找到node和input_tag相同的node
        if (!tagExists(nodes[j]->output_tags, nodes[i]->tag)) { // 如果还没有对应的output_tag
          nodes[j]->output_tags.push_back(nodes[i]->tag); // 就加进去
        }
      }
    }

这里很容易搞混,可以再结合manual里的解释理解一下
gici-open学习日记(1):主函数以及读取配置文件_第1张图片
所以一个是数据从哪来,一个是数据送到哪

然后就是检查之前建立的联系对不对,以及配置文件本身有没有错误了

NodeBase

这个类是一个基础类,里面定义的变量主要都是各个节点都有的属性以及指向的Node和是否有效的标识符

    NodeType node_type;
    std::string tag;
    std::string type;
    std::vector<std::string> input_tags;
    std::vector<std::string> output_tags; 
    YAML::Node this_node;

构造函数没什么好说的,就是用yaml_node里把每一个节点对应的这些变量SafeGet

StreamerNodeBaseFormatorNodeBaseEstimatorNodeBase

三个继承类,就分别对应三种节点了。构造函数重要的作用就是把节点的类型定义了

node_type = NodeType::Streamer;

FormatorNodeBaseEstimatorNodeBase类还有特有的两个变量,分别是FormatorNodeBase控制数据输入输出类型的ioEstimatorNodeBase中对应到tag名的xxx_roles

// FormatorNodeBase
if (!option_tools::safeGet(yaml_node, "io", &io))
// EstimatorNodeBase
if (!option_tools::safeGet(yaml_node, option_name, &roles) || roles.empty())

NodeHandle

里面的成员变量就三个,给的注释很详细

  // Streaming threads, handles streamer and formators
  std::vector<std::shared_ptr<Streaming>> streamings_;

  // Estimating threads, handles estimators
  std::vector<std::shared_ptr<EstimatingBase>> estimatings_;

  // Data integration handles that pack data according to its roles and send them to estimators
  // The outter vector aligns to estimatings_, which describes the data destinations.
  // The inner vector aligns to the input nodes of corresponding estimator.
  std::vector<std::vector<std::shared_ptr<DataIntegrationBase>>> data_integrations_;

构造函数

先是根据streamers节点来来初始化Streaming线程(我的理解就是数据流的线程),里面关于具体的配置读取,是通过初始化Streaming类时候的构造函数实现的

  // Initialize streaming threads (formators are initialized together)
  for (size_t i = 0; i < nodes->streamers.size(); i++) {
    // check if ROS streamer
    std::string type_str = nodes->streamers[i]->type;
    StreamerType type;
    option_tools::convert(type_str, type);
    if (type == StreamerType::Ros) continue;  // ROS模式

    auto streaming = std::make_shared<Streaming>(nodes, i); // 这里是读streamers
    if (!streaming->valid()) continue;
    streamings_.push_back(streaming);
  }

Streaming的构造函数的构造函数里还会调用makeStreamermakeFormator两个函数来设置对应的StreamerTypeFormatorType,关于具体有哪些类型manual里都有
然后再初始化多传感器估计MultiSensorEstimating线程,里面也是有着和manual对应的配置的读取

  // Initialize estimator threads
  for (size_t i = 0; i < nodes->estimators.size(); i++) {
    std::string type_str = nodes->estimators[i]->type;
    EstimatorType type;
    option_tools::convert(type_str, type);

    if (type != EstimatorType::None) {
      auto estimating = std::make_shared<MultiSensorEstimating>(nodes, i);    // estimators
      estimatings_.push_back(estimating);
    }
  }

接下来建立了节点之间的联系

  // Bind streamer->streamer, streamer->formator->formator->streamer pipelines
  Streaming::bindLogWithInput();
  // Bind streamer->formator->estimator pipelines
  bindStreamerToFormatorToEstimator(nodes);
  // Bind estimator->formator->streamer pipelines
  bindEstimatorToFormatorToStreamer(nodes);
  // Bind estimator->estimator pipelines
  bindEstimatorToEstimator(nodes);

然后再去读replay相关的内容
之后,就是创建了两个线程(start函数里会创建新的线程**)

  • streaming:数据流的线程
  • estimationg:估计器的线程
  // Start streamings
  for (size_t i = 0; i < streamings_.size(); i++) {
    streamings_[i]->start();
  }

  // Start estimators
  for (size_t i = 0; i < estimatings_.size(); i++) {
    estimatings_[i]->start();
  }

总结

其实花了两天,都还没有看到核心算法的部分。这里读配置文件的思路和结构十分严谨,主要就是把整个配置文件分成三个部分,然后我的理解是三个部分之间会通过一些关键字(比如taginput_tag)等联系起来,这样在后边无论是数据流读写还是状态估计的部分都是能对应上的。里面其实也有没有完全理解的部分,有时间再说吧。

你可能感兴趣的:(gici-open,学习,c++,计算机视觉)