在之前的文章中(《python-onvif实现客户端控制相机云台》),介绍过用python实现基于onvif协议的相机云台控制,考虑到嵌入式端的执行效率问题,还是需要实现C/C++版本的接口,因此尝试这方面的工作。经过将近一周的折腾,终于调通了onvif协议云台控制的代码,里边遇坑无数,从一个对onvif一无所知的小白,到最后顺利调通功能,还是有所收获的,将过程记录下来,给其他同学减少入坑的次数,目的也就达到了。话不多说,直接进入正题
关于onvif的背景介绍,网上有很多资料,我这里就不具体展开,我之前也转载过一篇,《基于ONVIF协议的摄像头开发总结》,对onvif的原理有一个大概的介绍,而且许振坪博主的专栏也介绍的很详细,对onvif不了解的可以参考他的前几篇介绍文章。
首先保证网络相机支持onvif协议,并且启用该协议,设置方法参考《海康相机之onvif测试工具使用》;
然后使用gsoap工具生成onvif代码框架;编写客户端程序,实现云台控制,客户端程序的调用流程会在后边详细介绍。
我要实现的是在嵌入式linux上开发客户端,来实现网络摄像头的云台控制。其它平台如x86、windows等平台流程类似,也同样可以采用类似的流程。
用gsoap生成onvif代码框架的时候,主要用到wsdl2h和soapcpp2两个可执行文件,在gsoap/bin目录下,官方已经编译好win32和macosx平台的可执行文件,但linux下需要自己编译,因此我们使用源码编译的方法来生成这两个工具。
在编译之前,需要先安装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
下载地址: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路径下,方便我们使用。
经过上边的步骤,环境已经准备完毕,接下来就可以生成代码框架了
下载地址: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文件。
这一步我们用到的工具是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"
接下来使用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即可。
在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
1、通过设备服务地址(形如http://xx/onvif/device_service),调用GetCapabilities函数接口,获取到Media的URL;
2、通过Media的URL,调用GetProfiles函数接口,获取到ProfileToken;
3、对_tptz__AbsoluteMove结构体进行填充;
4、调用soap_call___tptz__AbsoluteMove函数接口实现摄像头转动功能;
在bin文件夹下新建一个PTZ文件夹,新建myptz.c文件,内容如下
#include
#include
#include
#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, '\0', 255);
if (argc > 1)
{
ip = argv[1];
}
else
{
ip = "192.168.170.248";
}
sprintf(endpoint, "http://%s/onvif/device_service", ip);
soap_call___tds__GetCapabilities(&soap, endpoint, NULL, &req, &rep);
if (soap.error)
{
printf("[%s][%d]--->>> soap result: %d, %s, %s\n", __func__, __LINE__,
soap.error, *soap_faultcode(&soap),
*soap_faultstring(&soap));
}
else
{
printf("get capability success\n");
//printf("Dev_XAddr====%s\n",rep.Capabilities->Device->XAddr);
printf("Med_XAddr====%s\n",rep.Capabilities->Media->XAddr);
//printf("PTZ_XAddr====%s\n",rep.Capabilities->PTZ->XAddr);
strcpy(Mediaddr,rep.Capabilities->Media->XAddr);
}
printf("\n");
//第二步:获取profile,需要鉴权
//自动鉴权
soap_wsse_add_UsernameTokenDigest(&soap, NULL, USERNAME, PASSWORD);
//获取profile
if(soap_call___trt__GetProfiles(&soap,Mediaddr,NULL,&getProfiles,&response)==SOAP_OK)
{
strcpy(profile, response.Profiles[0].token);
printf("get profile succeed \n");
printf("profile====%s\n",profile);
}
else
{
printf("get profile failed \n");
printf("[%s][%d]--->>> soap result: %d, %s, %s\n", __func__, __LINE__,
soap.error, *soap_faultcode(&soap),
*soap_faultstring(&soap));
}
printf("\n");
//第三步:PTZ结构体填充
char PTZendpoint[255];
memset(PTZendpoint, '\0', 255);
sprintf(PTZendpoint, "http://%s/onvif/PTZ", ip);
printf("PTZendpoint is %s \n", PTZendpoint);
absoluteMove.ProfileToken = profile;
//setting pan and tilt
absoluteMove.Position = soap_new_tt__PTZVector(&soap, -1);
absoluteMove.Position->PanTilt = soap_new_tt__Vector2D(&soap, -1);
absoluteMove.Speed = soap_new_tt__PTZSpeed(&soap, -1);
absoluteMove.Speed->PanTilt = soap_new_tt__Vector2D(&soap, -1);
//pan
absoluteMove.Position->PanTilt->x = pan;
absoluteMove.Speed->PanTilt->x = panSpeed;
//tilt
absoluteMove.Position->PanTilt->y = tilt;
absoluteMove.Speed->PanTilt->y = tiltSpeed;
//setting zoom
absoluteMove.Position->Zoom = soap_new_tt__Vector1D(&soap, -1);
absoluteMove.Speed->Zoom = soap_new_tt__Vector1D(&soap, -1);
absoluteMove.Position->Zoom->x = zoom;
absoluteMove.Speed->Zoom->x = zoomSpeed;
//第四步:执行绝对位置控制指令,需要再次鉴权
soap_wsse_add_UsernameTokenDigest(&soap, NULL, USERNAME, PASSWORD);
soap_call___tptz__AbsoluteMove(&soap, PTZendpoint, NULL, &absoluteMove,
&absoluteMoveResponse);
//第五步:清除结构体
soap_destroy(&soap); // clean up class instances
soap_end(&soap); // clean up everything and close socket, // userid and passwd were deallocated
soap_done(&soap); // close master socket and detach context
printf("\n");
return 0;
}
新建一个makefile文件,内容如下
include Makefile.inc
PROGRAM = PTZ
SOURCES += myptz.c
OBJECTS := $(patsubst %.c,$(TEMPDIR)%.o,$(filter %.c, $(SOURCES)))
all: $(OBJECTS_ONVIF) $(OBJECTS_COMM) $(OBJECTS)
$(CC) -o $(PROGRAM) $(OBJECTS_ONVIF) $(OBJECTS_COMM) $(OBJECTS) $(LDLIBS)
clean:
rm -f $(OBJECTS_ONVIF)
rm -f $(OBJECTS_COMM)
rm -f $(OBJECTS)
rm -f $(PROGRAM)
新建Makefile.inc文件
SHELL = /bin/bash
CC := gcc
CPP := g++
LD := ld
AR := ar
STRIP := strip
CFLAGS += -c -g -Wall -DWITH_DOM -DWITH_OPENSSL -DDEBUG
CFLAGS += $(INCLUDE)
# openssl目录名
OPENSSL_DIR = /usr/local/ssl
# 源文件
SOURCES_ONVIF += \
../onvif/soapC.c \
../onvif/soapClient.c \
../onvif/stdsoap2.c \
../onvif/wsaapi.c \
../onvif/dom.c \
../onvif/mecevp.c \
../onvif/smdevp.c \
../onvif/threads.c \
../onvif/wsseapi.c \
# 目标文件
OBJECTS_ONVIF := $(patsubst %.c,$(TEMPDIR)%.o,$(filter %.c, $(SOURCES_ONVIF)))
# 头文件路径
INCLUDE += -I../onvif/ \
-I$(OPENSSL_DIR)/include \
# 静态库链接OpenSSL
LDLIBS += $(OPENSSL_DIR)/lib/libssl.a \
$(OPENSSL_DIR)/lib/libcrypto.a \
-ldl \
# 链接库(其他)
LDLIBS += -lpthread
%.o: %.cpp
@echo " CPP " $@;
@$(CPP) $(CFLAGS) -c -o $@ $<
%.o: %.c
@echo " CC " $@;
@$(CC) $(CFLAGS) -c -o $@ $<
.PHONY: all clean
然后运行make,即可在PTZ文件夹中生成PTZ可执行文件,运行该文件,可在目录下生成log日志文件,如果报错的话,可查看日志文件。注意如果是在嵌入式端部署的话,一定不要加-DDEBUG选项,否则日志文件会越来越大,影响板子性能
有很多onvif接口在调用之前需要鉴权(即调用soap_wsse_add_UsernameTokenDigest(&soap, NULL, USERNAME, PASSWORD)函数),并且鉴权完一次之后还需要重新鉴权,具体可参考《ONVIF协议网络摄像机(IPC)客户端程序开发(9):鉴权(认证)》,总结一下,只有以下接口是可以在不认证的情况下调用,一定要注意。
- GetWsdlUrl
- GetServices
- GetServiceCapabilities
- GetCapabilities
- GetHostname
- GetSystemDateAndTime
- GetEndpointReference
- GetRelayOutputOptions
这个错误很常见,主要是由于访问了没有分配地址的内存导致的,在填充功能函数时,很容易漏掉为必须的结构体分配内存,导致gSoap产生的代码会在不知情的状况下访问该结构体,然后报segmentation fault错误,需要注意这点
本文实现的是PTZ绝对控制,当然onvif还支持其它模式,具体实现代码可参考这个链接进行实现
关于结构体的描述,可参考这篇文章。