Shell小技巧

多行注释

bash脚本的多行注释可以采用HERE DOCUMENT特性,实现多行注释,比如

1
2
3
4
<<'COMMENT'
...

COMMENT

举例如下:
1
2
3
4
5
6
7
8
#!/bin/bash
echo "Say Something"
<<COMMENT
your comment 1
comment 2
blah
COMMENT
echo "Do something else"

将命令执行的结果写入日志文件

catkin_make > make.log, 注意不要用一直执行的命令,比如ping

head与tail

tail -f showMapInfo.log可以实时显示文件的更新
tail -n 5 showMapInfo.log显示文件最后5行
head -n 5 showMapInfo.log显示文件开始5行

修改指定目录下的所有文件的权限

全体可读、可修改、可执行: chmod -R 777 dir

history

使用history列出历史命令后,如果需要执行第 1024 历史命令,可以执行 !1024

如果以前执行了一个很长命令,只记录命令开头是 curl,这时就可以通过 !curl 快速执行该命令。但是执行的可能不是我们需要的,所以常常加上 :p,即 !curl:p ,打印出了搜索到的命令,如果要执行,请按 Up 键,然后回车即可。

在命令前额外多加一个空格,这样的命令是不会被记录到历史记录的

执行文件

对于可执行文件,在当前目录时用命令./file即可,如果是完整绝对路径,就用命令/path/file即可

检测温度

开始检测硬件传感器:sudo sensors-detect

要确保已经工作,运行命令:sensors,但只运行一次

设置终端初始路径

bashrc中加一行: cd PATH即可

有一次发现不管怎么改也没用,按照网上的方法修改bash_profile不奏效。 后来发现是系统环境变量$HOME被改变了,于是在/etc/profile中添加一行 export HOME=/home/user,问题解决

显示当前目录的文件和目录,按大小列表排序

ls -Slrh

如果排除目录: ls -Slrh | grep -v '^d'

当前目录下占用最大磁盘空间的目录

1
du -ahx . | sort -rh | head -5

Linux 踢出其他正在 SSH 登陆用户

查看系统当前所有在线用户: w

1
2
3
4
14:15:41 up 42 days, 56 min,  2 users,  load average: 0.07, 0.02, 0.00 
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root pts/0 116.204.64.165 14:15 0.00s 0.06s 0.04s w
root pts/1 116.204.64.165 14:15 2.00s 0.02s 0.02s –bash

查看当前自己占用终端

1
2
[root@apache ~]# who am i 
root pts/0 2013-01-16 14:15 (116.204.64.165)

用 pkill 命令踢掉对方: pkill -kill -t pts/1

再用 w 命令在看看踢掉没有,如果最后查看还是没有踢掉,加上 -9 强制杀死。

反向删除

文件夹下,删除abc文件以外的所有文件

1
ls | grep -v ^abc$ | xargs -i rm -rf {}

如果abc不加正则表达式,会保留文件名包含abc的文件,比如abcd

对当前目录下的所有文件排序

du -s *. | sort -nr

显示当前目录的最大的文件

du -s *. | sort -nr | head -1

检查系统中的设备情况

需要检查日志 /var/log/messages

只显示文件名和大小

  • ls -sh . 显示当前文件夹的所有文件大小

  • du -sh folder     显示指定文件夹或文件的大小,结果容易可读,比如4G

du -s /home/user/.ros/log的执行结果为

1
1147000 /home/user/.ros/log

大小的单位为 K。

如果只取空格左边部分,使用 echo $(du -s /home/user/.ros/log) | cut -d " " -f 1,如果取空格右边部分,那么把1改为2.


  • ls -Slrh 显示当前目录的文件和目录,按大小列表排序。如果排除目录: ls -Slrh | grep -v '^d'

获取CPU核数

getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu || echo 1

tar.gz 压缩

file.txt文件压缩成file.tar.gz: tar -zcvf myfile.tar.gz myfile.txt

删除包含字符串的行

把文件中包含某字符串的行删掉,比如下面文件中的abc所在行删除,使用sed命令:

1
2
3
4
5
6
7
8
abc12
abc402
a213bc
dlifjljs
abc
904650-9
aabbcc
123abc53465

经过sed处理,变为
1
2
3
4
a213bc
dlifjljs
904650-9
aabbcc

如果执行sed -e '/abc/d' test.txt,只显示修改后的结果,未修改文件。如果执行sed -i,修改并保存文件。

延时函数

使用 sleep 或usleep函数。只看sleep函数:

1
2
3
4
5
#参数单位默认为秒。
sleep 1s 或 sleep 1 #表示延迟一秒
sleep 1m #表示延迟一分钟
sleep 1h #表示延迟一小时
sleep 1d #表示延迟一天

xargs

xargs 是给命令传递参数的一个过滤器,也是组合多个命令的一个工具。它能够捕获一个命令的输出,然后传递给另外一个命令。
xargs 可以将管道或标准输入数据转换成命令行参数,这是最常用的场合,也能够从文件的输出中读取数据。
比如执行命令find *.sh,结果如下:

1
2
3
4
5
deb软件安装.sh
Kinetic插件安装.sh
ping.sh
ubuntu初始化精简版.sh
所有alias.sh

执行find *.sh | xargs会将多行结果变为单行:
1
deb软件安装.sh Kinetic插件安装.sh ping.sh ubuntu初始化精简版.sh 所有alias.sh

执行find *.sh | xargs ls -smh会使xargs捕获find结果,然后传递给之后的命令做参数,这是管道做不到的,结果:

1
2
3
4.0K deb软件安装.sh, 4.0K Kinetic插件安装.sh,
4.0K ping.sh, 4.0K ubuntu初始化精简版.sh,
4.0K 所有alias.sh

xargs与管道不同,首先看命令echo test.cpp,结果是test.cpp.再看命令echo test.cpp | cat,结果还是test.cpp,这是管道将echo的结果定向为cat的输入,执行cat当然还是那个结果.但是echo test.cpp | xargs cat就不一样了,xargs将test.cpp做cat的命令参数,也就是第二次执行的是cat test.cpp

修改某文件的修改时间

1
touch -t 201905171210.20 test.py  # 指定新时间

查看进程PID及线程

  • pidof chrome     显示所有进程的pid,进程名必须完整
  • pgrep sublime     通过名字获取pid,可以是部分进程名
  • ps -T -p pid     进程的子进程情况

pkill kill killall

  • kill pid     按pid杀死进程
  • kill -9 pid     按pid强制终止进程,但是可能造成数据丢失,轻易不要使用。比如 kill -9 $(pgrep rosmaster)
  • killall name     按名称杀死一个进程组的所有进程,比如killall chrome杀死所有的chrome进程

  • pkill name     按名称终止包含name的所有进程

  • pkill -o chrome    杀死最旧的chrome进程
  • pkill -n chrome     杀死最新的chrome进程
  • pkill -c chrome     杀死所有chrome进程,同时显示数量

用脚本杀死阻塞的进程

1
2
3
4
5
6
7
8
#!/bin/bash

while true; do

command & sleep 2 && kill -2 $!
sleep 1
echo " ---------- start next loop ---------- "
done

command是一个阻塞的进程,只有按下ctrl+C后才能继续运行,但在这种循环的情况,显然不能一直去按ctrl+C,所以需要脚本去终止进程command,也就是kill -2 $!

杀死终端的进程

ubuntu终端的进程名为bash,如果在自己电脑pkill bash,还会剩下当前操作的终端,其他的全关。如果通过另一台电脑远程登录,执行pkill bash,可以将所有终端关闭。

xdotool

sudo apt-get install -y xdotool

  • 最小化当前窗口: xdotool windowminimize $(xdotool getactivewindow)

还有很多功能,这个工具能模拟鼠标键盘的大量操作,可参考 神器 xdotool

带参数的alias

有时需要运行一个带参数的很长的命令,这样感觉很不方便,可以与alias结合,提高效率。alias不支持直接加参数,而是通过函数来间接实现。

1
2
# test()是个函数
alias ldd='test() { ldd $1 | grep -v kinetic | grep -v x86_64-linux-gnu;}; test'

统计文件行数、字节、字数

wc命令,选项 -l, -c, -w 分别统计行数、字节、字数,可统计多个文件,但不能统计目录。

1
2
3
wc -c main.cpp
wc -l *.cpp
wc -l main.cpp test.cpp t.cpp //统计三个文件行数

只显示文件名和日期

1
ls -lSh | awk '{print $6,$7,"   " $8,"  " $9}'

统计目录中的文件个数 (包括文件夹)

ls | wc -l

当前文件夹或文件大小

当前文件夹:du -smh
当前文件夹下所有文件和子文件夹: du -mh .
某文件: du -smh file

当前目录下大于400M的文件夹和文件,并从小到大排序

1
sudo du -h | grep "[4-9][0-9][0-9]M" | sort -n

ls -lht frames.pdf 显示文件的详细信息,比如:

1
-rw-rw-r-- 1 user user 20K 6月  29 10:24 frames.pdf

时间相关

  • date -s "2024-10-15 15:50:00"    设置时间
  • date +%s    输出UTC时间

判断文件/文件夹是否存在

1
2
3
4
5
6
7
if [ -d ~/Documents/dir ]
then
echo "yes"
else
echo "no, so create it"
mkdir dir
fi

-d判断后面的文件夹是否存在
-f判断是否存在某文件
-s判断是否存在某文件,且大小不为0,这个比-f更有用

函数

组合多个命令到一个操作。下面的命令组合了 mkdir 和 cd 命令。输入 mc folder_name可以在你的工作目录创建一个名为folder_name的目录并立刻进入

1
2
3
4
mc () {
mkdir -p $1
cd $1
}

scp出错

使用scp传递文件时,出现了这样的报错:

解决方法: ssh -o StrictHostKeyChecking=no 192.168.73.14

注意: 这里的IP是发送者的IP

自动化工作

每次本机提交代码后,远程再更新,编译和移动可执行文件,过程比较繁琐,干脆都放到一个Shell脚本里,用alias执行这个脚本

1
2
3
4
5
6
7
cd /home/user/Robot/workspace/src
svn up
cd /home/user/Robot/workspace
catkin_make --pkg package1 package2 package3

cp $packagebin"/exe1" $dir
cp $packagebin"/exe2" $dir

用echo管道命令给sudo自动输入密码

1
echo password | sudo -S cmd

-S表示从标准输入读取密码。这样用于system函数就比较方便了,但是这种方式密码会明文显示,密码不安全

拆分大文件

split命令拆分大文件,以原本文件名为前缀,数字为后缀

1
split --verbose -n5 myfile.cpp -d myfile_

myfile.cpp分割为5个文件, 以myfile_为前缀, 显示分割过程,但是缺陷在于拆分的文件没有扩展名

进一步优化:把test.log拆分,每个最大200k,并且按照test_数字命名,带扩展名。
split test.log -b 200k -d -a 2 test_&&ls | grep test_ | xargs -n1 -i{} mv {} {}.log

升级版ping命令

当能ping通一个主机时,此时ping命令会一直执行,要想终止,可采用CTRL+c或CTRL+z方式退出。也可以设置选项方式,使得ping命令执行若干次包就终止。ping -c 4 ip,此时ping命令将执行4次

下面的脚步是执行ping命令时,可以只输入最后地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#/bin/bash

ip=`hostname -I`
net=$(cut -d'.' -f3<<<$ip)
var1=$(cut -d'.' -f1<<<$ip)
var2=$(cut -d'.' -f2<<<$ip)
sub=${var1}'.'${var2}'.'${net}'.'

echo -e "\033[41;37m 输入要ping的IP最后一个地址 \033[0m"
read -p ":" addr
address=${sub}${addr}

ping -c 3 $address

echo -e "\033[44;37m Ping IP address done ! \033[0m"

当前文件夹中只有一个子目录时,直接进入

可以做成alias ccd :

1
2
3
4
5
6
7
8
9
10
11
#/bin/zsh

n=$(ls | wc -l)

if [ "$n" -eq "1" ]
then
folder=$(ls)
cd $folder
else
echo "can't cd, more than 1 child folder"
fi

当前文件夹中只有一个文件时,直接用vim打开

1
2
3
4
5
6
7
8
9
10
#!/bin/bash

n=$(ls | wc -l)
if [ "$n" -eq "1" ]
then
file=$(ls)
vim $file
else
echo "more than 1 file"
fi

使用脚本在当前终端打开一个新标签

1
2
3
gnome-terminal --tab
# 打开两个标签
gnome-terminal --tab --tab

在终端中打开多个tab,每个tab运行单独的命令

1
2
3
4
5
6
7
8
9
10
11
title1="tab 1"
title2="tab 2"
title3="tab 3"

cmd1="echo tab1"
cmd2="echo tab2"
cmd3="echo tab3"

gnome-terminal --tab --title="$title1" --command="bash -c '$cmd1; $SHELL'" \
--tab --title="$title2" --command="bash -c '$cmd2; $SHELL'" \
--tab --title="$title3" --command="bash -c '$cmd3; $SHELL'"

参考链接

linux删除某文件以外的文件

1
2
3
rm -f  !(a) 
# 删除a和b以外的文件
rm -f !(a|b)

找出当前文件夹下大于100M的文件

1
find . -type f -size +100M -print0 | xargs -0 du -h | sort -nr

只显示当前所有文件的名称和大小

1
ls -smhSlr | awk '{ print $1, $10 }'

判断当前某进程的个数或是否在运行

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

# ps -ef |grep zookeeper 这个就是看zookeeper的启动情况
# grep -v "grep" 是为了去掉查询 grep的那一条
# wc -l 是计数的

COUNT=$(ps -ef | grep zookeeper |grep -v "grep" |wc -l)
echo $COUNT
if [ $COUNT -eq 0 ]; then
echo NOT RUNNING
else
echo is RUNNING
fi

Linux 踢出其他正在 SSH 登陆用户

查看系统当前所有在线用户: w

1
2
3
4
14:15:41 up 42 days, 56 min,  2 users,  load average: 0.07, 0.02, 0.00 
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root pts/0 116.204.64.165 14:15 0.00s 0.06s 0.04s w
root pts/1 116.204.64.165 14:15 2.00s 0.02s 0.02s –bash

查看当前自己占用终端

1
2
[root@apache ~]# who am i 
root pts/0 2013-01-16 14:15 (116.204.64.165)

用 pkill 命令踢掉对方: pkill -kill -t pts/1

再用 w 命令在看看踢掉没有,如果最后查看还是没有踢掉,加上 -9 强制杀死。


ROS通信模型

ROS节点构成了一个计算图(computation graph),它是一个p2p的网络结构,节点之间的拓扑结构为 mesh(网状)。节点必须与其他节点通讯,否则就是个普通程序。ROS通讯中,节点通过XML-RPC远程调用来实现建立连接,传输数据。远程调用负责管理节点对计算图中信息的获取与更改,还有一些全局的设置, 但RPC不直接支持数据的流传输(而是通过 TCPROS与 UDPROS)

每个ROS节点运行一个XML-RPC server,ROS的通讯模块在ros_comm

topic通信模型

我运行了两个发布者节点:Pub和second,一个订阅者节点:Sub,话题名为topic,详细情况如下:


上面列出了三个节点的端口号,IP都是相同的,因为都在本机上,roscore的uri为http://192.168.0.106:11311,整个通信模型如下:

发布者节点通过一个名为registerPublisher的远程调用(XML-RPC),注册它们的话题和消息类型到主节点。同样地,订阅者通过registerSubscriber()远程调用,注册到主节点,这样主节点就获知了双方的uri、话题名称、消息类型。如果仅仅话题名一样,消息类型不一样,也是不能通信的。

订阅者注册时,主节点会回复注册同一话题的所有发布者,然后订阅者就直接与发布者通信,基于TCPROS协议传输消息,这就跟主节点无关了。当又有新的发布者注册同一个话题到主节点时,主节点执行publisherUpdate()给订阅者,回复一个新的发布者list。

二者通信是基于TCPROS协议,过程与三次握手类似,订阅者与发布者建立第一次连接,传输topic信息,然后再根据发布者返回的topic 信息,建立第二次连接,发布者开始传输具体的数据。

对于unregistration,发布者执行远程调用unregisterPublisher(),订阅者执行unregisterSubscriber()远程调用。

举个例子,我们可以看一个节点的信息:
rosnode info.png
话题tf_static是本节点传递给amcl节点的,方向outbound说明了这一点,也就是本节点负责发布,消息传递方式是TCPROS;话题joint_states的传递方向是inbound,也就是本节点从节点joint_state_publisher订阅了此话题

TCPROS中的重点:

  • Md5:如果两个话题名一样而消息类型不同,会无法通信成功。TCPROS为了保证两边传输数据类型一致,会在协议头中给出话题名的md5 hash算法处理过的值,而每次你生成新的msg时,md5的值都会因为你内部数据类型的变动而改变。

  • Subscriber 选项tcp_nodelay :如果是“1” 则给socket 设置 TCP_NODELAY 选项,降低延迟,可能会降低传输效率。

  • Service client 选项:persistent 设置为1,则service的链接会一直开放给多个 service request

使用netstat和wireshark研究通信过程

但是我在使用ROS时, 发现ROS命令显示的端口号并不准确,使用netstatwireshark是找不到的,最好以后两者为准

启动了一对发布订阅者的程序,用netstat查看如下,排除rosout节点:

1
2
# 同IP所有建立通信的节点,排除rosout和SSH
netstat -anop | grep 0.109 | grep ESTA | grep -v rosout | grep -v 109:22

结果还需要去掉3个通信,就是sub-rosout, pub-rosout, python-rosout。python对应的进程是roscore
netstat的结果
显然pub和sub的通信端口不是rosnode info显示的,不知道这算不算ROS的bug

wireshark抓到的发布和订阅TCPROS通信
两者之间的TCP通信只有一问一答:Publisher发PSH和ACK,要求Subscriber数据尽快达到应用层,Subscriber发回ACK,从wireshark里看不到传递的消息,但ROS又不是加密的,这点我没明白什么意思

参考:ROS的弊端


rosbag的使用

bag文件包含tf信息和话题消息,没有rosparam参数和node。 /clock是由ROS的录包回放系统rosbag发布的主题,主要是提供一个仿真时间

rosbag本质也是个ROS节点,whereis rosbag的结果: /opt/ros/kinetic/bin/rosbag,运行rosbag play会出现一个play开头的节点,后面是当前的utc时间

rosbag record

基本使用:rosbag record topic,可以有多个话题:rosbag record topic1 topic2 topic3,话题可以用shell脚本列出来,例如: rosbag record rostopic list | grep plan

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-o: 在文件名之前加指定的字符串, rosbag record -o test topic ,结果生成文件test_2019-06-24-14-32-08.bag

-node: 记录某节点订阅的所有话题,` rosbag record --node=/joy_teleop

-l: 只记录指定数量的消息, rosbag record -l 1000 /chatter,只记录1000条

--duration: 保持一定时间记录话题, rosbag record ---duration=30 /chatter, 即记录30秒,还可以有5m, 2h
rosbag record --split --duration=30 /chatter #持续时间到30s后分文件存储

-O: 大写O,指定完整文件名 rosbag record -O sess.bag /topic

--size: 指定文件大小,rosbag record --split --size=1024 /chatter #空间达到1024M后分文件存储

-e, -x: 用于正则表达式

我最常用的是rosbag record topic -o filename

rosbag info

显示指定bag文件的详细信息,rosbag info session*.bag

rosbag play

rosplay其实是重现话题数据传输的效果,比如向话题cmd_vel发数据的过程

rosbag回放过程中按空格暂停。默认模式下,rosbag play命令在公告每条消息后会等待0.2秒才真正开始发布bag文件中的消息。如果rosbag play在公告消息后立即发布,订阅器可能会接收不到几条最先发布的消息

如果同时回放两个单独的bag文件,则根据时间戳的间隔来播放。比如我先录制一个bag1,等待一个小时,然后录制另一个 bag2,在一起回放 bag1 和 bag2 时,实际是先回放bag1,然后等待1个小时才回放bag2

等待时间可以通过-d选项来指定,比如等待10秒再播放bag: rosbag play -d 10 realsense.bag

以暂停的方式启动,防止跑掉数据:

1
rosbag play --pause record.bag

要一帧一帧播放的效果,先用暂停方式启动,然后按S键,这个用的还比较少

设置以0.5倍速回放,也就是以录制频率的一半回放。可以加倍播放,但会增大CPU占用。

1
rosbag play -r 0.5 record.bag

循环播放:

1
rosbag play -l record.bag

use_sim_time的问题

播放bag时,运行算法程序,结果报警:

1
2
3
Warning: TF_OLD_DATA ignoring data from the past for frame base_footprint at time 1.59524e+09 according to authority unknown_publisher
Possible reasons are listed at http://wiki.ros.org/tf/Errors%20explained
at line 277 in /tmp/binarydeb/ros-kinetic-tf2-0.5.20/src/buffer_core.cpp

在其他节点之前运行rosbag play --clock ...,会设置好use_sim_time

--clock选项会让 rosbag play发布和订阅bag文件中的消息到/clock话题,这样会让你的其他节点运行在消息发布的时候。 如果use_sim_time是 true, /clock需要消息

这样一来launch文件里加入rosparam set use_sim_time true就不合适了,launch里常常不加入 rosbag play,即使加入了,也不一定先设置了use_sim_time,合适的执行顺序如下:

  1. 启动ROS,但是不包含你需要启动的节点
  2. rosparam set use_sim_time true
  3. 启动你需要的节点
  4. rosbag play —clock

有的资料说不用--clock才能正常,我估计这是数据集的问题,一般还是要有的

filter

可以实现对已有rosbag文件中的某些topic去除或者保留

1
rosbag filter input.bag output.bag 'topic == "/camera/image_raw/compressed" or topic == "/scan" or topic == "/timetag" or  topic == "/tf" and m.transforms[0].header.frame_id != "/odom" and m.transforms[0].child_frame_id != "/odom"'

结果是保留话题/camera/image_raw/compressed , /scan, /timetag, /tf。但是tf变换的frame_idchild_frame_id不是odom,这样的tf消息会被过滤

参考: How to remove a TF from a ROS bag

bag文件转txt csv

将file_name.bag文件中topic_name话题的消息转换到 file.txt文件中:

1
rostopic echo -b file.bag -p /topic_name > file.txt

-p 只能指定一个话题

rosbag可以直接转csv文件,用excel打开,跟转txt一样的

rosbag compress

如果录制的bag很大,我们可以压缩它,默认的压缩格式是bz2:

1
2
3
4
5
6
7
8
rosbag compress xxx.bag
# 可以添加`-j`手动指定压缩格式为bz2
rosbag compress -j xxx.bag
# 也可以使用LZ4来压缩数据
rosbag compress --lz4 xxx.bag

# 解压缩
rosbag decompress xxx.bag

实例

我们常常使用rosbag record命令录制机器人的建图或行走过程,一般先运行record命令,然后开始机器人的路径规划和行走,到达目标点后,在record命令的终端直接ctrl+C就可以保存bag文件.

这里涉及到一个时间戳的问题, bag文件的时间而非本机时间,在默认情况下,ROS使用ubuntu系统的时间,也就是wall clock time.但bag文件中记录的是历史时间,所以play之前要告诉ROS启用simulated time

1
rosparam set use_sim_time true

然后使用rosbag play --clock test.bag命令播放,当然要先打开rviz,添加对应的topic:

play结束之后,还要把参数还原:

1
rosparam set use_sim_time false

这样可以在rviz中离线分析问题了, 大大提升了效率

问题

我之前根据make_plan服务生成了路径,存放在bag文件里,在我使用rosbag play这些文件时,出现了报错:

不管怎么检查,文件命名也没有问题,不得其解。

使用rosbag info查看其信息,发现起始和终止时间相同,只有1个message。因为make_plan服务生成路径只是运行了一次回调函数,所以只有一个message,即使能在rviz显示,也只有几微妙的一瞬间,没什么意义

读取bag的大小不一致

录制bag包后可能没有正确结束掉程序,回放出现错误:

1
2
[ INFO] [1603677455.288200829]: Opening fuse_2020-10-24-18-01-07.bag
[FATAL] [1603677455.288476478]: Error reading from file: wanted 4 bytes, read 0 bytes

可使用如下命令修复:rosbag reindex xxx-xxx-xxx.bag

play时error reading version line

rosbag可能已损坏,你可以使用rosbag info XX.bag查看你想要读取的rosbag包的信息,如果无法读取到有效信息,说明该rosbag确实已损坏。


C++与Qt常用的类型转换

注意: c++11不允许char* c = "123";这种初始化,通常都用const char*stringchar*是不允许的。
printf("%s")只接收char*const char*,不能是string

stringToNum

template
Type stringToNum(const std::string& str)
{
std::istringstream iss(str);
Type num;
iss >> num;

return num;

}

const char* 转 string

1
2
const char* cc = "12345";
std::string s = cc;

其实就是直接赋值

string转const char*

1
2
3
std::string s = "12345";
const char* cc = s.c_str();
const char* cc = s.data(); // 实际调用同c_str()

const char 转 char

1
char* c = const_cast<char*>(cc);

char 转 const char

直接赋值转换即可

拼接const char*

先转换成string,用+运算符,最后转为const char*类型。或者用strcat,但是要保证空间足够:

1
2
3
4
5
6
char str1[30];
memset(str1, 0, sizeof(str1));
memcpy(str1, "abcd",4);
const char* str2 = "1234";
strcat(str1, str2);
cout << str1 <<endl;

还有stringstream
1
2
3
4
5
6
#include <sstream>

std::stringstream ss;
ss << a << b;

std::string combined = ss.str();

数值类型转为string

C++11标准增加了全局函数std::to_string,有多个重载,将常用数值类型转为string

1
2
3
4
5
6
7
8
9
string to_string (int val);
string to_string (long val);
string to_string (long long val);
string to_string (unsigned val);
string to_string (unsigned long val);
string to_string (unsigned long long val);
string to_string (float val);
string to_string (double val);
string to_string (long double val)

string转数值类型

也是C++11新增加的函数

1
2
3
4
std::stod 		// string转double
std::stof // string转float
std::stoi // string转int
std::stol // string转long

QString转std::string

QString::toStdString

std::string转QString

QString::fromStdString

QString转char*

1
2
3
QString str1 = "Hello";
QByteArray a = str.toLatin1();
char *b = a.data();

char* 转QString

1
2
char *b;
QString str2 = QString(QLatin1String(b));

QByteArray与QString

QString会用UTF-16编码存储,而qDebug()等I/O函数会以UTF-8编码处理。其实转换后的字节流是正确的,只是显示时用了和字节流不同的编码方式处理导致乱码

// Qt默认会使用本机编码,所以对于中文系统,下面这句设置是多余的
QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK"));

1
2
3
4
5
6
7
8
9
//QByteArray转QString
QByteArray ba;
QString s = QString(ba); // 强制转换

//QString转QByteArray
QString s="abc";
QByteArray ba = s.toLocal8Bit(); // 受setCodecForLocale影响,会转换为设定的编码。如果本机不支持指定编码,则会按toLatin1处理
QByteArray baLatin1 = str1.toLatin1(); //不受setCodecForLocale影响,强制转换为ISO-8859-1编码
QByteArray bUtf8 = str1.toUtf8(); // 不受setCodecForLocale影响,强制转换为UTF-8编码

QByteArray为汉字时,输出会出现乱码,解决方法:

1
2
3
4
5
QByteArray ba = "汉字";
QTextCodec* tc = QTextCodec::codecForName("GBK");
qDebug()<<ba; // "\xBA\xBA\xD7\xD6"
qDebug()<<QString(ba); // "????"
qDebug()<<tc->toUnicode(ba); // 汉字

wchar_t 转 QString

1
2
wchar_t a[10];
QString str1= QString::fromWCharArray(a);

int/char 转 16进制QString 补0

1
2
int a = 0x0483;
QString str1 = QString("%1").arg(a,4,16,QLatin1Char('0'));

环境设置为GB2312,汉字会显示为乱码,使用QStringLiteral会报错error: converting to execution character set: Illegal byte sequence,发现是Mingw编译器问题,换成MSVC编译正常。

参考:Qt字符编码、乱码的一点总结


使用第三方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教程(三)多线程

在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


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库提供了两个头文件,一个是<boost/filesystem.hpp>,这个头文件包含主要的库内容。它提供了对文件系统的重要操作。同时它定义了一个类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,也就是不如不提交。比第一条更严重,让别人也无法合并

配置gitlab密钥

git init

常常之后,出现下面结果

1
2
3
4
5
6
7
8
9
提示:使用 'master' 作为初始分支的名称。这个默认分支名称可能会更改。要在新仓库中
提示:配置使用初始分支名,并消除这条警告,请执行:
提示:
提示: git config --global init.defaultBranch <名称>
提示:
提示:除了 'master' 之外,通常选定的名字有 'main''trunk''development'
提示:可以通过以下命令重命名刚创建的分支:
提示:
提示: git branch -m <name>

可以对分支改名。此时使用git branch无法查看分支,因为还没有任何操作。

在本地建立仓库到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

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之后使用此命令无效。

撤销 commit 的多种方法

那么在执行完 commit 之后,想撤回 commit

1
git reset --soft HEAD^

HEAD^ 意思是上一个版本,也可以写成 HEAD~1 。如果进行了 2commit,都想撤回,可以使用 HEAD~2

撤销 commit、并撤销 git add 操作、不撤销修改代码

1
git reset --mixed HEAD^
1
2
git reset HEAD^
// 效果和 git reset --mixed HEAD^ 一样,--mixed 是默认参数

以上操作将把HEAD指针移动到父提交,但不会改变工作目录中的文件,修改将被保留。

撤销 commit、不撤销git add .

soft

1
git reset --soft HEAD^

撤销 commit、撤销 git add . 操作、撤销修改代码

hard

1
git reset --hard HEAD^
  • 这个命令将HEAD指针移动到当前提交的父提交,并且使用--hard选项会使工作目录中的文件恢复到这个父提交的状态;
  • 这意味着所有自上次提交以来的未提交的修改都将被删除;
  • 如果想保留这些修改,可以使用git stash命令来保存它们,然后在需要的时候再应用这些修改。

免密码 push的方法

  1. 使用文件创建用户名和密码

文件创建在用户主目录下:

1
2
3
touch .git-credentials
vim .git-credentials
https://{username}:{password}@github.com

记得在真正输入的时候是没有大括号的。

  1. 添加 git config 内容

git config --global credential.helper store

执行此命令后,用户主目录下的.gitconfig 文件会多了一项:[credential]

helper = store

重新 git push 就不需要用户名密码了。

重命名文件

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

修改commit message

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

gitignore

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


cout以及类型的格式输出

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表示科学计数法表示,此时精度数字是小数点位数

类型 标志
unit16_t %hu
unit32_t %u
unit64_t %llu
unit32_t %zu
unsigned int %u
long long int %lld
unit32_t %u
unit32_t %u

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

欧拉角

参考欧拉角的理解

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

规定: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]}};
}

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