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 msg 所生成的 python 对应的代码,会发现其实主要做了以下几件事:

继承了 genpy.Message。在使用时会强制校验是否是 genpy.Message 的子类
实现了 serialize 方法,对内容进行序列化
实现了 deserialize 方法,对内容进行反序列化

ctrl.msg的内容如下:

1
2
3
int8 cmd
---
bool ret

经过catkin_make后,生成的头文件在devel/include,内容如下:

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// Generated by gencpp from file riki_msgs/ctrl.msg
// DO NOT EDIT!

#ifndef RIKI_MSGS_MESSAGE_CTRL_H
#define RIKI_MSGS_MESSAGE_CTRL_H

#include <ros/service_traits.h>
#include <riki_msgs/ctrlRequest.h>
#include <riki_msgs/ctrlResponse.h>

namespace riki_msgs
{
struct ctrl
{
typedef ctrlRequest Request;
typedef ctrlResponse Response;
Request request;
Response response;

typedef Request RequestType;
typedef Response ResponseType;
}; // struct ctrl
} // namespace riki_msgs

namespace ros
{
namespace service_traits
{
template<>
struct MD5Sum< ::riki_msgs::ctrl >
{
static const char* value()
{
return "cb6150bb24e971aa4496e4ada0219f83";
}
static const char* value(const ::riki_msgs::ctrl&) { return value(); }
};

template<>
struct DataType< ::riki_msgs::ctrl >
{
static const char* value()
{
return "riki_msgs/ctrl";
}
static const char* value(const ::riki_msgs::ctrl&) { return value(); }
};

// service_traits::MD5Sum< ::riki_msgs::ctrlRequest> should match
// service_traits::MD5Sum< ::riki_msgs::ctrl >
template<>
struct MD5Sum< ::riki_msgs::ctrlRequest>
{
static const char* value()
{
return MD5Sum< ::riki_msgs::ctrl >::value();
}
static const char* value(const ::riki_msgs::ctrlRequest&)
{
return value();
}
};

// service_traits::DataType< ::riki_msgs::ctrlRequest> should match
// service_traits::DataType< ::riki_msgs::ctrl >
template<>
struct DataType< ::riki_msgs::ctrlRequest>
{
static const char* value()
{
return DataType< ::riki_msgs::ctrl >::value();
}
static const char* value(const ::riki_msgs::ctrlRequest&)
{
return value();
}
};

// service_traits::MD5Sum< ::riki_msgs::ctrlResponse> should match
// service_traits::MD5Sum< ::riki_msgs::ctrl >
template<>
struct MD5Sum< ::riki_msgs::ctrlResponse>
{
static const char* value()
{
return MD5Sum< ::riki_msgs::ctrl >::value();
}
static const char* value(const ::riki_msgs::ctrlResponse&)
{
return value();
}
};

// service_traits::DataType< ::riki_msgs::ctrlResponse> should match
// service_traits::DataType< ::riki_msgs::ctrl >
template<>
struct DataType< ::riki_msgs::ctrlResponse>
{
static const char* value()
{
return DataType< ::riki_msgs::ctrl >::value();
}
static const char* value(const ::riki_msgs::ctrlResponse&)
{
return value();
}
};

} // namespace service_traits
} // namespace ros

#endif // RIKI_MSGS_MESSAGE_CTRL_H

在msg文件中经常看到这样的成员类型:geometry_msgs/PoseStamped[] poses, 这样直接看不出它在C++中的类型,可以去它生成的头文件里查看,结果发现是vector. 可以得出结论, ROS消息中凡是带有 [] 的成员都是vector


使用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设备,即使连接到了主机,也可切换到虚拟机


使用Docker部署ROS程序

docker常用命令

命令 功能
docker version 查看docker的版本
docker info 查看docker的详细信息
docker images 查看本地所有镜像
docker pull 镜像名 拉取镜像
docker push 镜像名 推送镜像
docker images nginx 查看本地与nginx的相关的镜像
docker ps -a 查看当前所有容器的状态(包括没有运行的)
docker ps 查看当前正在运行的容器的状态
docker network ls 列出当前有哪些网络类型
docker container ls 查看正在运行的容器
docker build -t image . 用当前目录下Dockerfile创建镜像image
docker run -it —name vm1 image 使用镜像image创建容器vm1,并运行,并进入交互界面
docker run -it —rm image 用image镜像创建容器,并运行,运行完成之后,立即删除
docker inspect container 查看容器container的详情
docker inspect image 查看镜像image的详情
docker search 镜像名的一部分 查找镜像
docker rmi image 删除镜像image
docker rm container 删除容器container
docker stop vm1 停止容器vm1(容器vm1存在,并运行)
docker start vm1 启动容器vm1(容器vm1存在,但没有运行)
docker kill vm1 强制干掉容器vm1(容器vm1存在,并运行)
docker top vm1 查看容器vm1的进程

运行docker run image-name命令后,docker会先从本地查找这个image,如果没有,就会从Docker Hub查找,有则下载然后运行,如果没有就会报错返回。

由于Hyper-V,docker与VMWare不兼容,不能同时开启

编译docker image

1
2
3
FROM ros:kinetic-ros-base
# install ros tutorials packages
RUN apt-get update && apt-get install -y ros-kinetic-ros-tutorials ros-kinetic-common-tutorials && rm -rf /var/lib/apt/lists/

命令只有两行,FROM一行表示引入的基础镜像,这个是Docker Hub上已有的,第二行是镜像中运行的命令,从网上下载几个kinetic的官方package

编译image: docker build --tag ros:ros-tutorials .,现在image中有ros-tutorials这个package了

创建虚拟网络

使用命令docker network create foo创建网络foo

运行roscore

1
docker run -it --rm  --net foo --name master ros:ros-tutorials roscore

结果是这样的,ROS_MASTER_URI实际是镜像ID,重启镜像后就变了。不过没关系,其他节点不必每次都用这个ID,用master这个名称就行

执行rostopic

运行了roscore之后,执行命令docker exec -it master bash,进入了ROS环境,master是上面—name的参数。进入ROS后,执行命令
source /ros_entrypoint.sh或者source /opt/ros/kienetic/setup.bash

现在可以使用ROS命令了,rostopic list会显示所有话题

执行Publisher

1
docker run -it --rm --net foo --name talker --env ROS_HOSTNAME=talker --env ROS_MASTER_URI=http://master:11311 ros:ros-tutorials rosrun roscpp_tutorials talker

执行Subscriber

1
docker run -it --rm --net foo --name listener --env ROS_HOSTNAME=listener --env ROS_MASTER_URI=http://master:11311 ros:ros-tutorials rosrun roscpp_tutorials listener

使用ROS控制海康威视网络摄像头(二)

控制摄像头看海康威视的SDK即可,基本流程图如下,适用于多种功能:

其中注册设备现在用的是新函数NET_DVR_Login_V40,注册成功后,返回的用户 ID 作为功
能操作的唯一标识。

还有个重要函数NET_DVR_GetLastError(),返回最后操作的错误码,有网络通信、rtsp、解码错误。

控制摄像头用的是NET_DVR_SetDVRConfig函数,用到的参数有用户ID,设备配置命令(宏)等等;相对地,有函数NET_DVR_GetDVRConfig,可以获得PTZ状态,详见SDK。

最后还有个很关键的参数,就是两个函数中用到的位置结构体:

1
2
3
4
5
6
struct{
WORD wAction; //操作类型,仅在设置时有效。1-定位PTZ参数,2-定位P参数,3-定位T参数,4-定位Z参数,5-定位PT参数
WORD wPanPos; //P参数(水平参数)
WORD wTiltPos; //T参数(垂直参数)
WORD wZoomPos; //Z参数(变倍参数)
}NET_DVR_PTZPOS, *LPNET_DVR_PTZPOS;

本结构体中的wAction参数是设置时的操作类型,因此获取时该参数无效。实际显示的PTZ值是获取到的十六进制值的十分之一,如获取的水平参数P的值是0x1750,实际显示的P值为175度;获取到的垂直参数T的值是0x0789,实际显示的T值为78.9度;获取到的变倍参数Z的值是0x1100,实际显示的Z值为110倍。

下面是一个范例,摄像头向左右、上下转一定角度、焦距放大3倍,然后不断检测当前的定位和焦距,按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
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
97
98
#include <ros/ros.h>
#include <signal.h>
#include <camera/camera.h>
#include "HCNetSDK.h"
#include <iostream>
#include <iomanip>

using namespace std;
bool EXIT = false;

//转换函数
unsigned long HexToDecMa(unsigned long wHex)
{
return (wHex / 4096) * 1000 + ((wHex % 4096) / 256) * 100 + ((wHex % 256) / 16) * 10 + (wHex % 16);
}

void CALLBACK g_ExceptionCallBack(DWORD dwType, LONG lUserID, LONG lHandle, void *pUser)
{
switch(dwType)
{
case EXCEPTION_RECONNECT: //预览时重连
ROS_INFO("----------reconnecting--------");
break;
default:
break;
}
}

int main(int argc, char *argv[])
{
ros::init(argc,argv,"hikvision_camera");
ros::NodeHandle nh;
// 初始化
NET_DVR_Init();
//设置连接时间与重连时间
NET_DVR_SetConnectTime(10000, 1);
NET_DVR_SetReconnect(10000, true);
// 初始化设备参数
LONG lUserID;
NET_DVR_USER_LOGIN_INFO struLoginInfo = {0};
struLoginInfo.bUseAsynLogin = 0; //同步登录方式
strcpy(struLoginInfo.sDeviceAddress, "192.168.73.64"); //设备IP地址
struLoginInfo.wPort = 8000; //设备服务端口
strcpy(struLoginInfo.sUserName, "admin"); //设备登录用户名
strcpy(struLoginInfo.sPassword, "admin1234"); //设备登录密码
//设备信息, 输出参数
NET_DVR_DEVICEINFO_V40 struDeviceInfoV40 = {0};
// 注册设备
lUserID = NET_DVR_Login_V40(&struLoginInfo, &struDeviceInfoV40);
if (lUserID < 0)
{
printf("Login error, %d\n", NET_DVR_GetLastError());
NET_DVR_Cleanup();
return -1;
}
//设置异常消息回调函数
NET_DVR_SetExceptionCallBack_V30(0, NULL,g_ExceptionCallBack, NULL);

if (!NET_DVR_SetCapturePictureMode(BMP_MODE))
{
cout << "Set Capture Picture Mode error!" << endl;
cout << "The error code is " << NET_DVR_GetLastError() << endl;
}
//读取PTZ
NET_DVR_PTZPOS m_ptzPosCurrent,m_ptzPos;
m_ptzPosCurrent.wAction = 1;
DWORD dwtmp=0;

m_ptzPos.wAction = 1;
m_ptzPos.wPanPos = 0x0750;
m_ptzPos.wTiltPos = 0x0550;
m_ptzPos.wZoomPos = 0x0010;
//设置PTZ参数
if (!NET_DVR_SetDVRConfig(lUserID, NET_DVR_SET_PTZPOS, 1, &m_ptzPos, sizeof(NET_DVR_PTZPOS)))
{
ROS_INFO("Set device config error:%d",NET_DVR_GetLastError());
return -1;
}
while (!EXIT)
{
//获取当前PTZ状态
if(!NET_DVR_GetDVRConfig(lUserID, NET_DVR_GET_PTZPOS, 1, &m_ptzPosCurrent, sizeof(NET_DVR_PTZPOS), &dwtmp))
{
ROS_INFO("Get PTZ failed:%d",NET_DVR_GetLastError());
break;
}
//打印PTZ
int m_iPara1 = HexToDecMa(m_ptzPosCurrent.wPanPos);
int m_iPara2 = HexToDecMa(m_ptzPosCurrent.wTiltPos);
int m_iPara3 = HexToDecMa(m_ptzPosCurrent.wZoomPos);

cout << "action: "<<m_ptzPosCurrent.wAction<<endl; //一直是1
cout << "P: " << m_iPara1 / 10 + 1 << endl;
cout << "T: " << m_iPara2 / 10 + 1<< endl;
cout << "Z: " << m_iPara3 / 10 << endl;
}
return 0;
}

结果运行后发现只有焦距设置和检测成功,两个方向的角度一直是0,反复检查都未发现问题,最后咨询海康威视的技术支持,发现设备竟然不支持这个功能。只好先记录下来,以防以后使用。

串转网的控制协议

另一种控制摄像头的方式是使用串口,基于Pelco-D协议,从程序向串口发命令进行控制。方便的是,厂家在摄像头内部实现了一个串口转网口的Pelco-D协议,我们不必考虑这个协议了,它使用的是UDP方式,向摄像机的10006端口发7个字节的十六进制数据,摄像头会反馈7个字节十六进制数据,从中可获得摄像头水平和垂直坐标,另外也可进行转动控制。

但这种方法归根到底是使用了串口,所以我们要开发的是一个“像”串口调试助手的程序,而不是“像”UDP调试助手的程序。程序不是要监听摄像头的1006端口,而是用UDP连接它之后,发数据(以十六进制形式,很重要),等待反馈后进行解析。

PELCO-D协议格式:
数据格式:8位数据、1位停止位,无效验位。波特率:2400-9600b/S

命令格式:
字节1 字节2 字节3 字节4 字节5 字节6 字节7
同步字节 地址码 指令码1 指令码2 数据码1 数据码2 校验码

  1. 该协议中所有数值都为十六进制数.
  2. 同步字节始终为0xFF.
  3. 地址码为摄像机的逻辑地址号,地址范围:01H–FFH  
  4. 校验码 = MOD[(字节2 + 字节3 + 字节4 + 字节5 + 字节6)/100H]

绝对坐标协议:
查询水平坐标: 向端口发送: FF 01 00 51 00 00 52
坐标反馈: FF 01 00 59 XX YY 校验码, 例如:FF 01 00 59 23 E4 61

问题

  • 海康sdk库调用NET_DVR_CaptureJPEGPicture函数出现错误码107

这个错误码是预览组件加载失败,如果HCNetSDKCom目录以及libhcnetsdk.so、libhpr.so、libHCCore文件和可执行文件在同一级目录下,则使用同级目录下的库文件。如果不在同一级目录下,则需要将以上文件的目录加载到动态库搜索路径中:在环境变量里添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX:/XXX/HCNetSDKCom

  • NET_DVR_GetLastError错误码4

通道号错误。打开网页设置通道,然后再运行程序

  • NET_DVR_GetLastError错误码9

从设备接收数据失败,原因很简单,就是网络状况不好

  • NET_DVR_GetLastError错误码47

含义是:用户不存在。注册的用户 ID 已注销或不可用。同一个程序,控制摄像头转动,在我的电脑能用,换到工控机上报次错误。重新注册用户仍然报错。结果发现链接的库目录没有HCNetSDKCom时,报错107;有它的时候,报错47. 更奇怪的是,随便填用户名和密码时,不会报登录错误,此时获得的userID仍是0,正常来说,如果登录信息有错,userID应该为负。

查来查去,不明白为什么工控机与我的电脑有何不同。最后把以下文件夹和文件都和可执行文件放到一起,在CMakeList里链接相应地址,其中CMakeList链接的库文件是 libHCCore.so, libhpr.so, libPlayCtrl.so, libhcnetsdk.so , 这种情况下不设置环境变量也行

  • 调用NET_DVR_PTZControlWithSpeed_Other出现错误码107

使用函数NET_DVR_PTZControlWithSpeed_Other控制摄像头时,报错误码107,对应信息是要求预览组件,但是这个函数是不用预览的,最后重启摄像头又正常了.

  • 摄像头对焦失败

结果造成Tag识别失败,还会影响数据。有时画面甚至一直模糊,无法自动对焦成功。在摄像头旋转和放缩中都会出现这种情况。

  • 光线问题造成数据获取不准确

光线暗时,对较小的Tag识别不够准,可以使用SDK中的曝光

参考:
海康sdk库调用NET_DVR_CaptureJPEGPicture函数出现错误码107
海康SDK预览组件加载失败