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产生原因及处理,总结下来加自己的理解就是
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_tag
和output_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里的解释理解一下
所以一个是数据从哪来,一个是数据送到哪
然后就是检查之前建立的联系对不对,以及配置文件本身有没有错误了
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
到
StreamerNodeBase
、FormatorNodeBase
和EstimatorNodeBase
类三个继承类,就分别对应三种节点了。构造函数重要的作用就是把节点的类型定义了
node_type = NodeType::Streamer;
而FormatorNodeBase
和EstimatorNodeBase
类还有特有的两个变量,分别是FormatorNodeBase
控制数据输入输出类型的io
和EstimatorNodeBase
中对应到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
的构造函数的构造函数里还会调用makeStreamer
和makeFormator
两个函数来设置对应的StreamerType
和FormatorType
,关于具体有哪些类型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
函数里会创建新的线程**)
// 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();
}
其实花了两天,都还没有看到核心算法的部分。这里读配置文件的思路和结构十分严谨,主要就是把整个配置文件分成三个部分,然后我的理解是三个部分之间会通过一些关键字(比如tag
,input_tag
)等联系起来,这样在后边无论是数据流读写还是状态估计的部分都是能对应上的。里面其实也有没有完全理解的部分,有时间再说吧。