見出し画像

72. WCF で ONVIF へのアクセスを試みる ~ 失敗したけど…

前回の記事                        次回の記事

はじめに

C520WS のスマホアプリへのデバイス接続が復活したので、ONVIF へのアクセスを、WCF(Windows Communication Foundation)で試してみることにしました。最初に白状しておきます。一部実行できたものの、現時点では、必要な機能をすべて実行すること能わず、です。

WCF(Windows Communication Foundation)

まず、WCF とは何ぞやというところから始めます。WCF は Windows Communication Foundation の略で、Windows Vista のリリースに合わせて、このマガジンでも常用しているデスクトップ GUI アプリを作る時の定番フレームワークの WPF(Windows Presentation Foundation)と同時期に公開された分散システムでサービス連携を容易にするためのテクノロジーセットです。詳しくは、

Windows Communication Foundation とは - WCF | Microsoft Learn

で解説されています。Windows Vista は2006年にリリースされているので、WCF もその時期にリリースされたと記憶しています。当時は、サーバーが外部に API を公開するデファクトとして SOAP(Simple Object Access Protocol)が全盛を極めていたと記憶しています。SOAP では、API は、XML フォーマットベースの WSDL(Web Service Description Language)で記述することが一般的です。WSDL は、引数の情報とともにどんなリクエストがあって、そのリクエストに対してどんなレスポンスを返すかを記述します。それによって、サービスを使いたいクライアントは、WSDL の記述に従って、SOAP に規定に従い、HTTP リクエストをサーバーに送れば、WSDL に記述された通りのレスポンスを取得することができます。SOAP に対応したサービスを公開しているサーバーから、公開中のサービスの API を WSDL でダウンロードできる(常にそうだと思っていらそうでもないらしい)場合もあり、サービス連携の自動化も可能です。
WCF は、サービス連携に必要な通信やセキュリティのミドルウェアや、公開中のサービスや、WSDL から、そのサービスにアクセスする Wrapper クラスライブラリを自動生成する機能があり、サービス指向アプリケーションを容易に開発できる仕組みを提供してくれます。WCF は、分散システム間連携でなく、ローカルマシンのプロセス間通信でも使うことができます。
2006年のリリース当時から、すでに20年近くたって、Web サービスは、REST API も普及し、最近は gRPC による連携も増えてきたので、最新の WCF は、Open API や gRPC にも対応しているようです。
ちなみに、組込み機器においては、2000年代あたりだと、HW リソースが少なく、SOAP に対応するにはちょっと無理があったので、機能を若干そぎ落としてはあるものの、分散システム連携用に、DPWS(Device Profile for Web Service)という標準があり、それに対応していた、.NET Micro Framework 機器で、デモ環境など作り、エバンジェライズにいそしんだなと、懐かしい気持ちになります。
更に、付け加えておくと、WS-Discovery というローカルネットワークにつながったサービス対応機器を発見するプロトコルがあって、組込み機器をネットワークに接続した際、組込み機器が、同一ネットワークに接続されている IoT ゲートウェイ機器を発見したり、逆に、IoT ゲートウェイ機器が接続された機器のサービスを検知して、IoT ソリューションに組み入れる処理を自動化するといったことも可能です。

ONVIF を WCF でアクセスする

さて、本題に入りますね。
ONVIF は、Web サービス API の定義を WSDL で公開しています。

Network Interface Specifications - ONVIF

このページから、ONVIF がサポートしている各機能ごとの WSDL ファイルが公開されています。例えば、Core Specification は、device.wsdl(このページは WSDL ファイルで記述された各項目の説明が記載されている)で、

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../ver20/util/onvif-wsdl-viewer.xsl"?>
<!--
Copyright (c) 2008-2012 by ONVIF: Open Network Video Interface Forum. All rights reserved.

Recipients of this document may copy, distribute, publish, or display this document so long as this copyright notice, license and disclaimer are retained with all copies of the document. No license is granted to modify this document.

THIS DOCUMENT IS PROVIDED "AS IS," AND THE CORPORATION AND ITS MEMBERS AND THEIR AFFILIATES, MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE CONTENTS OF THIS DOCUMENT ARE SUITABLE FOR ANY PURPOSE; OR THAT THE IMPLEMENTATION OF SUCH CONTENTS WILL NOT INFRINGE ANY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
IN NO EVENT WILL THE CORPORATION OR ITS MEMBERS OR THEIR AFFILIATES BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, PUNITIVE OR CONSEQUENTIAL DAMAGES, ARISING OUT OF OR RELATING TO ANY USE OR DISTRIBUTION OF THIS DOCUMENT, WHETHER OR NOT (1) THE CORPORATION, MEMBERS OR THEIR AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, OR (2) SUCH DAMAGES WERE REASONABLY FORESEEABLE, AND ARISING OUT OF OR RELATING TO ANY USE OR DISTRIBUTION OF THIS DOCUMENT.  THE FOREGOING DISCLAIMER AND LIMITATION ON LIABILITY DO NOT APPLY TO, INVALIDATE, OR LIMIT REPRESENTATIONS AND WARRANTIES MADE BY THE MEMBERS AND THEIR RESPECTIVE AFFILIATES TO THE CORPORATION AND OTHER MEMBERS IN CERTAIN WRITTEN POLICIES OF THE CORPORATION.
-->
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tds="http://www.onvif.org/ver10/device/wsdl" xmlns:tmd="http://www.onvif.org/ver10/deviceIO/wsdl" targetNamespace="http://www.onvif.org/ver10/deviceIO/wsdl">
	<wsdl:import namespace="http://www.onvif.org/ver10/media/wsdl" location="media.wsdl"/>
	<wsdl:import namespace="http://www.onvif.org/ver10/device/wsdl" location="devicemgmt.wsdl"/>
	<wsdl:types>
		<xs:schema targetNamespace="http://www.onvif.org/ver10/deviceIO/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" version="2.4.1">
			<xs:import namespace="http://www.onvif.org/ver10/schema" schemaLocation="./onvif.xsd"/>
			<!--===============================-->
			<xs:element name="GetServiceCapabilities">
				<xs:complexType>
					<xs:sequence/>
				</xs:complexType>
			</xs:element>
			<xs:element name="GetServiceCapabilitiesResponse">
				<xs:complexType>
					<xs:sequence>
						<xs:element name="Capabilities" type="tmd:Capabilities">
							<xs:annotation>
								<xs:documentation>The capabilities for the device IO service is returned in the Capabilities element.</xs:documentation>
							</xs:annotation>
						</xs:element>
					</xs:sequence>
				</xs:complexType>
			</xs:element>
			<!--===============================-->
			<xs:complexType name="Capabilities">
				<xs:sequence>

のように、XML ファイルで定義されています。
要するに、これらの WSDL ファイルを使えば WCF であくせすできるんじゃないの…って寸法。

C# Wrapper クラスライブラリを生成する

早速試してみることにします。ツールは、Visual Studio 2022 を使います。
Visual Studio で WCF 機能使うのいつ以来だろう…ひっさしぶりぃ!

使用する WSDL ファイルは、前々回の記事で C520WS の接続で試した、Python の python-onvif-zeep に包含されている WSDL ファイルを使用します。

python-onvif-zeep の WSDL ファイル群

python-onvif-zeep の client.py を見ると、テスト用に作成したアプリで使っていた、ライブラリはそれぞれ、

  • devicemgmt-> devicemgmt.wsdl

  • ptz ->ptz.wsdl

  • media -> media.wsdl

をそれぞれ使っているようなので、まずは、devicemgmt.wsdl から順に C# Wrapper クラスライブラリを作成していくことにします。

まずは、テスト用に WPF アプリプロジェクトを一つ新規に作成します。名前は、WpfAppOnvifWCF としました。ソリューションエクスプローラーで、プロジェクトアイコンを右クリックして、”Add”→”Service Reference…”を選択します。

Service Reference の追加開始

表示されたダイアログで、

WCF Web Service を選択

”WCF Web Service”を選択し、先に進みます。 

WSDL ファイルの選択

”Browse…”ボタンをクリックして、python-onvif-zeep/wsdl の devicemgmt.wsdl を選択します。

WSDL からの Wrapper 生成

namespace は、適切な名前に変更します。今回は、”OnvifDM” としました。あとは、”Next” を適宜押下して作業を進めます。最後に、”Successfully added service reference” と表示されれば、”Close”をクリックして完了です。選択肢は、そのままで生成しました。
結果として、”Connected Service”という名前のフォルダーが出来上がり、そこに namespace で指定した名前のフォルダーができて、生成されたファイルが出来上がります。

生成結果

devicemgmt のテストコードを実行してみる

早速、WPF アプリの GUI にボタンを一つ作成 & Click ハンドラーを作成してそこにコードを書いていきます。
pytho-onvif-zeep の場合は、

from onvif import ONVIFCamera

# https://www.onvif.org/onvif/ver20/util/operationIndex.html

mycam = ONVIFCamera('192.168.xxx.yyy', 2020, 'username', 'password', 'python-onvif-zeep/wsdl')
# Get Hostname
resp = mycam.devicemgmt.GetHostname()
print( 'My camera`s hostname: ' + str(resp.Name))

# Get system date and time
dt = mycam.devicemgmt.GetSystemDateAndTime()
tz = dt.TimeZone
year = dt.UTCDateTime.Date.Year
hour = dt.UTCDateTime.Time.Hour
...

こんなコードでしたが、今回生成した Wrapper Code には、ONVIFCamera に相当するものは存在しません。

WCF の場合は、選択した WSDL ファイルのターゲット名前空間の文字列(多分)に、Client を後ろにつけた名前のクラスライブラリがとっかかりになります。devicemgmt.wsdl の場合は、”device”なので、”DeviceClient” になるようです。
それを頼りに、あれこれ試しつつ…
要するに、接続先とユーザー名、パスワードを指定して接続オープンすりゃいいんじゃね!ってことで、

        private string address = "192.168.xxx.yyy";
        private string username = "username";
        private string password = "password";
        private int port = 2020;
        private string  serviceEndpoint = "onvif/service";
        private async void buttonTest_Click(object sender, RoutedEventArgs e)
        {
            var dmEndPointConfig = new OnvifDM.DeviceClient.EndpointConfiguration();
            string endpoint = $"http://{address}:{port}/{serviceEndpoint}";

            var dmClient = new OnvifDM.DeviceClient(dmEndPointConfig, endpoint);
            dmClient.ClientCredentials.UserName.UserName = username;
            dmClient.ClientCredentials.UserName.Password = password;

            try
            {
                await dmClient.OpenAsync();
                var dateTime = await dmClient.GetSystemDateAndTimeAsync();
                var capability = await dmClient.GetServiceCapabilitiesAsync();

            }
            catch(Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }
        }

こんなコードを試してみると、Exception が発生せずに、GetSystemDateAndTimeAsync method のコールができて、それっぽい値が入っていました。どうやら成功したらしいです。
DeviceClient のコンストラクターの引数に、

  • OnvifDM.DeviceClient.EndpointConfiguration 型のインスタンス

    • とりあえずインスタンスを作成して渡してみた

  • http://ipaddress:2020/onvif/service というエンドポイントの文字列

という二つの変数を渡して、作成された DeviceClient インスタンスの ClientCredentials プロパティの UserName プロパティに C520WS で設定したユーザー名とパスワードを設定した後、OpenAsync method をコールする、というのがパターンのようです。

他の WSDL ファイルを追加で試す

どうやらうまくいったようなので、ptz と media を試すことにします。追加方法は、devicemgmt.wsdl と同じですが、それぞれ異なる名前空間名を設定します。

ptz.wsdl の追加
media.wsdl の追加

それぞれ、名前空間は、

  • ptz.wsdl → OnvifPtz

  • media.wsdl → OnvifMedia

としました。これ、同じ名前空間名にしてしまうと、追加するたびに、上書きされてしまうので気を付けてください。

python-onvif-zeep のテストコードと devicemgmt のパターンを考慮して、

            var dmEndPointConfig = new OnvifDM.DeviceClient.EndpointConfiguration();
            string endpoint = $"http://{address}:{port}/{serviceEndpoint}";

            var dmClient = new OnvifDM.DeviceClient(dmEndPointConfig, endpoint);
            dmClient.ClientCredentials.UserName.UserName = username;
            dmClient.ClientCredentials.UserName.Password = password;

            var ptzEndPointConfig = new OnvifPtz.PTZClient.EndpointConfiguration();
            var ptzClient = new OnvifPtz.PTZClient(ptzEndPointConfig, endpoint);
            ptzClient.ClientCredentials.UserName.UserName = username;
            ptzClient.ClientCredentials.UserName.Password= password;

            var mediaEndPointConfig = new OnvifMedia.MediaClient.EndpointConfiguration();
            var mediaClient = new OnvifMedia.MediaClient(mediaEndPointConfig, endpoint);
            mediaClient.ClientCredentials.UserName.UserName = username;
            mediaClient.ClientCredentials.UserName.Password = password;

            try
            {
                await dmClient.OpenAsync();
                var dateTime = await dmClient.GetSystemDateAndTimeAsync();
                var capability = await dmClient.GetServiceCapabilitiesAsync();

                await ptzClient.OpenAsync();
                await mediaClient.OpenAsync();
                var mediaProfiles = await mediaClient.GetProfilesAsync();

                var mediaProfile = mediaProfiles.Profiles[0];
                var ptzConfig = await ptzClient.GetConfigurationAsync(mediaProfile.PTZConfiguration.token);
            }
            catch(Exception ex)
            {
                Debug.WriteLine(ex.Message);
            }

Media サービスから Profiles を取得して、その先頭から PTZ サービス用の Token を取り出せばよいだろうという見込みです。

さっそく実行してみると、残念ながら動きませんでした。ptz、media ともに OpenAsync method までは成功しましたが、GetProfilesAsync method のコールで、

System.ServiceModel.ProtocolException:
'リモート サーバーから予期しない応答が返されました: (400) Bad Request。'

と Bad Request 例外が発生。

無念!

WCF の C# Wrapper クラスライブラリ生成時には、いくつかオプション設定があります。

まずは、

同期オペレーション生成

昨今の WCF が生成する method は基本、非同期(method の名前の後ろが ”Async”)で生成されますが、”Generate Synchronous Operations”にチェックを入れて生成すると、非同期に加えて同期 method も生成されます。微妙に method のシグネチャは変わりますが、devicemgmt では、こちらも動きましたが、media の GetProfiles method は相変わらず、Bad Request 例外が発生しました。
他の生成オプションとしては、

message contracts、Reuse types in referenced assemblies

と、

  • message contracts

  • Reuse types in referenced assemblies

の選択が可能ですが、こちらも選択してやってみたところ NG。
それぞれの Wrapper クラスライブラリは、それぞれの名前空間に対応するフォルダーの下に生成された Reference.cs で定義されています。試しに、DeviceClient の GetSystemDateAndTimeAsync を見てみると

        public System.Threading.Tasks.Task<OnvifDM.SystemDateTime> GetSystemDateAndTimeAsync()
        {
            return base.Channel.GetSystemDateAndTimeAsync();
        }

となっていて、MediaClient の GetProfiles を見てみると

        [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
        System.Threading.Tasks.Task<OnvifMedia.GetProfilesResponse> OnvifMedia.Media.GetProfilesAsync(OnvifMedia.GetProfilesRequest request)
        {
            return base.Channel.GetProfilesAsync(request);
        }
        
        public System.Threading.Tasks.Task<OnvifMedia.GetProfilesResponse> GetProfilesAsync()
        {
            OnvifMedia.GetProfilesRequest inValue = new OnvifMedia.GetProfilesRequest();
            return ((OnvifMedia.Media)(this)).GetProfilesAsync(inValue);
        }

となっていました。微妙にパターンが違うな…それですかね…原因は…

ONVIF については、他にやりようはいくらでもあるので、このマガジンでは、ここまでとします。残念無念

ここから先は

413字

2022年3月にマイクロソフトの中の人から外の人になった Embedded D. George が、現時点で持っている知識に加えて、頻繁に…

この記事が気に入ったらサポートをしてみませんか?