gSOAP/linuxで同一LAN内のONVIFカメラをサーチするプログラムを作成

# 目的
最新のgSOAPを使って、同一LAN内のONVIFカメラをサーチする
Raspberry Pi 4で実行できるプログラムをビルドする。

# 環境
+ 実行環境 Raspberry Pi 4 　　インストールとgSOAPツール実行時にインターネット接続状態必要
+ 開発環境 Raspberry Pi 4
+ ONVIF準拠カメラ [TP-Link Tapo C100/A](https://www.amazon.co.jp/gp/product/B085LFQ9BN)

# 開発ツールのインストール
## 各種開発ツール
```
$ sudo apt-get install cmake libxml2-dev uuid-dev libavformat-dev libavcodec-dev libavutil-dev libswresample-dev libswscale-dev libjson-c-dev flex bison libssl-dev dh-autoreconf
```
## gSOAP
[gSOAP Toolkit](https://sourceforge.net/projects/gsoap2/files/latest/download)をSourceForgeからダウンロードしてインストールする。

2021/7/14 にダウンロードしたら↓のファイルだった。
gsoap_2.8.116.zip

Windows 10のPCでダウンロードし、TeratermでRaspberry Pi4にSSH接続し、TeraTerm メニュー→ファイル→SSH SCP　でPCからコピーした。
他の方法でも もちろん可。
例として、ユーザーホームディレクトリの直下の gsoap-2.8 フォルダにインストールする。

```
$ unzip gsoap_2.8.116.zip
$ cd gsoap-2.8/
$ ./configure
$ make
```

makeに約12分かかった。

```
$ sudo make install
```

## gSOAPが入ったかどうかの確認

Geniviaの[Installing gSOAP on Unix/Linux](https://www.genivia.com/downloads.html#unix)には以下の記載があるが…
>After successful configuration and completion of the build steps, the following two tools are produced:
>
gsoap/bin/wsdl2h translator of WSDL/XSD to services and XML data bindings (interface tool)
gsoap/bin/soapcpp2 code generator for services and XML data bindings (implementation tool)

ファイル wsdl2、stdsoapcpp2 は gsoap/bin/ にではなく、 gsoap/ に存在した。

# プログラムの作成

## プログラム用フォルダの作成
例として、ユーザーホームディレクトリの直下に onvif フォルダを作って、そこでプログラムをビルドする。

```
$ cd
$ mkdir onvif
$ cd onvif
```

## typemap.datのカスタマイズ

typemap.datをgSOAPのフォルダから自分のビルド用フォルダにコピる。

```
cp ~/gsoap-2.8/gsoap/typemap.dat .
```

typemap.datの以下の2行のコメントを外す。

```
# xsd__duration = #import "custom/duration.h" | xsd__duration
```
↓

```
xsd__duration = #import "custom/duration.h" | xsd__duration
```



```
# xsd__dateTime = #import "custom/struct_timeval.h" | xsd__dateTime
```
↓

```
xsd__dateTime = #import "custom/struct_timeval.h" | xsd__dateTime
```

## wsdl2hでonvif.hを生成

```
$ wsdl2h -O4 -P -x -o onvif.h \
 http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl \
 http://www.onvif.org/onvif/ver10/media/wsdl/media.wsdl \
 http://www.onvif.org/onvif/ver10/event/wsdl/event.wsdl \
 http://www.onvif.org/onvif/ver10/deviceio.wsdl \
 http://www.onvif.org/onvif/ver20/imaging/wsdl/imaging.wsdl \
 http://www.onvif.org/onvif/ver10/receiver.wsdl \
 http://www.onvif.org/onvif/ver10/search.wsdl \
 http://www.onvif.org/onvif/ver10/network/wsdl/remotediscovery.wsdl
```
※[Geniviaのonvif exmapleのページ](https://www.genivia.com/examples/onvif/index.html)から ほぼコピペしてるが、もう少し減らしても大丈夫かも。


## onvif.hを手修正

```
#import "wsdd10.h"	// wsdd10 = <http://schemas.xmlsoap.org/ws/2005/04/discovery>
```
↓

```
//#import "wsdd10.h"	// wsdd10 = <http://schemas.xmlsoap.org/ws/2005/04/discovery>
#import "wsdd5.h"
#import "wsse.h"
```

## soapcpp2でsoap*.h/soap*.cpp等の作成

```
soapcpp2 -2 -C -I ~/gsoap-2.8/gsoap/import -I ~/gsoap-2.8/gsoap -j -x onvif.h
```

## WS-Discoveryのclientに必要なwsdd*.h/cpp等の作成 

```
soapcpp2 -a -x -L -pwsdd -I ~/gsoap-2.8/gsoap/import ~/gsoap-2.8/gsoap/import/wsdd5.h
```

## wsaapi.cをcppとしてコピー(中身そのまま)

```
cp ~/gsoap-2.8/gsoap/plugin/wsaapi.c wsaapi.cpp
```

## main.cpp を書く

※あくまでも例

```main.cpp
#include "soapDeviceBindingProxy.h"
#include "soapMediaBindingProxy.h"
#include "soapImagingBindingProxy.h"
#include "soapRemoteDiscoveryBindingProxy.h" 
#include "soapDeviceIOBindingProxy.h" 
#include "plugin/wsddapi.h"
#include "plugin/wsseapi.h"
#include "wsdd.nsmap"

#define USERNAME "onvif-camera account" //ONVIFカメラで設定したアカウント
#define PASSWORD "onvif-camera password" //ONVIFカメラで設定したパスワード

int main(int argc, char** argv)
{
    struct soap* serv = soap_new1(SOAP_IO_UDP);
    soap_register_plugin(serv, soap_wsse);
    soap_register_plugin(serv, soap_wsa);
   if (!soap_valid_socket(soap_bind(serv, NULL, 0, 1000)))
    {
        soap_print_fault(serv, stderr);
        exit(1);
    }

    serv->connect_flags = SO_BROADCAST;
    ip_mreq mcast;
    mcast.imr_multiaddr.s_addr = inet_addr("239.255.255.250");
    mcast.imr_interface.s_addr = htonl(INADDR_ANY);
    if (setsockopt(serv->master, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mcast, sizeof(mcast))<0) {
        std::cout << "group membership failed:" << strerror(errno) << std::endl;

    }
    const char* MessageID = soap_wsa_rand_uuid(serv);
    std::cout << "MessageID of Probe:" << MessageID << std::endl;

    int res = soap_wsdd_Probe(serv, SOAP_WSDD_ADHOC, SOAP_WSDD_TO_TS,
        "soap.udp://239.255.255.250:3702", MessageID, NULL, "dp0:NetworkVideoTransmitter", NULL, "");
    if (res != SOAP_OK)
    {
        soap_print_fault(serv, stderr);
        exit(1);
    }
    soap_wsdd_listen(serv, 10); //timeout 10sec
    soap_destroy(serv);
    soap_end(serv);
    soap_done(serv);
    return 0;
}
void wsdd_event_Hello(struct soap *soap, unsigned int InstanceId, const char *SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, const char *EndpointReference, const char *Types, const char *Scopes, const char *MatchBy, const char *XAddrs, unsigned int MetadataVersion)
{ }

void wsdd_event_Bye(struct soap *soap, unsigned int InstanceId, const char *SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, const char *EndpointReference, const char *Types, const char *Scopes, const char *MatchBy, const char *XAddrs, unsigned int *MetadataVersion)
{ }

soap_wsdd_mode wsdd_event_Probe(struct soap *soap, const char *MessageID, const char *ReplyTo, const char *Types, const char *Scopes, const char *MatchBy, struct wsdd__ProbeMatchesType *ProbeMatches)
{
    printf("wsdd_event_Probe()\n");
  return SOAP_WSDD_ADHOC;
}

// to report an error
void report_error(struct soap *soap)
{
  std::cerr << "Oops, something went wrong:" << std::endl;
  soap_stream_fault(soap, std::cerr);
  exit(EXIT_FAILURE);
}

void set_credentials(struct soap *soap)
{
    soap_wsse_delete_Security(soap);
    if (soap_wsse_add_Timestamp(soap, "Time", 10) || soap_wsse_add_UsernameTokenDigest(soap, "Auth", USERNAME, PASSWORD))
    {
        report_error(soap);
    }
}

void wsdd_event_ProbeMatches(struct soap *soap, unsigned int InstanceId, const char *SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, struct wsdd__ProbeMatchesType *ProbeMatches)
{
    std::cout << "wsdd_event_ProbeMatches()\n";
    std::cout << "InstanceId=" << InstanceId << ", MessageNumber=" <<  MessageNumber << std::endl;
    if (SequenceId) printf("SequenceId[%s]\n", SequenceId);
    if (MessageID) printf("MessageID[%s]\n", MessageID);
    if (RelatesTo) printf("RelatesTo[%s]\n", RelatesTo);
    if (ProbeMatches->ProbeMatch->XAddrs)
    {
        std::cout << "ProbeMatches->XAddrs:" << ProbeMatches->ProbeMatch->XAddrs << std::endl;

        // create the proxies to access the ONVIF service API at HOSTNAME
        DeviceBindingProxy proxyDevice(soap);

        // get device info and print
        proxyDevice.soap_endpoint = ProbeMatches->ProbeMatch->XAddrs;
        _tds__GetDeviceInformation GetDeviceInformation;
        _tds__GetDeviceInformationResponse GetDeviceInformationResponse;
        set_credentials(soap);
        if (proxyDevice.GetDeviceInformation(&GetDeviceInformation, GetDeviceInformationResponse))
        {
            report_error(soap);
        }
        //check_response(soap);
        std::cout << "Manufacturer:    " << GetDeviceInformationResponse.Manufacturer << std::endl;
        std::cout << "Model:           " << GetDeviceInformationResponse.Model << std::endl;
        std::cout << "FirmwareVersion: " << GetDeviceInformationResponse.FirmwareVersion << std::endl;
        std::cout << "SerialNumber:    " << GetDeviceInformationResponse.SerialNumber << std::endl;
        std::cout << "HardwareId:      " << GetDeviceInformationResponse.HardwareId << std::endl;
    }
    if (ProbeMatches->ProbeMatch->Scopes->__item) printf("ProbeMatches->Scopes->__item[%s]\n", ProbeMatches->ProbeMatch->Scopes->__item);
    if (ProbeMatches->ProbeMatch->wsa5__EndpointReference.Address) printf("ProbeMatches->wsa5__EndpointReference.Address[%s]\n", ProbeMatches->ProbeMatch->wsa5__EndpointReference.Address);
    if (ProbeMatches->ProbeMatch->Types) printf("ProbeMatches->Types[%s]\n", ProbeMatches->ProbeMatch->Types);
    printf("ProbeMatches->MetadataVersion=%u\n", ProbeMatches->ProbeMatch->MetadataVersion);
}

soap_wsdd_mode wsdd_event_Resolve(struct soap *soap, const char *MessageID, const char *ReplyTo, const char *EndpointReference, struct wsdd__ResolveMatchType *match)
{
    printf("wsdd_event_Resolve()\n");
  return SOAP_WSDD_ADHOC;
}

void wsdd_event_ResolveMatches(struct soap *soap, unsigned int InstanceId, const char * SequenceId, unsigned int MessageNumber, const char *MessageID, const char *RelatesTo, struct wsdd__ResolveMatchType *match)
{
    printf("wsdd_event_ResolveMatches()\n");
}

int SOAP_ENV__Fault(struct soap *soap, char *faultcode, char *faultstring, char *faultactor, struct SOAP_ENV__Detail *detail, struct SOAP_ENV__Code *SOAP_ENV__Code, struct SOAP_ENV__Reason *SOAP_ENV__Reason, char *SOAP_ENV__Node, char *SOAP_ENV__Role, struct SOAP_ENV__Detail *SOAP_ENV__Detail)
{
  // populate the fault struct from the operation arguments to print it
  soap_fault(soap);
  // SOAP 1.1
  soap->fault->faultcode = faultcode;
  soap->fault->faultstring = faultstring;
  soap->fault->faultactor = faultactor;
  soap->fault->detail = detail;
  // SOAP 1.2
  soap->fault->SOAP_ENV__Code = SOAP_ENV__Code;
  soap->fault->SOAP_ENV__Reason = SOAP_ENV__Reason;
  soap->fault->SOAP_ENV__Node = SOAP_ENV__Node;
  soap->fault->SOAP_ENV__Role = SOAP_ENV__Role;
  soap->fault->SOAP_ENV__Detail = SOAP_ENV__Detail;
  // set error
  soap->error = SOAP_FAULT;
  // handle or display the fault here with soap_stream_fault(soap, std::cerr);
  // return HTTP 202 Accepted
  return soap_send_empty_response(soap, SOAP_OK);
}
```

## Makefile を書く
※あくまでも例

```Makefile
# Makefile for onvifclient
OBJS = main.o soapC.o wsddClient.o soapDeviceBindingProxy.o soapwsddProxy.o soapDeviceIOBindingProxy.o soapImagingBindingProxy.o soapMediaBindingProxy.o stdsoap2.o dom.o smdevp.o mecevp.o duration.o struct_timeval.o wsddapi.o wsseapi.o wsaapi.o
CC = g++
INCLUDEDIR = -I./ -I ~/gsoap-2.8/gsoap
CFLAGS = $(INCLUDEDIR) -DWITH_OPENSSL -DWITH_DOM -DWITH_ZLIB

onvifclient: $(OBJS)
	$(CC) $(OBJS) -lcrypto -lssl -lz -o $@

main.o: main.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

#wsddC.o: wsddC.cpp
#	$(CC) $(CFLAGS) -c -o $@ $<

soapC.o: soapC.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

wsddClient.o: wsddClient.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

soapDeviceBindingProxy.o: soapDeviceBindingProxy.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

soapwsddProxy.o: soapwsddProxy.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

soapDeviceIOBindingProxy.o: soapDeviceIOBindingProxy.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

soapImagingBindingProxy.o: soapImagingBindingProxy.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

soapMediaBindingProxy.o: soapMediaBindingProxy.cpp
	$(CC) $(CFLAGS) -c -o $@ $<

stdsoap2.o: ~/gsoap-2.8/gsoap/stdsoap2.c
	$(CC) $(CFLAGS) -c -o $@ $<

dom.o: ~/gsoap-2.8/gsoap/dom.c
	$(CC) $(CFLAGS) -c -o $@ $<

smdevp.o: ~/gsoap-2.8/gsoap/plugin/smdevp.c
	$(CC) $(CFLAGS) -c -o $@ $<

mecevp.o: ~/gsoap-2.8/gsoap/plugin/mecevp.c
	$(CC) $(CFLAGS) -c -o $@ $<

duration.o: ~/gsoap-2.8/gsoap/custom/duration.c
	$(CC) $(CFLAGS) -c -o $@ $<

struct_timeval.o: ~/gsoap-2.8/gsoap/custom/struct_timeval.c
	$(CC) $(CFLAGS) -c -o $@ $<

wsddapi.o: ~/gsoap-2.8/gsoap/plugin/wsddapi.c
	$(CC) $(CFLAGS) -c -o $@ $<

wsseapi.o: ~/gsoap-2.8/gsoap/plugin/wsseapi.cpp
	$(CC) $(CFLAGS) -fpermissive -c -o $@ $<

wsaapi.o: wsaapi.cpp
	$(CC) $(CFLAGS) -I ~/gsoap-2.8/gsoap/plugin -DSOAP_H_FILE=wsddH.h -c -o $@ $<
```

## ビルドする
```
$ make
```

# 実行する
```
$ ./onvifclient
```

# ハマったこと
+ ツールやビルドの実行が速いから、と、途中から WSL2/ubuntu で実行していたら
WSL2/ubuntu環境で実行すると、ブロードキャスト宛送信が出来なかった。（WSL2/ubuntuを適切に設定すればいいのかもしれないが調べてない)
+ 開発に使用しているWin10PCから、Resolveパケットが定期的に出ていて、自分の作成したプログラムから送信されていると勘違いしてしまったことも、解決を長引かせた。プログラムを追っていくと、Probeパケットが出ているはずなのに、なぜwiresharkで見るとResolveになるのかが見当つかなかった。
+ ブロードキャストアドレスを試しにONVIFカメラのIPアドレスに変更してみたら、Probeパケットが出て、ONVIFカメラからのProbeMatches応答がwireshark上で見れるようになった。が、今度は、そのProbeMatchesを作成プログラム上で受信できなかった。これも、WSL2で動かすのをやめ、ラズパイ4で動かせば、受信できるようになった。PCのlinuxの場合は、WSL2ではなくnative linux環境なら大丈夫だろうと予想。
+ GeniviaのONVIF exampleページの手順通りにコマンド実行していっても、リンクエラーがなかなか取れず、typemap.datを適切に修正するのが必要であると分かるまで、かなり遠回りした。（直接的にリンクエラーを解消する方向でトライし始めてしまった）

# 今後やりたい関連事項
+ WSL2/ubuntu上でラズパイ用のクロスコンパイル環境を構築した。簡単なプログラムだと動いたが、gSOAP込みのプログラムは　まだ…。


# 参考URL
+ [GeniviaのONVIFでのgSOAP利用方法解説](https://www.genivia.com/examples/onvif/index.html)
+ https://qiita.com/lis-hanzomon/items/6cf0f0759d7a6120a29d
+ [【エラー】ラズベリーパイで「autoreconf: コマンドが見つかりません」と表示される](https://www.limemo.net/blog/2016/04/%E3%80%90%E3%82%A8%E3%83%A9%E3%83%BC%E3%80%91%E3%83%A9%E3%82%BA%E3%83%99%E3%83%AA%E3%83%BC%E3%83%91%E3%82%A42%E3%81%A7%E3%80%8Cautoreconf-%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%8C%E8%A6%8B.html)
+ [The WS-Discovery plugin](https://www.genivia.com/doc/wsdd/html/wsdd_0.html)
+ [Use gSOAP to generate ONVIF framework C code under linux](https://www.programmersought.com/article/56874892941/)
+ [Automatic Discovery with ONVIF and gSOAP](http://tech.yipp.ca/onvif/automatic-discovery-onvif-gsoap/)
+ [gsoap compiles onvif under linux to realize discovery detection equipment](https://www.programmersought.com/article/50776028974/)
+ [soap_in_xsd__dateTimeのリンクエラー解決方法](https://blog.csdn.net/qq_36351159/article/details/107787158?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.control)
+ [ONVIF to obtain basic device information (webcam)-example notes](https://www.programmersought.com/article/34784791062/)
+ [ONVIF siteのspec doc](https://www.onvif.org/profiles/specifications/)
+ [ONVIF application programmer's guide](https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_WG-APG-Application_Programmers_Guide-1.pdf)
+ [Stackoverflow ONVIF WS-Discover 1.0 - Client and Event Handlers](https://stackoverflow.com/questions/27061083/onvif-ws-discover-1-0-client-and-event-handlers)