Ubuntu系统装机和初始化

安装

必须用U盘启动方式进入系统,否则会报错:
Alt text
步骤:

  1. 使用PowerISO和Ubuntu的ISO压缩包制作启动U盘
  2. 先选择”try me without install”模式,进入系统后看看有什么问题,比如无线网能不能连接,没什么问题后,退出重启,按流程装好Ubuntu系统
  3. 插上U盘重新启动Ubuntu,选择”try me without install”模式。如果没有从U盘启动,按住F2进入BIOS,到BOOT标签,修改启动的优先顺序为U盘启动为第一优先
  4. 从”try me”模式进入系统后,打开GParted
  5. umount硬盘,然后按提示进行Resize partition
  6. 每次分盘要等待一段时间,完成后拔出U盘重启电脑
  7. 更改各磁盘的 label
    输入 sudo fdisk -l,然后按照sudo e2label /dev/sda1 "diskname"的格式修改

安装 Qt

安装Qt必须根据系统是32位还是64位,从官网下载对应版本的Qt安装包,然后./qt-***,在安装完成后缺少编译器和调试器。从软件安装程序里安装CMake,然后安装gcc和GDB:

1
sudo apt-get install gcc && gdb

  ,完成后运行程序可能报错:cannot find -lGL collect2:error:ld returned 1 exit status 因为缺少opengl库
在联网状态下:sudo apt-get libgl1-mesa-dev

解决没声音问题 & 安装PulseAudio

在终端运行sudo alsamixer,按M键把关闭的都大开(关闭的都显示的是MM),然后调节到合适的位置.
Alt text

如果耳机还是没声音,可是试着运行sudo gedit /etc/modprobe.d/alsa-base.conf
然后在最后一行加入

1
#enable headphoneoptions snd-hda-intel power_save=10 power_save_controller=N model=6stack-dig

然后运行 sudo apt-get remove alsa-base

重启电脑后运行 sudo apt-get install alsa-base

另外可以安装PulseAudio音量控制软件,性能更好

1
sudo apt-get install pavucontrol

禁止窗口 System program problem detected 弹出

sudo gedit /etc/default/apport,将参数1改为0

Ubuntu 18 的最大最小关闭布局

Ubuntu是关闭在左侧,结果Ubuntu18又弄到了右侧。于是想要调整,需要安装dconf-editor

1
sudo apt install dconf-editor

进入/org/gnome/desktop/wm/preferences/button-layout,改为 close,maximize,minimize:

放大标题栏

font的放大只适用于字体,窗口的标题栏还是太小,尤其最大最小按钮。可以做如下设置

但是ubuntu 18.04里没有这个选项了,只能在19.10版本之后

安装字体

从Windows复制或从网上下载后,放到 /usr/share/fonts即可

安装中文输入法

  1. 安装Chinese语言包
    鼠标依次点击System Settings–>Language Support–>Install/Remove Languages
    选中Chinese,点击Apply应用即可,等待下载安装完成。

  2. 这里完成的只是中文语言包的安装,还并不能使用中文输入法。所谓iBus pinyin输入法,这个pinyin输入法是基于iBus框架。
    安装ibus框架:

    1
    sudo apt-get install ibus ibus-clutter ibus-gtk ibus-gtk3 ibus-qt4

    启动ibus框架:im-config -s ibus
    安装ibus拼音:sudo apt-get install ibus-pinyin

  3. 重启电脑后到Language Support选择fcitx框架,到Text Entry添加输入法

参考:
Ubuntu安装Ibus拼音
ibus官方主页

trash-cli 不直接删除文件

这个工具是把文件放到了回收站,不是永久删除

sudo apt install -y trash-cli

  • trash-put 把文件或目录移动到回收站
  • trash-empty 清空回收站
  • trash-list 列出回收站文件
  • trash-restore 恢复回收站文件
  • trash-rm 删除回收站文件

安装oh-my-zsh

  1. 运行cat /ect/shells可知目前安装的shell,一般默认是bash
  2. 运行sudo apt-get install -y zsh安装zsh,再运行上面的命令发现多了一个:/bin/bash
  3. 安装oh-my-zsh: git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
  4. zsh --version查看zsh的版本,用chsh -s (which zsh) 切换shell至zsh,重启电脑
  5. 修改主题,vim ~/.zshrc修改配置文件,修改ZSH_THEME="ys",重启终端
  6. 安装插件。这个插件会记录你常去的那些目录,然后做一下权重记录,可以用这个命令看到你的习惯:j --stat,如果这个里面有你的记录,那你就只要敲最后一个文件夹名字即可进入,比如program:j program,就可以直接到:/usr/program
  7. 安装zsh-syntax-highlighting, 编辑配置文件,plugins=(zsh-syntax-highlighting)
    刷新下配置:source ~/.zshrc
  8. 安装powerline

使用时发现一个终端会记录其他终端的命令历史,这样使用向上箭头时,得到的不是希望的命令,这是因为zsh默认共享了命令历史,在~/.zshrc里添加 setopt no_share_history 即可解决

参考:安装oh-my-zsh

安装 unity tweak tool

1
sudo apt-get install unity-tweak-tool

启动:unity-tweak-tool

安装缺失文件:

1
2
3
sudo apt-get install notify-osd

sudo apt-get install overlay-scrollbar

添加桌面程序的启动

1
2
3
4
5
6
7
[Desktop Entry]
Version=1.0
Name=Groot
Exec=/usr/local/lib/groot/Groot
Icon=/home/user/Pictures/groot.ico
Type=Application
Terminal=false

如果运行后没有打开,很可能是执行有问题,单独运行Exec部分试试

开机启动

Utilities-Tweaks-Startup Applications里设置

设置系统快捷键

打开系统设置-Keyboard 键盘设置,以shutter的截图为例:

单击右侧 Disabled,然后快速按下 Ctrl+Alt+A就成功了

安装cairo-dock

实现类似Mac的效果

1
2
3
sudo add-apt-repository ppa:cairo-dock-team/ppa 
sudo apt-get update
sudo apt-get install cairo-dock cairo-dock-plug-ins

启动:cairo-dock

剪贴板命令

sudo apt-get install xclip
xclip可以将内容输出到‘X’的剪切板中, 命令echo "Hello, world" | xclip -selection clipboard,可以做成alias,这样很实用

更改鼠标风格

下载鼠标风格文件后,将整个文件夹放到/usr/share/icons,然后到Tweak Tool中就能选择了。

解决Ubuntu双显示器屏幕边缘鼠标粘滞问题

笔记本在ubuntu下使用了扩展屏幕,使得两个显示屏可以协同工作,但是每次鼠标在两个屏幕直接移动的时候,总是被屏幕边缘粘住,不让移到另一个屏幕上,除非以很快的速度移动才能把鼠标从一个屏幕移动到另一个屏幕上。

在桌面点击屏幕右上角账户名旁边的关机按钮,在弹出的菜单里选择”显示“,打开显示配置窗口,你会发现里面有一个选项叫做粘滞边缘,只要把这个选项关闭就可以了。

定制窗口侧边栏的默认书签

打开文件~/.config/user-dirs.dirs,然后修改如下:

1
2
3
XDG_DESKTOP_DIR="$HOME/桌面"
XDG_TEMPLATES_DIR="$HOME/"
XDG_PUBLICSHARE_DIR="$HOME/"

桌面最好不要修改,否则HOME下的文件都会显示在桌面上.再执行xdg-user-dirs-gtk-update,重启.

效果如下:

CPU跑满测试

安装stresssudo apt-get install stress

运行下面命令,可以使得6核CPU跑满,-c是指CPU,6是指6核,具体几核可以打开jtop查看。如用于其他jetson板卡,根据具体CPU核数更改下面命令即可。

1
stress -c 6


cmake教程(三)find_package和pkg_check_modules

find_package

find_package用于加载第三方库,可以将需要的部分指定为组件,例如

1
2
3
4
5
6
7
8
set(PACKAGE_DEPENDENCIES
cartographer_ros_msgs
eigen_conversions
geometry_msgs
urdf
visualization_msgs
)
find_package(catkin REQUIRED COMPONENTS ${PACKAGE_DEPENDENCIES})

  • REQUIRED可选字段:表示一定要找到包,找不到的话就立即停掉整个cmake。而如果不指定REQUIRED则cmake会继续执行。
  • COMPONENTS: 可选字段,表示查找的包中必须要找到的组件,如果有任何一个找不到就算失败,类似于REQUIRED,导致cmake停止执行。


使用Find_Package寻找模块时,每一个模块都会产生如下变量:

1
2
3
_FOUND
_INCLUDE_DIR
_LIBRARY or _LIBRARIES

如果_FOUND为真,需要把_INCLUDE_DIR加入到INCLUDE_DIRECTORIES中,_LIBRARY加入到TARGET_LINK_LIBRARIES

module 模式

find_package将先到module路径下查找 Find<name>.cmake。首先它搜索 ${CMAKE_MODULE_PATH}中的所有路径,然后搜索 /usr/share/cmake-3.5/Modules.比如find_package(Boost)搜索的文件是/usr/share/cmake-3.5/Modules/FindBoost.cmake

如果在CMakeLists.txt中没有下面的指令:

1
set(CMAKE_MODULE_PATH  "Findxxx.cmake文件所在的路径")

那么cmake不会搜索CMAKE_MODULE_PATH指定的路径,此时cmake会搜索第二优先级的路径.

如果cmake需要的是Find<name>.cmake,但是没在路径找到,可以添加路径到环境变量:

1
set(CMAKE_MODULE_PATH  ${CMAKE_MODULE_PATH} "Findxxx.cmake文件所在的路径")

config 模式

如果按照module模式未找到,cmake将会查找 <Name>Config.cmake<lower-case-name>-config.cmake 文件。cmake会优先搜索自定义的xxx_DIR路径。如果在CMakeLists中没有下面的指令:

1
set(xxx_DIR  "xxxConfig.cmake文件所在的路径")

那么cmake就不会搜索xxx_DIR指定的路径.而是到/usr/local/lib/cmake/xxx/中搜索,比如/usr/local/lib/cmake/yaml-cpp/yaml-cpp-config.cmake,如果还没有就失败了。一般情况下,对第三方库执行make install都会把cmake文件放到这个文件夹里,如果编译报错就需要找到文件然后手动复制过去,但是不是光复制xxxConfig.cmake,还要有xxxTargets.cmake,否则find_package就会报错:
缺abslTargets.cmake.png

上面两种添加路径到环境变量的方法要根据寻找的文件进行设置,比如我在QtCreator+Cmake中使用OpenCV,直接find_package(OpenCV REQUIRED)会报错 找不到OpenCVConfig.cmake ,先用locate尝试寻找FindOpenCV.cmakeOpenCVConfig.cmake,发现只有电脑上只存在后者,所以要用config模式: set(OpenCVDIR "/opt/ros/kinetic/share/OpenCV-3.3.1-dev")



按上面的方法一般都能寻找库成功,如果还是失败,就用下面这种 优先级最高的强制方法: </font>

1
find_package(OpenCV REQUIRED PATHS /usr/local/share/OpenCV NO_DEFAULT_PATH) 

相关的宏

cmake找到任意一个之后就会执行这个文件,然后这个文件执行后就会设置好一些cmake变量。比如下面的变量(NAME表示库的名字,比如可以用Opencv 代表Opencv库):

1
2
3
4
<NAME>_FOUND	# 布尔量
<NAME>_INCLUDE_DIRS or <NAME>_INCLUDES
<NAME>_LIBRARIES or <NAME>_LIBRARIES or <NAME>_LIBS
<NAME>_DEFINITIONS

我们可以在CMakeList中用下面代码检验find_package的结果:

1
2
3
4
5
6
7
8
9
10
11
find_package(but_velodyne  REQUIRED)
if (but_velodyne_FOUND)
MESSAGE (STATUS "definitions: ${ButVELODYNE_DEFINITIONS}")
MESSAGE (STATUS "include dirs: ${ButVELODYNE_INCLUDE_DIRS}")
MESSAGE (STATUS "lib dirs: ${ButVELODYNE_LIBRARY_DIRS}")

include_directories(${ButVELODYNE_INCLUDE_DIRS})
target_link_libraries (helloworld ${ButVELODYNE_LIBRARY_DIRS})
else()
MESSAGE (STATUS " but_velodyne not found")
endif(but_velodyne_FOUND)

如果cmake在两种模式提供的路径中没有找到对应的Findxxx.cmake和xxxConfig.cmake文件,此时系统就会提示最上面的那些错误信息。

ROS中的find_package

对于catkin_make,它会搜索ROS Package的安装目录,不必用set指定搜索目录.比如std_msgs对应的文件路径在/opt/ros/kinetic/share/std_msgs/cmake/std_msgsConfig.cmake.这两个文件是库文件安装时自己安装的,将自己的路径硬编码到其中。

ROS中常常需要多个package,比如roscpp,rospy,std_msgs。我们可以写成:

1
2
find_package(roscpp REQUIRED)
find_package(std_msgs REQUIRED)

这样的话,每个依赖的package都会产生几个变量,这样很不方便。所以还有另外一种方式:
1
2
3
4
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
)

这样,它会把所有pacakge里面的头文件和库文件等等目录加到一组变量上,比如catkin_INCLUDE_DIRS,这样我们就可以用这个变量查找需要的文件了,最终就只产生一组变量了。


REQUIRED 参数:其含义是指是否是工程必须的,表示如果包没有找到的话,cmake的过程会终止,并输出警告信息。对应于Find.cmake模块中的 NAME_FIND_REQUIRED 变量。

COMPONENTS参数:在REQUIRED选项之后,或者如果没有指定REQUIRED选项但是指定了COMPONENTS选项,在它们的后面可以列出一些与包相关(依赖)的部件清单

编译ROS工作空间的问题

ROS的工作空间用的时间一长,就会创建很多package,有些package的编译又用到了其他的package,这时单纯使用catkin_make就会出现问题。比如package A用到了B,B里又用到了C。即使catkin_make --pkg也不行,因为它会把所有的package都处理一遍,此时处理到A时就会报错,会显示找不到B和C的cmake文件。它们的路径在devel/share/B/cmake

比较笨的方法就是在A的CMakeLists里先把B和C注释掉,同样在B的CMakeLists里把A注释掉,先用catkin_make --pkg C编译C,再依次编译B和A。

找了好长时间没找到很好的解决方法,最后只能预先编译好cmake文件放到SVN上,以后就算B和C的程序改变,也可以编译。

Qt库的情况

以上问题都还简单,问题是在ROS中调用Qt的情况,以Core模块为例,我们有下面的代码:

1
2
3
4
5
6
7
8
find_package(Qt5 COMPONENTS Core Xml)
target_link_libraries(bin Qt5::Core Qt5::Xml)

if(Qt5Core_FOUND)
MESSAGE(STATUS "##### ${Qt5Core_VERSION}")
MESSAGE(STATUS "##### ${Qt5Core_INCLUDE_DIRS}")
MESSAGE(STATUS "##### ${Qt5Core_LIBRARIES}")
endif(Qt5Core_FOUND)

运行结果是:

1
2
3
--  5.5.1
-- /usr/include/x86_64-linux-gnu/qt5/; /usr/include/x86_64-linux-gnu/qt5/QtCore; /usr/lib/x86_64-linux-gnu/qt5//mkspecs/linux-g++-64
-- Qt5::Core

首先注意:能使用Qt5的CMake最低版本是 3.1.0,Qt库的宏规则实际仍然符合cmake,不同的地方在于不需要`命令,只要target_link_libraries就足够,它会自动添加相应的include directories,compile definitions`等等

Qt5Core对应的cmake文件在Linux上有两个位置:

1
2
3
/home/user/Qt5.11.1/5.11.1/gcc_64/lib/cmake/Qt5Core/Qt5CoreConfig.cmake

/usr/lib/x86_64-linux-gnu/cmake/Qt5Core/Qt5CoreConfig.cmake

经过测试,CMakeLists中起作用的应当是第二个,应当是安装Qt时,在此目录生成了文件.这个路径与环境变量Qt5_DIR相近,在CMakeLists中用MESSAGE函数能看到:/usr/lib/x86_64-linux-gnu/cmake/Qt5

Boost

Boost比较特殊,cmake对它有特别照顾,使用命令cmake --help-module FindBoost可以看到极为详细的使用方法.使用Boost有时要加上REQUIRED COMPONENTS XXX,这是在搜索已经编译的库,但不会检查只有头文件的库.比如threadsystem要加入COMPONENTS但asio不需要.

cmake中使用Boost的filesystem,thread模块:

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

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

必须得加上system模块

pkg_check_modules

pkg_check_modules是 CMake 自己的 pkg-config 模块的一个用来简化的封装:你不用再检查 CMake 的版本,加载合适的模块,检查是否被加载,等等,参数和传给 find_package 的一样:先是待返回变量的前缀,然后是包名(pkg-config 的)。这样就定义了<prefix>_INCLUDE_DIRS和其他的这类变量,后续的用法就与find_package一致。

当安装某些库时(例如从RPM,deb或其他二进制包管理系统),会包括一个后缀名为 pc 的文件,它会放入某个文件夹下(依赖于系统设置,例如,Linux 为该库文件所在文件夹/lib/pkgconfig),并把该子文件夹加入pkg-config的环境变量PKG_CONFIG_PATH作为搜索路径。
pkg_check_modules实质上是检测系统中的 pkg-config 是否存在指定的 .pc 文件。

在我的电脑上执行echo $PKG_CONFIG_PATH,结果是:

1
/home/user/Robot/workspace/devel/lib/pkgconfig:/opt/ros/kinetic/lib/pkgconfig:/opt/ros/kinetic/lib/x86_64-linux-gnu/pkgconfig

bash.rc里没有设置,但是能获得这个环境变量,这是因为我们的环境变量里设置了

1
2
source /opt/ros/kinetic/setup.bash
source /home/user/Robot/workspace/devel/setup.bash



在cmake中使用pkg_check_modules时,就会去上面的路径里搜索pc文件,例如

1
2
3
4
# bfl (Bayesian Filtering Library)是一个使用pkg-config的第三方库
# 先搜索cmake自己的PkgConfig模块,才能使用pkg_check_modules
find_package(PkgConfig)
pkg_check_modules(BFL REQUIRED orocos-bfl)

其中PkgConfig的路径在/opt/ros/kinetic/share/ros/core/rosbuild/FindPkgConfig.cmake

搜索对应库文件,发现在以下路径:

1
2
/opt/ros/kinetic/lib/liborocos-bfl.so
/opt/ros/kinetic/lib/pkgconfig/orocos-bfl.pc

现在我们可以获得对应的宏,使用这个库了:

1
2
include_directories(${BFL_INCLUDE_DIRS})
link_directories(${BFL_LIBRARY_DIRS})

参考:
CMake Manual For Qt
Boost的编译库列表


POD数据类型

看《STL源码剖析》时发现了POD类型这个名词,一开始初步的理解就是普通数据类型,后来查了查比较完整的定义。

POD是Plain Old Data的缩写,是 C++ 定义的一类数据结构概念,比如 int、float 等都是 POD 类型的。两个系统进行交换数据,如果没有办法对数据进行语义检查和解释,那就只能以非常底层的数据形式进行交互,而拥有 POD 特征的类或者结构体通过二进制拷贝后依然能保持数据结构不变。也就是说,能用C语言的memcpy(),memset()等函数进行操作的类、结构体就是POD类型的数据。一般我们要研究的问题是某class, struct, union是不是POD,在STL中就是元素的类型了。

POD的特点

POD类型要满足两个特征:trivial和布局有序。我们可以用std::is_trivial<T>::value判断一个类型是否是POD

对于class,struct,要成为POD类型说白了就是要 “像” C语言下的struct,因此要满足下面条件:

  1. 不能自定义构造/析构函数、拷贝/移动构造函数、拷贝/移动运算符,而是用编译器自动为我们生成的默认版本,那这个数据就是trivial。非要写的话,用 C++ 11 的 default 关键字。

  2. 不能有虚函数和虚基类,否则肯定会具备面向对象特性了

  3. 非静态成员变量的访问级别相同

  4. 派生类不增加新的成员变量,因为 C 没有继承的概念,所以不要把普通成员在两个类中都写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
};
class B
{
B(){}
};
class C
{
C()=default;
};

qDebug()<<std::is_trivial<A>::value; //true
qDebug()<<std::is_trivial<B>::value; //false,自定义构造函数
qDebug()<<std::is_trivial<C>::value; //true,使用默认构造函数

来试验一下memcpy操作POD类型:

1
2
3
4
5
6
7
8
    C obj;
obj.a = 13;
obj.b = 99;
// memset(&obj, 0 ,sizeof(obj));
int *foo = new int(sizeof(C));
memcpy(foo,&obj,sizeof(C));
C* t = reinterpret_cast<C*>(foo);
qDebug()<<t->a<<" "<<t->b;

运行结果是13 99


Vim使用技巧

用Vim编辑新文件的时候, 由于服务器断开链接,会导致编辑很久的文件丢失,但是我们可以使用swp临时文件进行恢复, swp文件是隐藏文件. 比如文件路径下有以下文件.test.py.swp,可以使用vim -r test.py恢复文件test.py

设置主题

设置Vim为dracula主题,参考Dracula,在/etc/vim/vimrc的最后添加

1
2
3
packadd! dracula
syntax enable
colorscheme dracula

在普通状态下的命令

  • u: 返回旧状态
  • ctrl+r: 进入新状态
  • 点号: 重复上一次操作
  • dd: 删除光标所在行
  • ndd: 删除光标下面n行
  • yy: 复制光标所在行
  • nyy: 复制光标下面n行
  • 小写p: 将复制的内容在光标下一行粘贴
  • 大写P: 将复制的内容在光标上一行粘贴

  • dw: 删除光标后的单词剩余部分

  • d$: 删除光标后的该行剩余部分
  • /string: 查找string,有结果后n查找下一个,N查找上一个
  • :set nu : 显示行号
  • 清空文件: 先执行gg跳至文件首行,再执行dG就清空了文件。

查找与替换

:s(substitute)命令用来查找和替换字符串。语法如下:
:{作用范围}s/{目标}/{替换}/{替换标志}

例如: 输入:进入命令模式,然后%s/foo/bar/g会在全局范围(%)查找foo并替换为bar,所有出现都会被替换(g)

查看和设置编码

:set fileencoding即可显示文件编码格式。

以在Vim中直接进行转换文件编码,比如将一个文件转换成utf-8格式
:set fileencoding=utf-8

用vim来设置UTF-8编码的BOM标记:

1
2
3
:set nobomb // 去掉BOM
:set bomb // 加上BOM
:set bomb? //查询当前UTF-8编码的文件是否有BOM标记

Vim 跳到某行

在编辑模式下输入ngg

安装 Vim 主题

  1. 下载主题文件,比如solarized.vim
  2. 找到vim的runtime路径,在Vim的命令行模式输入 echo $VIMRUNTIME会显示。Ubuntu 17.10的是/usr/share/vim/vim80。它里面有colors文件夹,放入下载的主题文件。
  3. 修改.vimrc文件,它应该在/etc/vim目录,添加以下内容:
1
2
3
syntax enable
set background=dark
colorscheme solarized

然后重启Vim即可

配置Vim

/etc/vim/vimrc添加下面内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
set nu
colorscheme falcon

"禁止生成临时文件
set nobackup
set noswapfile

"搜索逐字符高亮
set hlsearch
set incsearch
set confirm
set background=dark
set autoread
set cursorline

hi CursorLine cterm=NONE ctermbg=darkred ctermfg=white
hi CursorColumn cterm=NONE ctermbg=darkred ctermfg=white

filetype plugin on

set showcmd " Show (partial) command in status line.
set showmatch " Show matching brackets.

配置Vim的光标颜色样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if &term =~ "xterm"
" INSERT mode
let &t_SI = "\<Esc>[5 q" . "\<Esc>]12;orange\x7"
" REPLACE mode
let &t_SR = "\<Esc>[3 q" . "\<Esc>]12;black\x7"
" NORMAL mode
let &t_EI = "\<Esc>[5 q" . "\<Esc>]12;red\x7"
endif
" 1 -> blinking block 闪烁的方块
" 2 -> solid block 不闪烁的方块
" 3 -> blinking underscore 闪烁的下划线
" 4 -> solid underscore 不闪烁的下划线
" 5 -> blinking vertical bar 闪烁的竖线
" 6 -> solid vertical bar 不闪烁的竖线

模板类和模板函数

定义模板类时,函数声明和实现都必须放在头文件里,否则编译失败,准确地讲,是在链接阶段报错.原因在于编译器虽然也编译了包含模板定义的源码文件,但是该文件仅仅是模板的定义,而并没有真正的实例化出具体的函数来。因此在链接阶段,编译器进行符号查找时,发现源码文件中的符号,在所有二进制文件中都找不到相关的定义,因此就报错了。
不过,函数的实现可以放在头文件的类外,不一定必须在类体内.

参考:
C++模板编译模型


容器类和allocator

new运算是先分配内存再执行构造函数,delete是先执行析构函数再释放内存。

STL allocator将这些操作做了精密分工:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责; 对象构造由::construct()负责,对象析构由::destroy()负责。

STL采用了两级配置器,当分配的空间大小超过128字节时,会使用第一级空间配置器,直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放;当分配的空间小于128字节时,为减少申请小内存造成的内存碎片和额外负担问题,将使用第二级空间配置器,它采用了内存池技术,通过16个free-list来配置和回收内存,free-list对内存的需求量按8的倍数处理,也就是16个free-list分别管理8,16,24……128字节的内存区块。


C++所有的标准容器类都接受一个allocator类作为其模板参数;这个参数有一个默认值,比如std::vector是 vector >的简写。

1
2
3
4
template <class T, class Alloc = alloc>  // 预设使用 alloc 为配置器
class vector {

}

1
2
std::string s="123";
printf("%s",s);

结果报错: cannot pass non-trivial object of type ‘std::string’ (aka ‘basic_string, allocator >’) to variadic function; expected type from format string was char*

也有可能不报错但是显示不正常,这跟编译器有关。


如何让回调函数与main函数互相传值

在main函数中访问回调函数中处理的变量

做法是让类的成员函数做回调函数,在回调函数中处理相应变量,在main函数中实例化类,然后通过返回对象的成员进行访问

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
class Listener 
{
public:
double theta1f;
double theta2f;
double theta3f;

void callback(const std_msgs::Float32MultiArray::ConstPtr& msg);
};

void Listener::callback(const std_msgs::Float32MultiArray::ConstPtr& msg)
{
theta1f = msg->data[0];
theta2f = msg->data[1];
theta3f = msg->data[2];
}

int main(int argc, char **argv)
{
ros::init(argc, argv, "node");
ros::NodeHandle nh;
ros::Rate loop_rate(30);
Listener listener;
ros::Subscriber sub = nh.subscribe<std_msgs::float32multiarray>("topic_subscribed", 1, &Listener::callback, &listener);
while (ros::ok())
{
ros::spinOnce();
ROS_INFO("This is theta1f: %.2f", listener.theta1f);
ROS_INFO("This is theta2f: %.2f", listener.theta2f);
ROS_INFO("This is theta2f: %.2f", listener.theta2f);
loop_rate.sleep();
}

return 0;
}



回调函数中访问main函数中的变量赋值

以前我都是用全局变量来实现,但这种方法总感觉不保险,后来发现ROS中有专门的解决方法,NodeHandle::subscribe有一个重载可以处理这种情况,用到了boost::bind,其实用std::bind也能实现。代码如下:

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
#include <ros/ros.h>
#include <std_msgs/Int8.h>
//回调函数里访问main中的一个结构体
typedef struct _Struct
{
int m;
double n;
} myStruct;

void callback(const std_msgs::Int8ConstPtr& msg, myStruct p)
{
ROS_INFO("msg: %d", msg->data);
ROS_INFO("m: %d",p.m);
ROS_INFO("n: %f",p.n);
sleep(1);
}


int main(int argc, char** argv)
{
ros::init(argc,argv, "Sub");
ros::NodeHandle nh;

myStruct p;
p.m = 7;
p.n = 1.85;

// "_1" 是占位符,对应 void callback(const std_msgs::Int8ConstPtr& msg, myStruct p)第一个参数
/* subscribe (
* const std::string &base_topic,
* uint32_t queue_size,
* const boost::function< void(const std_msgs::Int8ConstPtr &)> &callback )
*/
ros::Subscriber sub = nh.subscribe<std_msgs::Int8>("topic", 20, boost::bind(&callback, _1, p) );
ros::spin();
return 0;
}

myStruct是我自定义的结构体,在main函数里对其声明的变量赋值,回调的形参里多了它的变量。
其中subscribe函数的原型是这个:

1
2
3
4
5
6
7
template<class M >
Subscriber ros::NodeHandle::subscribe(const std::string& topic,
uint32_t queue_size,
const boost::function< void(const boost::shared_ptr< M const > &)> & callback,
const VoidConstPtr & tracked_object = VoidConstPtr(),
const TransportHints & transport_hints = TransportHints()
)

boost::function对象能接受函数和仿函数, boost::bind创建了仿函数,包含了所有想传给回调函数的参数。

与平时的重载形式不同,这次要用到话题的模板类型,第三个形参不是回调函数的指针,而是boost::bind,其中_1是一个占位符,要传给回调函数参数,在发生函数调用时才接受实参;最后是结构体的变量。

注意:回调函数第一个形参必须是const std_msgs::Int8ConstPtr&形式,其他形式比如const std_msgs::Int8Ptr&会报错,另外看到一篇博客说这里不能用引用,但是我用引用也没报错。对于回调函数callback,一般在bind里不用&,但是用了也可以,因为bind可接受函数或函数指针

参考:
How to make callback function called by several subscriber?
How to pass arguments to/from subscriber callback functions


使用ROS Service(二) 代码演示

程序演示

下面是我写的一个服务程序,服务文件ctrl.srv的构成为:

1
2
3
int8 cmd
---
bool ret

客户端发出命令,如果cmd!=0,服务端会打开摄像头程序;如果cmd=0,服务端会关闭摄像头程序。

注意:最好是将所有自定义的服务和消息文件放到一个单独的package里面,否则会出现一个package的修改会影响到另一个用到它的消息/服务的package的编译.

新建srv文件后,容易忘记在CMakeLists里添加这个文件,导致编译失败

服务端

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
#include <ros/ros.h>
#include <riki_msgs/ctrl.h>

bool control(riki_msgs::ctrl::Request &req, riki_msgs::ctrl::Response &res)
{
if(req.cmd==0)
{
ROS_INFO("shutting down camera");
system("rosnode kill /usb_cam");
ROS_INFO("close camera done");
res.ret = 0;
}
else
{
ROS_INFO("starting camera");
system("roslaunch usb_cam usb_cam.launch & ");
ROS_INFO("camera is up");
res.ret = 1;
}
}

int main(int argc, char** argv)
{
ros::init(argc,argv,"camServer");
ros::NodeHandle nh;
ros::ServiceServer server = nh.advertiseService("control_cam",control);
ROS_INFO("------ waiting for client's request ------");
ros::spin();
return 0;
}

可以看出服务端的程序与话题中的订阅者程序高度相似,control函数就是个回调函数。

system函数调用roslaunch时,由于是在fork出的子进程里执行,launch的节点会一直阻塞不返回,在命令最后加&,让子进程返回

service回调函数里只能return truefalse,若return整数会导致客户端的call结果为false,但实际成功,这样会影响判断。

service客户端发命令后,出现报错 ERROR: service [/service_name] responded with an error: b’’ ,原因在于service服务端的回调函数必须 return true


advertiseService的部分源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool ServiceManager::advertiseService(const AdvertiseServiceOptions& ops)
{
boost::recursive_mutex::scoped_lock shutdown_lock(shutting_down_mutex_);
if (shutting_down_)
{
return false;
}
{
boost::mutex::scoped_lock lock(service_publications_mutex_);
// 如果service已经发布,就报错,然后返回,其实这里改成报警比较合适
if (isServiceAdvertised(ops.service))
{
ROS_ERROR("Tried to advertise a service that is already advertised in this node [%s]", ops.service.c_str());
return false;
}

如果涉及到耗时的工作,回调函数应该这样写

1
2
3
4
5
6
bool serviceCB(mow_msgs::task::Request &req, mow_msgs::task::Response &res)
{
// ......
res.ret = "to charge";
// 耗时的工作
}

如果把耗时的工作放在res之前,那么客户端提前退出时(比如ctrl+C),会报错没有收到返回值,虽然不影响,但是不优雅。

客户端

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
#include <ros/ros.h>
#include <riki_msgs/ctrl.h>

int main(int argc, char** argv)
{
ros::init(argc, argv, "controlCam");
if(argc!=2)
{
ROS_INFO("client need command");
return 1;
}
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<riki_msgs::ctrl>("control_cam");
riki_msgs::ctrl srv;
srv.request.cmd = atoi(argv[1]);
if(client.exists())
{
ROS_INFO("service control_cam is up");
ROS_INFO("service name:%s",client.getService().c_str());
}
else
{
ROS_ERROR("service control_cam is not available");
return 1;
}
if(client.call(srv))
{
ROS_INFO("client calling srv !");
if(srv.request.cmd>0)
ROS_INFO("Sending command start!");
else
ROS_INFO("Sending command stop!");
}
else
{
ROS_ERROR("client calls srv failed !");
return 1;
}
// 调用成功以后才能获取response
cout<<"服务端的返回值: "<<srv.response.ret<<endl;
return 0;
}

构建client的时候后面的路径要写绝对路径,有时候需要加个 /

客户端的程序与话题发布者的程序高度相似,exists()检查服务端的服务是否启动,若未启动则返回。call调用了服务,成功会返回true而且调用完成后,可以在客户端程序里获得服务端的返回值

最后,先运行服务端,然后运行客户端,如果是rosrun control_cam controlCam 1则在服务端所在终端启动摄像头程序,若是0则关闭。

另一种使用方式

1
2
3
4
5
6
7
8
9
std_srvs::Empty srv;
ros::service::call("/move_base_node/clear_costmaps", srv);

while(!ros::service::call("static_map", req, resp))
{
ROS_WARN("Request for map failed; trying again...");
ros::Duration d(0.5);
d.sleep();
}

常用函数

ros::ServiceClient常用函数:

1
2
3
4
5
6
7
8
9
bool call (Service &service)  //调用service,client发起通信。成功返回true

bool exists () //检查相应名称的服务是否可用

std::string getService () //返回客户端通信的服务名称
// 单例模式
static const ServiceManagerPtr & instance ()

bool unadvertiseService (const std::string &serv_name)

如果要解除service,用法是ros::ServiceManager::instance()::unadvertiseService,也就是使用单例模式进行全局访问。 API说isServiceAdvertised可以判断某service是否发布,但可惜是private

问题


切换算法时,会有个报警 Tried to advertise a service that is already advertised in this node。这个其实无任何影响,报警在ROS底层的源码,原因在于GlobalPlanner::initialize函数有一句:

1
make_plan_srv_ = private_nh.advertiseService("make_plan", &GlobalPlanner::makePlanService, this);

这里是已经注册了service,如果想要去掉这个报警,可以这样改:
1
2
3
4
std::string makePlanServiceName = "/move_base/"+name+"/make_plan";

ros::ServiceManager::instance()->unadvertiseService(makePlanServiceName);
make_plan_srv_ = private_nh.advertiseService("make_plan", &GlobalPlanner::makePlanService, this);

其中的bool ros::ServiceManager::unadvertiseService(const std::string& serv_name)作用是 Unadvertise a service. This call unadvertises a service, which must have been previously advertised, using advertiseService().

md5不匹配

调用service时报错md5不匹配,其实是调用失败了,call的返回值是false,首先应检查客户端定义是否正确和服务是否存在:

客户端一直阻塞

我在move_base.cpp里写了一个函数 func,是rosservice的客户端,调用service clear_costmaps。在MoveBase的其他函数里,调用这个函数,结果发现会一直阻塞。后来注意到服务端程序就是MoveBase::clearCostmapsService,也就是在同一个类里

客户端call service阻塞的原因只有两个:

  1. 多个客户端call 同一个service,而服务端一次只能处理一个请求
  2. 服务端程序没有return值

服务端程序是MoveBase的,而且有了返回值,那么只可能是第一个原因了。

参考:blocking service callbacks


VMWare常见问题

无法进入图形界面

![https://live.staticflickr.com/7870/46689319155_bc1d198671_z.jpg]
![https://live.staticflickr.com/7892/33727819068_7e69d575f5.jpg]

1.Ctrl+ALt+F1(F1~F5都可以)进入控制台

2.输入用户名和密码进入系统;

3.依次输入以下命令

1
2
3
4
5
cd /etc/X11

sudo cp xorg.conf.failsafe xorg.conf

sudo reboot

开机启动VMWare并启动指定系统

将VMWare的快捷方式放到C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp,然后打开快捷方式,修改目标

改成

1
"....exe"  -x  "/path_to_system/Ubuntu 64.vmx"

其中x是小写,后面加vmx文件的路径,而且加双引号

如何让虚拟机网段与主机相同

先用管理员身份打开VMWare,再打开编辑-虚拟网络编辑器,将网络做如图设置:桥接模式,并选择网卡为主机真实网卡。这样进入虚拟机后,IP就同主机了,设置成DHCP即可。

无法打开虚拟机,提示要获得所有权,但还是失败

很简单,删除虚拟机文件夹下的lck文件夹即可,映像被lck锁定了。应当是上次没有正常关闭虚拟机导致的

硬盘空间太少

向虚拟机复制文件就是先在cache文件夹里面生成一个同样的文件,并使用拷贝的方式将其拷贝到拖拽放置的目录中。因此,如果不进行清理的话,cache文件夹中产生的文件,并不会自动删除或者释放。

该文件夹位于用户目录下/home/xxxx/.cache/vmware/drag_and_drop进入文件夹,可以见到每一次拖拽产生的文件,都在子文件夹中有一份。直接删除便可以腾出海量的空间。

vmware tool安装后仍然不能复制的问题

1
2
3
4
sudo apt-get upgrade
sudo mkdir /mnt/cdrom
sudo apt-get install open-vm-tools-desktop -y
sudo reboot

VM虚拟机增加磁盘空间

VM虚拟机增加磁盘空间

VMWare 分辨率设置

参考链接

VMware连接USB设备,即使连接到了主机,也可切换到虚拟机


ROS机器人的开发心得体会
abstract Welcome to my blog, enter password to read.
Read more