解析ros init(三)

param::init

Parameter是ROS系统运行所定义的全局变量,它由master节点的parameter server基于XML-RPC负责维护。它是全局可见的,因此可以运行时修改。ROS的namespace使得参数的命名具备非常清晰的层级结构,避免他们之间的冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//./src/ros_comm/roscpp/src/libros/param.cpp
void init(const M_string& remappings)
{
M_string::const_iterator it = remappings.begin();//remappings变量的头元素
M_string::const_iterator end = remappings.end();//remappings变量的末元素
for (; it != end; ++it)//依次遍历remappings变量的所有元素
{
const std::string& name = it->first;//提取键
const std::string& param = it->second;//提取值

if (name.size() < 2) //跳过键的长度小于2的元素
{
continue;
}
if (name[0] == '_' && name[1] != '_')//如果键以“__”开头
{
//为name赋予一个本地名称,用符号"~"代替“__”
std::string local_name = "~" + name.substr(1);
bool success = false;

try
{
int32_t i = boost::lexical_cast<int32_t>(param); //尝试将param转化成整型
//将local_name规整化
ros::param::set(names::resolve(local_name), i);
success = true;//将成功标志置上
}
catch (boost::bad_lexical_cast&)
{

}
if (success) //如果成功标志已被置上,则越过后续过程
{
continue; //此时,即param成功被转化为整型
}

try
{
//没能转化为整型,尝试将param转化成浮点型
double d = boost::lexical_cast<double>(param);
//将local_name规整化
ros::param::set(names::resolve(local_name), d);
success = true; //将成功标志置上
}
catch (boost::bad_lexical_cast&)
{

}

if (success) //如果成功标志已被置上,则越过后续过程
{
continue; //此时,即param成功被转化为浮点型
}
// 处理param为布尔型或其他的情况
if (param == "true" || param == "True" || param == "TRUE")
{
ros::param::set(names::resolve(local_name), true);
}
else if (param == "false" || param == "False" || param == "FALSE")
{
ros::param::set(names::resolve(local_name), false);
}
else
{
ros::param::set(names::resolve(local_name), param);
}
}
}

XMLRPCManager::instance()->bind("paramUpdate", paramUpdateCallback);
}

ros::param::set()函数的定义也在文件./src/ros_comm/roscpp/src/libros/param.cpp中,有一系列的重载函数:

1
2
3
4
5
6
void set(const std::string& key, const XmlRpc::XmlRpcValue& v) 
void set(const std::string& key, const std::string& s)
void set(const std::string& key, const char* s)
void set(const std::string& key, double d)
void set(const std::string& key, int i)
void set(const std::string& key, bool b)

以转化为整型的重载函数为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void set(const std::string& key, const std::map<std::string, int>& map)
{
setImpl(key, map);
}

// 显然这个函数为模板函数比较合适,适用于多种情况
// 将第二个参数转化成相应了XmlRpcValue类型,然后调用第一个重载函数
template <class T>
void setImpl(const std::string& key, const std::map<std::string, T>& map)
{
// XmlRpcValue starts off as "invalid" and assertStruct turns it into a struct type
XmlRpc::XmlRpcValue xml_value;
xml_value.begin();

// Copy the contents into the XmlRpcValue
for(typename std::map<std::string, T>::const_iterator it = map.begin();
it != map.end(); ++it)
{
xml_value[it->first] = it->second;
}
ros::param::set(key, xml_value);
}

set第一个重载如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void set(const std::string& key, const XmlRpc::XmlRpcValue& v)
{
std::string mapped_key = ros::names::resolve(key);

XmlRpc::XmlRpcValue params, result, payload;
params[0] = this_node::getName();
params[1] = mapped_key;
params[2] = v;
{
//Lock around the execute to the master in case we get a parameter update on this value between executing on the master and setting the parameter in the g_params list
boost::mutex::scoped_lock lock(g_params_mutex);

if (master::execute("setParam", params, result, payload, true))
{
// Update our cached params list now so that if get() is called immediately after param::set()
// we already have the cached state and our value will be correct
if (g_subscribed_params.find(mapped_key) != g_subscribed_params.end())
{
g_params[mapped_key] = v;
}
invalidateParentParams(mapped_key);
}
}
}

函数master::execute(“setParam”, params, result, payload, true)用于在master(节点管理器)上执行XML-RPC通信机制。

1
2
3
4
5
6
bool ros::master::execute(const std::string&  method,
const XmlRpc::XmlRpcValue & request,
XmlRpc::XmlRpcValue & response,
XmlRpc::XmlRpcValue & payload,
bool wait_for_master
)

  • method:要调用的 RPC 方法
  • request:The arguments to the RPC call //传递给RPC的参数
  • response:[out] The resonse that was received. //接收到的回应
  • payload: [out] The payload that was received
  • wait_for_master: //是否一直循环等待与master建立连接

函数的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
bool execute(const std::string& method, const XmlRpc::XmlRpcValue& request, XmlRpc::XmlRpcValue& response, XmlRpc::XmlRpcValue& payload, bool wait_for_master)
{
ros::WallTime start_time = ros::WallTime::now();

std::string master_host = getHost(); //获取g_host的值
uint32_t master_port = getPort(); //获取g_port的值
//根据 master_host, master_port 获取XMLRPC通信的客户端,这两个一般是根据环境变量所得
XmlRpc::XmlRpcClient *c = XMLRPCManager::instance()->getXMLRPCClient(master_host, master_port, "/");
bool printed = false;
bool slept = false;
bool ok = true;
bool b = false;
do
{
{
#if defined(__APPLE__)
boost::mutex::scoped_lock lock(g_xmlrpc_call_mutex);
#endif
//c是根据master_host, master_port的值获取XMLRPC通信的客户端指针(XmlRpc::XmlRpcClient *c)
// 循环不断执行execute,以保持和master的通信
b = c->execute(method.c_str(), request, response);
}
ok = !ros::isShuttingDown() && !XMLRPCManager::instance()->isShuttingDown();

if (!b && ok)
{
if (!printed && wait_for_master)
{
ROS_ERROR("[%s] Failed to contact master at [%s:%d]. %s", method.c_str(), master_host.c_str(), master_port, wait_for_master ? "Retrying..." : "");
printed = true;
}
if (!wait_for_master)
{
XMLRPCManager::instance()->releaseXMLRPCClient(c);
return false;
}

if (!g_retry_timeout.isZero() && (ros::WallTime::now() - start_time) >= g_retry_timeout)
{
ROS_ERROR("[%s] Timed out trying to connect to the master after [%f] seconds", method.c_str(), g_retry_timeout.toSec());
XMLRPCManager::instance()->releaseXMLRPCClient(c);
return false;
}
ros::WallDuration(0.05).sleep();
slept = true;
}
else
{
if (!XMLRPCManager::instance()->validateXmlrpcResponse(method, response, payload))
{
XMLRPCManager::instance()->releaseXMLRPCClient(c);
return false;
}
break;
}
// 不断执行循环,除非调用ros::shutdown() 或 XMLRPCManager::shutdown()
ok = !ros::isShuttingDown() && !XMLRPCManager::instance()->isShuttingDown();
} while(ok);

if (ok && slept)
{
ROS_INFO("Connected to master at [%s:%d]", master_host.c_str(), master_port);
}
XMLRPCManager::instance()->releaseXMLRPCClient(c);
return b;
}

函数主要就是基于XML-RPC进行setParam的远程调用,需要不断判断连接状态

每个节点其实就是个XML-RPC服务端,在运行ros::start()时执行了XMLRPCManager::instance()->start();——XmlRpcServer::bindAndListen,在这个函数中又开启了一个线程依次调用:XMLRPCManager::serverThreadFunc——server_.work(0.1);,服务端的两个关键函数完成了。


总结

init()其实做很少工作,主要就是解析一下环境和命令行参数,init()不允许参与实际的连接,因而,用户可以手动检测像是master启动没有,有没有按自定义的行为启动这样的事


Boost教程(四)读写JSON

Boost读写JSON用的是property_tree模块,这个模块不用加到find_package里,它没有库文件,直接include头文件即可.但用法总体上不如Qt的JSON模块好用。

今天发现用Boost写JSON有个大问题,就是数值类型和bool最终都被转化为字符串,无法避免。所以最好不要用Boost读写JSON了

写JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>

const std::string file_path="/home/user/test.txt";

boost::property_tree::ptree root;
boost::property_tree::ptree items;

boost::property_tree::ptree item1;
item1.put("ID","1");
item1.put("Name","wang");
items.push_back(std::make_pair("1",item1));

boost::property_tree::ptree item2;
item2.put("ID","2");
item2.put("Name","zhang");
items.push_back(std::make_pair("2",item2));

root.put_child("user",items);
boost::property_tree::write_json(file_path,root);

这样写出的JSON如下:

1
2
3
4
5
6
7
{
"user": {
"1": { "ID": "1","Name": "wang"},
"2": { "ID": "2","Name": "zhang"},
"3": { "ID": "3", "Name": "li"}
}
}

把最后两句改一下:

1
boost::property_tree::write_json(file_path,item1);

结果如下:
1
2
3
4
{
"ID": "1",
"Name": "wang"
}

把最后两句再这样改:

1
2
root.put_child("user",item1);
boost::property_tree::write_json(file_path,root);

结果如下:
1
2
3
4
5
6
{
"user": {
"ID": "1",
"Name": "wang"
}
}

修改某个JSON值

1
2
3
4
json::ptree pt;
pt.put("data",1);
pt.put("num",2);
pt.get_child("num").put_value(9);

JSON数组

实现JSON数组还是只用ptree类型,稍微复杂点,之前因为make_pair而一直以为用别的方法,其实是让paire的key为空即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace json = boost::property_tree;

json::ptree pt;
json::ptree children;
json::ptree child1, child2, child3;

child1.put("", 1);
child2.put("", 2);
child3.put("", 3);

children.push_back(std::make_pair("", child1));
children.push_back(std::make_pair("", child2));
children.push_back(std::make_pair("", child3));

pt.add_child("MyArray", children);

std::stringstream stream;
json::write_json(stream, pt);
cout<< stream.str() <<endl;

运行结果:
1
2
3
4
5
6
7
{
"MyArray": [
"1",
"2",
"3"
]
}

同样结果,代码还可以优化成这样:

1
2
3
4
5
6
json::ptree child;
for(int i=0;i<3;i++)
{
child.put("", i);
children.push_back(std::make_pair("", child));
}

child的内容会依次更新,逐个都插入children

将json写入string

1
2
3
std::stringstream stream;
boost::property_tree::write_json(stream, item1);
cout<<stream.str()<<endl;

不能直接用cout<<,没有重载运算符,不过这样也比较简单

从string中解析json串

1
2
3
4
5
6
7
8
// json是 { "a": 2}
std::string c; //c为json串
std::istringstream iss;
iss.str(c);

boost::property_tree::ptree item;
boost::property_tree::json_parser::read_json(iss, item);
int n = item.get<int>("a");

使用第三方win10主题

win10自带的主题就那几个,希望能用一些好看的,但Win10原生是不支持第三方主题的,需要特别之后才可以用

需要先安装一个叫UltraUXThemePatcher的软件,下载在这里

安装时要注意,软件版本应当和系统版本基本一致,否则会出问题

  1. 右键用管理员身份运行 UltraUXThemePatcher,然后点击 Install,开启安装。

  2. 重启电脑,打开Cortana,打开创建还原点程序,这个应当是win10的新程序,应当是与VMWare的还原点类似的。打开后选C盘,先到配置启用系统保护,然后创建。因为主题文件偶尔会出问题,所以最好有还原点。

  3. (可选)安装OldNewExplore,让Windows 10的资源管理器变成Windows 7风格(有些主题需要)

  4. 把下载的第三方主题放到C:\Windows\Resources\Themes文件夹

  5. 到 桌面-右键-个性化-主题 里面选择主题。

效果:

参考:
第三方主题
视频指导使用第三方主题


Boost教程(三)多线程

使用Windows写Boost多线程时,还需要先编译Boost源码产生库文件,我试了几次都没成功,就懒得在Windows上编程了。

在Linux上,Boost使用多线程还得用到filesystem和system模块,否则会报错,可能是thread模块用到了它们,因此cmake这样写

1
2
3
4
5
6
find_package(Boost COMPONENTS filesystem system thread REQUIRED)

add_executable(${PROJECT_NAME} "main.cpp" )
target_link_libraries(untitled ${Boost_FILESYSTEM_LIBRARY}
${Boost_SYSTEM_LIBRARY}
${Boost_THREAD_LIBRARY})

头文件只需要#include <boost/thread.hpp>

对于ROS环境,库文件已经包含在${catkin_LIBRARIES}当中

join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void thread_1(int n)
{
cout<<"slot1: "<<n<<endl;
}

void thread_2(int n)
{
cout<<"slot2: "<< 2*n<<endl;
}

int main()
{
int n = 13;
boost::thread th_1 = boost::thread(boost::bind(&thread_1,n));
th_1.join();
boost::thread th_2 = boost::thread(boost::bind(&thread_2,n));
th_2.join();

cout<<endl<<"end"<<endl;
return 0;
}

没有两个join()会有多种结果:

1
2
3
slot2: 26end

slot1: 13

1
2
3
slot1: end
13
slot2: 26
1
2
3
4
slot1: slot2: 13
26

end

可见几个线程的执行顺序乱套了,给两个线程都加join()才确定顺序是 线程1-线程2-主线程,如果只加一个,剩下两个线程的顺序还是不确定。

boost::condition

程序如下,main函数里要把上面两个线程的join交换一下,先执行线程2,这样在线程2里的随机数如果大于90,会唤醒线程1。如果先执行线程1,会一直阻塞,程序没法向下执行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
boost::mutex mut;
boost::condition cond;
boost::mutex::scoped_lock _lock(mut);

void thread_1(int n)
{
while(true)
{
cond.wait(_lock);
cout<< "thread 1: "<<rand()%50 <<endl;
sleep(1);
}
}

void thread_2(int n)
{
int m;
while(true)
{
m = 50 + rand()%50;
cout<< "thread 2: "<< m <<endl;
if(m>90)
{
cond.notify_one();
cout<<"thread 2 wake thread 1 ";
}
sleep(1);
}
}

某次的测试结果:

1
2
3
4
5
6
7
thread 2: 65
thread 2: 93
thread 2 wake thread 1 thread 1: 35
thread 2: 86
thread 2: 92
thread 2 wake thread 1 thread 1: 49
thread 2: 71

atomic 无锁编程

多个线程之间共享地址空间,所以多个线程共享进程中的全局变量和堆,都可以对全局变量和堆上的数据进行读写,但是如果两个线程同时修改同一个数据,可能造成某线程的修改丢失;如果一个线程写的同时,另一个线程去读该数据时可能会读到写了一半的数据。这些行为都是线程不安全的行为,会造成程序运行逻辑出现错误。下面的程序很常见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// boost::atomic<int> i(0);
int i=0;
boost::mutex mut;
void thread_1(int n);

int main()
{
int n=100000; // n不够大时,i不容易出现不同的情况
boost::thread th1 = boost::thread(boost::bind(&thread_1,n));
boost::thread th2 = boost::thread(boost::bind(&thread_1,n));
th1.join();
th2.join();
cout<< i<<endl;
return 0;
}

void thread_1(int n)
{
while(n--)
{
mut.lock();
i++;
mut.unlock();
}
}

如果不加lock,i最终值是不确定的,因为两个线程同时对i进行了写操作。一般地,我们用互斥锁mutex保护临界区,保证同一时间只能有一个线程可以获取锁,持有锁的线程可以对共享变量进行修改,修改完毕后释放锁,而不持有锁的线程阻塞等待直到获取到锁,然后才能对共享变量进行修改,最后i必定为200000

Boost提供了原子类型atomic,通过使用原子类型可摆脱每次对共享变量进行操作都进行的加锁解锁动作,节省了系统开销,同时避免了线程因阻塞而频繁的切换。atomic封装了不同计算机硬件的底层操作原语,提供了跨平台的原子操作功能,解决并发竞争读写变量的困扰,只需要包含文件<boost/atomic.hpp>,在上面的代码中使用boost::atomic<int> i(0);,然后去掉函数中的互斥锁,运行效果是一样的,而且节省了系统开销。

atomic可以把对类型T的操作原子化,T的要求:

  1. 标量类型(算数,枚举,指针)
  2. POD类型,可以使用memcmp,memset等函数

两种方式创建atomic对象:

1
2
3
4
5
6
atomic<int> a(10);
assert(a==10); //安全函数,若表达式不成立结束程序


atomic<long> L;
cout << L <<endl; //初始值不确定

最重要的两个成员函数: store() (operator=) 和 load() (operator T() )以原子方式存取,不会因为并发访问导致数据不一致。

1
2
3
4
5
6
7
8
9
boost::atomic<bool> b(1);
assert(b != 0);
std::cout << b << std::endl;
b.store(0);//存值
std::cout << b << std::endl;

boost::atomic<int> n1(100);
std::cout << n1.exchange(200) << std::endl; //交换两个值,并且返回原值100
std::cout << n1 << std::endl;

测试代码中临界区非常短,只有一个语句,所以显得加锁解锁操作对程序性能影响很大,但在实际应用中,我们的临界区一般不会这么短,临界区越长,加锁和解锁操作的性能损耗越微小,无锁编程和有锁编程之间的性能差距也就越微小。

无锁编程最大的优势在于两点:

  • 避免了死锁的产生。由于无锁编程避免了使用锁,所以也就不会出现并发编程中最让人头疼的死锁问题,对于提高程序健壮性有很大积极意义
  • 代码更加清晰与简洁

参考:C++11多线程编程


Boost教程(二)文件系统

首先在cmake中使用Boost的filesystem模块:

1
2
3
4
5
6
find_package(Boost COMPONENTS system filesystem REQUIRED)

target_link_libraries(mytarget
${Boost_FILESYSTEM_LIBRARY}
${Boost_SYSTEM_LIBRARY}
)

必须得加上system模块

filesystem库提供了两个头文件,一个是,这个头文件包含主要的库内容。它提供了对文件系统的重要操作。同时它定义了一个类path,正如大家所想的,这个是一个可移植的路径表示方法,它是filesystem库的基础。

filesystem在任何时候,只要不能完成相应的任务,它都可能抛出 basic_filesystem_error异常,当然并不是总会抛出异常,因为在库编译的时候可以关闭这个功能。

路径的创建很简单,仅仅需要向类boost::filesystem::path()的构造器传递一个string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    filesystem::path cur_path = filesystem::current_path();
filesystem::path parent_path = cur_path.parent_path();
//path支持重载/运算符,这个很好用
filesystem::path file_path = cur_path/"test";
cout<<"curren path: "<<cur_path<<endl;
cout<<"parent path: "<<parent_path<<endl;
if(filesystem::exists(file_path)) // 适合判断文件的存在
cout<<"exists test "<<endl;

filesystem::path filePath = "/home/user/yaml";
cout<<fs::is_directory(filePath)<<endl;
cout<<fs::is_empty(filePath)<<endl; // 判断是否为空

// unsigned long int, byte
cout<<"file test size: "<<filesystem::file_size(file_path)<<endl;
// filesystem::remove(file_path);
filesystem::rename(file_path, cur_path/"newTest");

filesystem::path p("/home/user/ost.yaml");
if(fs::exists(p)) // Boost缺陷,若文件不存在,两函数也能正常输出
{
cout<<p.extension()<<endl; // "yaml"
cout<<p.stem()<<endl; // 文件名,不带扩展名
}
// 删除文件,失败会强行结束:terminate called after throwing an instance of 'boost::filesystem::filesystem_error' 所以要用 try catch throw
try
{
boost::filesystem::remove(p);
}
catch( const boost::exception & e )
{
cout<< "remove error"<<endl;
throw;
// return -1;
}
filesystem::create_directory(cur_path/"dir");
cout<<"is dir: "<<filesystem::is_directory(cur_path/"dir")<<endl;

递归获取某文件夹中符合某扩展名的所有文件名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace fs = boost::filesystem;

std::vector<fs::path> getFileNames(fs::path p, std::string extension)
{
std::vector<fs::path> names;

if(!fs::is_directory(p) )
return names;

if(fs::is_empty(p))
return names;

fs::recursive_directory_iterator iter(p); //迭代目录下的所有文件
fs::recursive_directory_iterator end_iter; // 只接就是end iterator
if( !p.empty() && fs::exists(p))
{
for(; iter!= end_iter;iter++)
{
try{
if (fs::is_directory( *iter ) )
{
// std::cout<<*iter << "is dir" << std::endl;
}
else
{
// std::cout<<iter->path().stem() <<endl;
// std::cout<<iter->path().extension() <<endl;
// std::cout << "full name: "<<*iter << endl;
if(iter->path().extension() == extension)
{
names.push_back(iter->path().stem());
}
}
}
catch ( const std::exception & ex ){
std::cerr << ex.what() << std::endl;
continue;
}
}
}
return names;
}

// 使用
std::vector<fs::path> names = getFileNames("/home/user/yaml", ".yaml");
if(names.empty())
{
cout<<"It's empty or not a dir"<<endl;
return -1;
}
else
{
for(int i=0;i<names.size();i++)
cout<<"name: "<<names.at(i)<<endl;
}


Git常用命令

使用Git的大忌:

  • 没什么有价值的修改就提交,也就是没有意义的提交
  • 提交到别人的分支
  • 提交的代码又引入了新的更严重更明显的bug,也就是不如不提交。比第一条更严重,让别人也无法合并

如果git下载不成功,试试下面的代理

1
git clone https://mirror.ghproxy.com/https://github.com/stilleshan/dockerfiles

wget 和 curl 同理
1
2
wget https://mirror.ghproxy.com/https://github.com/stilleshan/dockerfiles/archive/master.zip
curl -O https://mirror.ghproxy.com/https://github.com/stilleshan/dockerfiles/archive/master.zip

在本地建立仓库到push的过程:

1
2
3
4
5
git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/rjosodtssp/Design-Modes.git
git push -u origin master

提交会附带消息和一个哈希值,哈希值是一串包含数字和字母的随机序列。

生成公钥

单独push某文件

1
2
3
git commit test.cpp
//在vim中输入更新信息
git push origin master

删除Repository

在GitHub网站上操作,打开要删除的Repository,点 Settings

Commit message 用 emoji

初次提交示例:

1
git commit -m ":tada: Initialize Repo"

🆕 (全新) :new: 引入新功能
(闪电) :zap: 提升性能
🔖 (书签) :bookmark: 发行/版本标签
🐛 (bug) :bug: 修复 bug
(急救车) :ambulance: 重要补丁

(扳手) :wrench: 修改配置文件
(加号) :heavy_plus_sign: 增加一个依赖
➖ (减号) :heavy_minus_sign: 减少一个依赖
(备忘录) :memo: 撰写文档
(锤子) :hammer: 重大重构
🔥 (火焰) :fire: 移除代码或文件

撤销对文件的修改

test.cpp 修改后,打算返回上次提交的状态,使用命令:

1
git checkout -- qclipper.cpp

如果要撤销 所有文件 的修改,命令为:
1
git checkout -f

git diff ReadMe.mdown 表示在commit之前查看文件修改哪些地方, 在commit之后使用此命令无效。

避免每次提交时登录

如果我们git clone的下载代码的时候是连接的https://而不是git@git (ssh)的形式,当我们操作git pull/push到远程的时候,总是提示我们输入账号和密码才能操作成功,频繁的输入账号和密码会很麻烦。

解决办法:git bash进入你的项目目录,输入

1
git config --global credential.helper store

然后你会在你本地生成一个文本,上边记录你的账号和密码。不用关心在哪,使用上述的命令配置好之后,再操作一次git pull,然后它会提示你输入账号密码,这一次之后就不需要再次输入密码了。

在Github找到以前提交的版本

点击commits
点击红圈的标志,进入旧版本的repository,然后可以下载

Alt text

重命名文件

1
2
git mv file.cpp new_file.cpp
git push origin master

删除远程库的某文件

1
2
git rm file.cpp
git commit

从本地库中删除了,再push就可以从远程库删除

从远程仓库拉取

1
2
git init
git clone

修改commit message

  上一次的message如果需要修改,使用:git commit --amend 如果上一次的commit已经push了,那么需要强制提交 git push -f origin master

修改别人的代码

先到别人的项目上fork到自己的GitHub下,clone下来以后进行修改,push到自己的项目。

在自己的项目页面pull request,把我的修改发到对方的项目里,GitHub同时给对方发了邮件,由对方决定是否接受修改。

注意:不要在对方的项目页面clone

查看当前用户名和邮箱

1
2
git config user.name
git config user.email

修改用户名和地址,不加global只能生效一次

1
2
git config --global user.name "your name"
git config --global user.email "your email"

打标签

Git 中的tag指向一次commit的id,通常用来给开发分支做一个标记,如标记一个版本号。

1
git tag -a v1.01 -m "Relase version 1.01"

注解:git tag 是打标签的命令,-a 是添加标签,其后要跟新标签号,-m 及后面的字符串是对该标签的注释。

gitignore

gitignore在本地仓库的根目录,执行文件屏蔽 屏蔽规则

问题累计

对git本地仓库,使用rm而不是git rm删除一个文件A后,直接commit 和 push,远程仓库仍然有A。此时已经不能再执行git rm了,所以将删除动作添加到暂存区 git add .,然后再commit 和 push
操作过程


git add 报错: fatal: in unpopulated submodule

先执行 git rm --cached . -rf,再 git add


github push 代码出现fatal: Authentication failed for

2021.8月份开始远程登录不在支持使用账户密码的方式,解决方法: 把git remote add origin https://github.com/xxx/xxx.git 换成 git remote set-url origin https://<your_token>@github.com/<USERNAME>/<REPO>.git

github获取token



解析ros init(二)

this_node::init

命名空间names是在这里初始化的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
void init(const std::string& name, const M_string& remappings, uint32_t options)
{
ThisNode::instance().init(name, remappings, options);
}
// ThisNode是一个类,instance()是单例模式
static ThisNode& instance()
{
static ThisNode singleton;
return singleton;
}

// 主要实现在这里
void ThisNode::init(const std::string& name, const M_string& remappings, uint32_t options)
{
char *ns_env = NULL;
#ifdef _MSC_VER
_dupenv_s(&ns_env, NULL, "ROS_NAMESPACE");
#else
ns_env = getenv("ROS_NAMESPACE");
#endif

if (ns_env) //如果获得环境变量
{
namespace_ = ns_env; // namespace_是成员变量
#ifdef _MSC_VER
free(ns_env);
#endif
}
if (name.empty()) { // 节点名不能为空
throw InvalidNameException("The node name must not be empty");
}

name_ = name; // 节点名赋值,name_也是类成员变量,初始值为"empty"

bool disable_anon = false;
//在输入参数remappings查找键为"__name"的项
M_string::const_iterator it = remappings.find("__name");
if (it != remappings.end())
{
name_ = it->second;
disable_anon = true;
}
//在输入参数remappings查找键为"__ns"的项
it = remappings.find("__ns");
if (it != remappings.end())
{
namespace_ = it->second;
}
// 这里可以看出ROS_NAMESPACE不是必要的
if (namespace_.empty())
{
namespace_ = "/";
}
// 如果在上面赋值为 / ,最终就是 / ;否则就是 /+namespace
namespace_ = (namespace_ == "/")
? std::string("/")
: ("/" + namespace_)
;

std::string error;
// 对命名空间进行验证,这肯定又是个字符串处理函数
// 检查首字符,首字符只能是~ / 或 alpha,逐个检查name中的每个字符是否为合法字符
if (!names::validate(namespace_, error))
{
std::stringstream ss;
ss << "Namespace [" << namespace_ << "] is invalid: " << error;
throw InvalidNameException(ss.str());
}

// names must be initialized here, because it requires the namespace to already be known so that it can properly resolve names.
// It must be done before we resolve g_name, because otherwise the name will not get remapped.

// 将remappings映射为g_remappings和g_unresolved_remappings两个变量
names::init(remappings);
// 节点名不能含有 / 和 ~
if (name_.find("/") != std::string::npos)
{
throw InvalidNodeNameException(name_, "node names cannot contain /");
}
if (name_.find("~") != std::string::npos)
{
throw InvalidNodeNameException(name_, "node names cannot contain ~");
}
// 简单的格式化操作
name_ = names::resolve(namespace_, name_);
// 如果初始化时的options选择的时匿名节点,那么在节点名后加UNIX时间戳,单位是纳秒
if (options & init_options::AnonymousName && !disable_anon)
{
char buf[200];
// 其实是ros::WallTime::now() 返回当前时间的纳秒表示
snprintf(buf, sizeof(buf), "_%llu", (unsigned long long)WallTime::now().toNSec());
name_ += buf;
}
//把节点和名字联系起来
ros::console::setFixedFilterToken("node", name_);
}

最后的函数是:

1
2
3
4
void setFixedFilterToken(const std::string& key, const std::string& val)
{
g_extra_fixed_tokens[key] = val;
}

主要是对变量g_extra_fixed_tokens进行赋值

file_log::init

file_log就是个命名空间,函数定义在./src/ros_comm/roscpp/src/libros/file_log.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
void init(const M_string& remappings)
{
std::string log_file_name;
M_string::const_iterator it = remappings.find("__log");
//在remappings中找到键为"__log"的项
if (it != remappings.end())
{
log_file_name = it->second; //如果找到了,将对应的值赋值给log_file_name
}

{
// Log filename can be specified on the command line through __log
// If it's been set, don't create our own name
if (log_file_name.empty())
{
// Setup the logfile appender
// Can't do this in rosconsole because the node name is not known
pid_t pid = getpid(); //获取当前进程号
std::string ros_log_env;
//获取"ROS_LOG_DIR"
if ( get_environment_variable(ros_log_env, "ROS_LOG_DIR"))
{
log_file_name = ros_log_env + std::string("/");
}
else //如果不存在"ROS_LOG_DIR"这个环境变量
{ //获取"ROS_HOME"的环境变量值
if ( get_environment_variable(ros_log_env, "ROS_HOME"))
{
log_file_name = ros_log_env + std::string("/log/");
}
else //如果不存在环境变量"ROS_HOME"
{
// 无法跨平台?
// 如果没有设置以上环境变量,日志最终放在 ~/.ros/log
if( get_environment_variable(ros_log_env, "HOME") )
{
std::string dotros = ros_log_env + std::string("/.ros/");
fs::create_directory(dotros);
log_file_name = dotros + "log/";
fs::create_directory(log_file_name);
}
}
}

// log_file_name是完整路径,这里是取 文件名=节点名_
for (size_t i = 1; i < this_node::getName().length(); i++)
{
if (!isalnum(this_node::getName()[i]))
{
log_file_name += '_';
}
else
{
log_file_name += this_node::getName()[i];
}
}
// 变成了 节点名_pid_log
char pid_str[100];
snprintf(pid_str, sizeof(pid_str), "%d", pid); //将pid以整形变量的形式写入pid_str
log_file_name += std::string("_") + std::string(pid_str) + std::string(".log");
}
// 赋值
log_file_name = fs::system_complete(log_file_name).string();
g_log_directory = fs::path(log_file_name).parent_path().string();
}
}

这个函数其实就是确定日志存放目录和日志名称,例如:

问题是为什么有的节点最后还要加上时间戳?

参考:
ros time.h


解析cout,cerr,clog

clog和cerr主要用于错误输出。

  • cout 写入标准输出流 stdout,有缓冲
  • cerr 写入标准错误流 stderr,无缓冲
  • clog 写入标准错误流 stderr,有缓冲

std::endl的含义

常常有 std::cout<<"test"<<std::endl,这是把test先放到标准输出流,cout会对内容进行缓冲,不会立即输出到显示器.有两种方法立即显示:加flush或endl,后者还要换行,这是在缓冲区不满时刷新.有时不加这两个关键字也能显示,是因为缓冲区满了或者长时间未输入.

precision

1
2
3
cout.precision(3);
cout<<123.567890<<endl;  // 124
cout<<scientific<<123.567890<<endl; // 1.236e+02

precision是控制输出浮点数的精度,3表示四舍五入后得到3个有效数字.精度数字超出数字个数时,还按原来数字.

scientific表示科学计数法表示,此时精度数字是小数点位数


欧拉角,旋转矩阵,旋转向量,四元数

欧拉角

参考欧拉角的理解

对于在三维空间里的一个参考系,任何坐标系的取向,都可以用三个欧拉角来表现。参考系又称为实验室参考系,是静止不动的,而刚体坐标系则固定于刚体,随着刚体的旋转而旋转。

规定:XYZ坐标轴是旋转的刚体坐标轴;而xyz坐标轴是静止不动的实验室参考轴。

坐标系XYZ原与参考系xyz重合,旋转后,称xy平面与XY平面的交线为交点线,用英文字母N代表。zXZ顺规的欧拉角可以静态地定义如下:

  • α是x轴与交点线的夹角;

  • β是z轴与Z轴的夹角;

  • γ是交点线与X轴的夹角。


第一绕z轴旋转α,第二绕交点线(即X轴旋转后的轴)旋转β,第三绕Z轴旋转γ. 因此,此过程可分解为三个基本的旋转,从左到右依次代表绕着z轴的旋转、绕着交点线的旋转、绕着Z轴的旋转。即其旋转矩阵为

对于欧拉角,我们规定任何两个连续的旋转,必须是绕着不同的转动轴旋转。因此,一共有 12=3x2x2 种顺规。第一次旋转可以绕三个坐标轴中的任意一轴转动,有3种情况,第二次旋转可以绕除第一次旋转轴外的任意一轴转动,有2种情况,第三次旋转可以绕除第二次旋转轴外的任意一轴转动,有2种情况。

  • 经典欧拉角:z-x-z, x-y-x, y-z-y, z-y-z, x-z-x, y-x-y

  • 泰特-布莱恩角(Tait–Bryan angles):x-y-z, y-z-x, z-x-y, x-z-y, z-y-x, y-x-z

我们平时所说的roll, pitch, yaw 其实是泰特-布莱恩角,我简称之为TB角

可以看出两者的区别是:经典欧拉角的第一个旋转角度和第三个旋转角度都是围绕同一个轴的,而Tait-Bryan角使用三个轴上的旋转角度去表示

  • 内旋和外旋

同样的一个旋转,按旋转的坐标系又可分为内旋和外旋。

内旋是基于自身坐标系的旋转,旋转轴是动态的。内旋的旋转矩阵是按顺序右乘,

外旋是基于外部坐标系的旋转,旋转轴始终不变,也就是RPY。外旋的旋转矩阵是按顺序左乘。

所以欧拉角要知道旋转顺序和是否定轴旋转,而旋转矩阵和四元数则是一个姿态就对应的一个旋转矩阵或四元数。除了万向锁问题,这也是SLAM不常用欧拉角的另一个原因。

在使用欧拉角的场合,大多数情况下都是采取外旋 XYZ


转换

欧拉角和旋转矩阵

《SLAM十四讲》没有讲到这部分。坐标系A旋转到坐标系B,先绕Z轴旋转了yaw,这里遵循右手法则,大拇指朝坐标系方向,四指代表正向。或者说正向旋转是以沿X轴方向看,顺时针旋转为正向,否则为负向。那么相应的旋转矩阵为
绕坐标轴旋转的轴坐标是不会变化的,所以对应位置是1
之后绕旋转后坐标系的Y轴旋转了pitch,之后绕旋转后坐标系X轴旋转了roll,最终的旋转矩阵如下,即依次左乘相应的矩阵

典型举例

如图,大坐标系是base_link,小的是 camera

我们希望的坐标系关系是这样的

变化的过程是camera坐标系做内旋方式,也就是绕自身轴按照Z-Y-X的顺序旋转,即先绕自身轴Z,再绕自身轴Y,最后绕自身轴X,可得旋转矩阵(内旋是右乘)

判断欧拉角的正负: 使用右手系,旋转轴正方向面对观察者时,逆时针方向的旋转是正、顺时针方向的旋转是负。这里先绕Z轴转 -90°,再绕X轴转-90°,camera就能变成希望的样子。

最终的旋转矩阵

打开网站rotationconverter,在左上角的旋转矩阵输入计算的结果,在右下角出现XYZ对应的结果,而我们需要的是ZYX,即[ x: -90, y: 0, z: -90 ],这里并不是简单的把XYZ的结果倒序就行了。

我们要的欧拉角rpy就是 (-1.57, 0, -1.57)

旋转向量

旋转矩阵有几个缺点:SO(3)的旋转矩阵有9个量,但一次旋转只有3个自由度。因此,这种表达式冗余。而且对于旋转矩阵自身也有约束,它必须是正交矩阵,且行列式为1,这些约束会使求解变得困难。扩展到变换矩阵,就是用16个量表达了6个自由度的变换。

上面说的欧拉角是绕三个正交轴的依次旋转,其实任意旋转都可以用一个旋转轴和一个旋转角来表示。这只需要一个旋转向量即可,也就是说一个三维向量表示了旋转,再加上一个平移向量,这样就是6个自由度,没有冗余了。

四元数

Quaternion用一个冗余的变量解决了欧拉角的Singularity问题,在运算插值时也比较方便,因此四元数是程序中表示旋转最常用的一种表示方法

四元数——>旋转矩阵——>欧拉角 这个过程转出来的欧拉角,不可能是想要的。因为同一个四元数,可以用2个欧拉角来表示,而这个方法得到的结果有可能是用转角大于2π的方式表达的.

最好不要直接从四元数转欧拉角后,处理向量的旋转.

四元数加减法: 四元数的加法只需要将分量相加就可以了

四元数的共轭是让四元数的向量部分取负,记做 (w, -x, -y, -z)。 四元数和它的共轭代表相反的角位移,因为相当于旋转轴反向。

四元数的逆表示一个反向的旋转,定义为四元数的共轭除以它的模的平方,四元数和逆的乘积为实数 1。 如果是单位四元数,那么逆就是共轭

四元数的插值不能用简单的线性插值,而是用slerp。因为当时间t匀速变化时,代表姿态矢量的角速度变化并不均匀

Eigen::Quaterniond::Identity().slerp(t, q_last_curr)能够实现四元数的球面插值。
t ∈ [0, 1]为插值点,q_last_curr为两帧之间的旋转四元数,即针对两帧之间的旋转而线性插入一个四元数。

cartographer中重新实现的slerp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template <typename T>
std::array<T, 4> SlerpQuaternions(const T* const start,
const T* const end,
double factor)
{
// Angle 'theta' is the half-angle "between" quaternions. It can be computed
// as the arccosine of their dot product.
const T cos_theta = start[0] * end[0] + start[1] * end[1] +
start[2] * end[2] + start[3] * end[3];
// Avoid using ::abs which would cast to integer.
const T abs_cos_theta = ceres::abs(cos_theta);
// If numerical error brings 'cos_theta' outside [-1 + epsilon, 1 - epsilon]
// interval, then the quaternions are likely to be collinear.
T prev_scale(1. - factor);
T next_scale(factor);
if (abs_cos_theta < T(1. - 1e-5)) {
const T theta = acos(abs_cos_theta);
const T sin_theta = sin(theta);
prev_scale = sin((1. - factor) * theta) / sin_theta;
next_scale = sin(factor * theta) / sin_theta;
}
if (cos_theta < T(0.)) {
next_scale = -next_scale;
}
return {{prev_scale * start[0] + next_scale * end[0],
prev_scale * start[1] + next_scale * end[1],
prev_scale * start[2] + next_scale * end[2],
prev_scale * start[3] + next_scale * end[3]}};
}

参考:
四元数和欧拉角互相转换
欧拉角与旋转矩阵


搭建Hexo博客

Disqus评论

这个教程操作即可

快捷上传图片

以前一直用flicker,每次都到网站上传,然后复制图片的网址,操作实在太麻烦了,而且flicker有上传图片限制。今天终于看到一种简便的方法,使用一个Hexo Editor的工具,截图之后复制图片文本,比如![](/20190616210334907/20190616091008239.png),然后右键选择上传SM.MS,这时图片会上传到图床SM.MS,这个图床应当没有上传限制,并且把图片的网址直接复制到剪贴板:![](https://i.loli.net/2019/06/16/5d064094f300180845.png)

问题

hexo g 报错

1
2
3
4
5
6
7
8
9
INFO  Start processing
FATAL Something's wrong. Maybe you can find the solution here: http://hexo.io/docs/troubleshooting.html
TypeError: ejs:13
11| <title><%-: post.title %></title>
12| <pubTime><%= post.date.toDate().toISOString() %></pubTime>
>> 13| <%if(post.tags){ post.tags.toArray().forEach(function(tag){ %>
14| <tag><%= tag.name %></tag>
15| <% })}/*%>
16| <content><%-: post.content%></content>

将hexo-generator-baidu-sitemap文件删除掉即可

另一个可能的原因:把一个未写完的.md文件放到了_posts文件夹同名下,也就是放到了source文件夹下

hexo d报错


结果是git配置和SSH Key的问题

还在hexo博客的根目录,执行ssh-keygen,根据提示一直按回车即可,一般生成的位置在C:\Users\username\.ssh,找到id_rsa.pub后打开,到github网站的Settings中新建一个SSH key,将id_rsa.pub中的内容复制到Key中

git config --global配置git的用户名和邮箱

通过命令ssh-add解决每次操作都需要输入key的密码的问题

使用ssh -v git@github.com测试连接,如果看到验证通过就是成功了



algolia搜索

整个配置过程参见

如果搜索成功,在algolia网站的indices项目可以测试搜索结果

问题1

开始一直安装algolia失败,发现是把package.json设置为只读了

问题2

安装配置成功后,打开博客发现有了搜索按钮但没出现搜索条,在网页上按F12打开网页源码发现有报错,所以是浏览器问题而不是algolia的问题.


解决方法:还用npm安装hexo插件hexo-all-minifier,重新执行hexo cleanhexo algolia,再hexo g hexo d,打开博客应该成功了.

问题3

搜索时会发现不在post目录的文章也能搜索到,但是打开是404

参考:
Youtube上的视频教程