概述
在之前的文章中(《python-onvif实现客户端控制相机云台》),介绍过用python实现基于onvif协议的相机云台控制,考虑到嵌入式端的执行效率问题,还是需要实现C/C++版本的接口,因此尝试这方面的工作。经过将近一周的折腾,终于调通了onvif协议云台控制的代码,里边遇坑无数,从一个对onvif一无所知的小白,到最后顺利调通功能,还是有所收获的,将过程记录下来,给其他同学减少入坑的次数,目的也就达到了。话不多说,直接进入正题
1 预备知识
关于onvif的背景介绍,网上有很多资料,我这里就不具体展开,我之前也转载过一篇,《基于ONVIF协议的摄像头开发总结》,对onvif的原理有一个大概的介绍,而且许振坪博主的专栏也介绍的很详细,对onvif不了解的可以参考他的前几篇介绍文章。
2 开发流程
首先保证网络相机支持onvif协议,并且启用该协议,设置方法参考《海康相机之onvif测试工具使用》;
然后使用gsoap工具生成onvif代码框架;编写客户端程序,实现云台控制,客户端程序的调用流程会在后边详细介绍。
我要实现的是在嵌入式linux上开发客户端,来实现网络摄像头的云台控制。其它平台如x86、windows等平台流程类似,也同样可以采用类似的流程。
3 gsoap安装
用gsoap生成onvif代码框架的时候,主要用到wsdl2h和soapcpp2两个可执行文件,在gsoap/bin目录下,官方已经编译好win32和macosx平台的可执行文件,但linux下需要自己编译,因此我们使用源码编译的方法来生成这两个工具。
3.1 环境依赖
在编译之前,需要先安装OpenSSL,以便得到开启SSL/TLS的wsdl2h文件。建议先把系统中的SSL卸载掉重新安装,避免后边报错,卸载方法
sudo apt-get purge openssl
下载链接:https://www.openssl.org/source/,下载1.0.2版本并解压到home目录下
进入目录安装
cd /openssl-1.0.2r
./config
sudo make
sudo make install
3.2 安装gsoap
下载地址:https://sourceforge.net/projects/gsoap2/
下载后解压到home目录下,然后按照以下指令安装
$ cd gsoap-2.8
$ sudo make distclean
$ sudo ./configure --with-openssl=/usr/local/ssl/lib --host=arm-linux
$ sudo make
$ sudo make install
说明:
--with-openssl的路径要与自己环境中的路径保持一致,如果是按照我上边源码安装SSL的方法,路径就是/usr/local/ssl/lib。
--host用来指定生成Gsoap工具的编译器,如果是在x86上面执行的,则默认的就可以,即不用配置。
--prefix用来指定生成工具的路径,采用默认即可。
查看安装路径
$ which wsdl2h
/usr/local/bin/wsdl2h
找到文件夹/usr/local/bin,找到wsdl2h和soapcpp2这两个可执行文件,拷贝到/home/gsoap-2.8/gsoap/bin路径下,方便我们使用。
4 gsoap生成代码框架
经过上边的步骤,环境已经准备完毕,接下来就可以生成代码框架了
4.1 wsdl文件准备
下载地址:https://www.onvif.org/profiles/specifications/
在Onvif官网Specification页面中下载提供的功能相应的wsdl文件,如analytics.wsdl;devicemgmt.wsdl等。直接将WSDL的链接另存为,保存下来就是wsdl文件了。因为不太确定哪些文件不需要,所以我这里全部都下载了,全部放在bin下新建的wsdl文件夹内,包括这些wsdl中需要调用的xsd文件,
在gsoap2.8/gsoap/bin目录下,新建一个文件夹wsdl,用来存放刚才下载的所有wsdl和xsd文件。
4.2 onvif.h文件生成
这一步我们用到的工具是wsdl2h,我们要实现PTZ控制,有几个主要的wsdl文件就够了,我用到的是device.wsdl、event.wsdl、media.wsdl、ptz.wsdl,cd进入bin目录,使用如下指令
./wsdl2h -c -t ../typemap.dat -o onvif.h ./wsdl/devicemgmt.wsdl ./wsdl/event.wsdl ./wsdl/media.wsdl ./wsdl/ptz.wsdl
各个选项的含义,可通过wsdl2h -help查看帮助。其中-c为产生纯c代码,默认为c++代码,-t为typemap.dat的标识。
有一个小技巧,如果网络条件不好的话,可以把wsdl文件中的schemaLocation的路径修改为本地文件路径,前提是下载好对应的文件,这样会速度快一些。
运行成功后,生成onvif.h文件,由于摄像头需要鉴权认证,需要在该文件头部添加如下代码
#import "wsse.h"
4.3 c文件生成
接下来使用onvif.h文件来生成c文件,用到的工具是soapcpp2,运行如下代码
./soapcpp2 -c -x onvif.h -I ../ -I ../import -I ../custom
其中,-c表示只生成c代码,-x表示不要产生XML示例文件,-I是包含的路径,运行成功后,会生成如下文件
soapC.c、soapH.h、soapClient.c、soapClientLib.c、soapStub.h、soapServer.c、soapServerLib.c、*.nsmap
新建一个文件夹onvif,把生成的这些文件全部拷贝到onvif文件夹中,其中*.nsmap文件内容都一样,我们只保留一个PTZBingding.nsmap即可。
5 PTZ控制
5.1 环境准备
在onvif文件夹中,继续添加文件,cd到上一级目录,直接使用cp命令来复制
$ cd gsoap-2.8/gsoap
$ cp stdsoap2.c stdsoap2.h dom.c plugin/wsaapi.c plugin/wsaapi.h plugin/wsseapi.c plugin/wsseapi.h plugin/mecevp.c plugin/mecevp.h plugin/smdevp.c plugin/smdevp.h plugin/threads.c plugin/threads.h custom/duration.c custom/duration.h bin/onvif/
最终onvif文件夹中的文件内容如下
dom.c onvif.h soapC.c soapServerLib.c threads.h
duration.c PTZBinding.nsmap soapClient.c soapStub.h wsaapi.c
duration.h README.txt soapClientLib.c stdsoap2.c wsaapi.h
mecevp.c smdevp.c soapH.h stdsoap2.h wsseapi.c
mecevp.h smdevp.h soapServer.c threads.c wsseapi.h
5.2 控制流程
1、通过设备服务地址(形如http://xx/onvif/device_service),调用GetCapabilities函数接口,获取到Media的URL;
2、通过Media的URL,调用GetProfiles函数接口,获取到ProfileToken;
3、对_tptz__AbsoluteMove结构体进行填充;
4、调用soap_call___tptz__AbsoluteMove函数接口实现摄像头转动功能;
5.3 代码实现
在bin文件夹下新建一个PTZ文件夹,新建myptz.c文件,内容如下
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "soapH.h"
#include "wsseapi.h"
#include "wsaapi.h"
#include "PTZBinding.nsmap"
//宏定义设备鉴权的用户名和密码
//注意对于海康相机而言,鉴权的用户名和密码需要单独设置,不一定等同于登录账户密码
//设置方法参考https://zongxp.blog.csdn.net/article/details/89632354
#define USERNAME "admin"
#define PASSWORD "123456"
int main(int argc, char** argv)
{
struct soap soap;
soap_init(&soap);
char * ip;
char Mediaddr[256]="";
char profile[256]="";
float pan = 1;
float panSpeed = 1;
float tilt = 1;
float tiltSpeed = 0.5;
float zoom = 0;
float zoomSpeed = 0.5;
struct _tds__GetCapabilities req;
struct _tds__GetCapabilitiesResponse rep;
struct _trt__GetProfiles getProfiles;
struct _trt__GetProfilesResponse response;
struct _tptz__AbsoluteMove absoluteMove;
struct _tptz__AbsoluteMoveResponse absoluteMoveResponse;
req.Category = (enum tt__CapabilityCategory *)soap_malloc(&soap, sizeof(int));
req.__sizeCategory = 1;
*(req.Category) = (enum tt__CapabilityCategory)0;
//第一步:获取capability
char endpoint[255];
memset(endpoint, '