Skip to content

DevicePluginManual

TakayukiHoshi1984 edited this page Mar 27, 2017 · 3 revisions

目次

本ドキュメントは、DeviceConnect API 2.0を用いてiOSデバイスプラグインを開発する方法について解説しています。
iOSそしてObjective-Cに関する一般的な知識を保持している事を前提に解説していきます。

DeviceConnectとは、RESTfulコンセプトを用いて通信先への命令送信や、通信元と通信先の間でのデータ送受信を可能とするプロトコルです。
各処理にはProfileという形でURIが割り振られ、そのURIに対しHTTPでアクセスしたり、AndroidやiOSなどのプラットフォーム毎に定められたメッセージ通信プロトコルでアクセスしたりする事で処理を行います。

またDeviceConnectにおけるデバイスプラグインとは、DeviceConnectプロトコルに従って作られた処理のリクエストメッセージを受け取って解釈し、実デバイスに対し適切な処理を行わせ、得られた結果をDeviceConnectに従ったレスポンスメッセージとして返却する様な、デバイスとのやり取りを実質的に担うモジュールとなります。

まず事前にiOS版DeviceConnect一式をダウンロードしてください。
DeviceConnect一式が入ったディレクトリで、このチュートリアルで特に取り扱う物は以下になります。

  1. dConnectSDK/dConnectSDKForIOS
  2. dConnectDevicePlugin
  3. DeviceConnectTutorial

1つめのdConnectSDKForIOSには、デバイスプラグインやアプリで必要となるdConnectSDKのフレームワーク(.framework拡張子ファイル)や、最終的にアプリでリソースとして同梱する必要のあるdConnectSDKのバンドル(.bundle拡張子ファイル)をビルドする為のXcodeプロジェクトが入っています。
2つめのdConnectDevicePluginには各種デバイスに対応したデバイスプラグインのXcodeプロジェクトが入っています。
そして3つめのdConnectTutorialには次節「Get Started」で使う開発用ワークスペースのサンプルが入っています。

このチュートリアルを終える頃にはデバイスプラグイン作成、そして簡単なDeviceConnect対応アプリの作成方法に関する知識が身に付くはずです。
もしお手元にdConnectDevicePlugin以下にあるデバイスプラグインの対応デバイスがあるのでしたら、DeviceConnect対応アプリでデバイスに様々な処理を行わせてみるのも楽しいかもしれません。
そしてDeviceConnectプロトコルのコンセプトを理解し、様々なデバイスとプログラムスキルに恵まれた方々は是非、新たなDeviceConnect対応アプリ、そしてDeviceConnectデバイスプラグインの開発に挑戦してみてください。

デバイスプラグイン、そして簡単なDeviceConnect対応アプリを作成・開発するための環境を1から整えるのは簡単な作業ではありません。
そのため、まずは既に出来上がった作成・開発環境の見本から触れてみてください。
そうすることでデバイスプラグイン開発における最終的な終着点、作成・開発を行う為に必要な物事とは何かを掴めるはずです。

第2節で挙げたDeviceConnectTutorialには開発用ワークスペースのサンプルが入っています。
まずXcodeワークスペースであるDeviceConnectDev.xcworkspaceを開いてください。
開くと以下の様にExampleDevicePluginそしてDevicePluginUIDemoプロジェクトが既にプロジェクトナビゲーターに追加されているのが確認できる筈です。

ExampleDevicePluginはこのチュートリアルで取り扱うことになるデバイスプラグイン見本であり、DevicePluginUIDemoはそのデバイスプラグイン見本がどう機能するのかチェックする為の簡単なアプリです。


DConnectSDKのフレームワークやバンドルは既に同梱してあり、各プロジェクトで既に参照済みとなっています。
ここでやる事はExampleDevicePluginそしてDevicePluginUIDemoプロジェクトの必要な各ビルドターゲットのビルドです。
ビルドに際して各ビルドターゲットの依存関係を解決する必要がありますが、既にその対処は済ませてあります。
そして依存関係を解決する順番でのビルドは以下の様になります。

  1. ExampleDevicePlugin_framework
  2. ExampleDevicePluginResources
  3. DevicePluginUIDemo

ExampleDevicePluginはデバイスと連携させることなく、SystemやService Discoveryを含む各種DeviceConnectプロファイルのモックアップとして準備されています。
デバイスプラグインの開発を行う際はこれらモックアップ実装を足がかりにすることをお勧めします。

それでは上記の順番でビルドターゲットをビルドし、最後にDevicePluginUIDemoをシミュレーターで実行してみてください。

アプリを起動すると以下のような画面が表示されます。


次に「Perform Network Service Discovery」を押すことによってデバイスプラグインの検索を行います。
このデモアプリではExampleDevicePluginが見つかるはずですので、以下のように「No device」の部分にExampleDevicePluginのデバイス名が表示されます。


この状態で「Open Settings view」を押すとデバイスプラグインで作成した設定画面が起動します。
設定画面では、デバイスプラグインとスマートフォンの接続設定を記述する画面となります。
ユーザがシームレスにDeviceConnectを使ってデバイスプラグインを利用できる様な、デバイスとの接続方法や接続処理などを正しく、解りやすく行える内容を記述してください。


このアプリではDeviceConnectのセキュリティ機構であるLocal OAuth認証を必要とする場面、例えばVibrationでデバイスを振動させたり、Settingsで日時変更をしたりなどのユーザ認証が必要な場面を用意していません。
更に詳細なアプリ作成、Local OAuth認証の使い方を知りたい場合は、HTML5/Android/iOSアプリチュートリアルを参照してください。

ここでは、ExampleDevicePluginの構成について簡単に説明します。


基本、デバイスプラグインはframeworkとして作成されるため、「Resources」と「Classes」と「Headers」フォルダを作成してください。
詳しい説明は、iOSにおけるFrameworkの作成の仕方について調べてください。
 「公開するヘッダー」は、デバイスプラグインが拡張プロファイルという独自に定義したプロファイルを公開する場合にヘッダーファイルを配置します。
DeviceConnectでは、「DConnectProfile」を継承することによって独自のプロファイルを作成することが出来ます。

Classesディレクトリには、作成するデバイスプラグインのソースコードがおかれています。
例えば、デバイスプラグインが実現する機能である各種プロファイルのmファイル、それらプロファイルを登録するデバイスプラグインのmファイル、そして設定画面UIやその他の画面UIを制御する機能を持つmファイルを配置したりします。
 基本的に、Classesディレクトリ内ではディレクトリの制限は特にありませんので各自わかりやすいように配置してみてください。

ここで、バッテリーの機能を提供するプロファイル実装であるExampleBatteryProfileの仕組みを見てみましょう。
基本的な仕組みはAndroidなどプラットフォームを問わず同じで、プラグインを継承し用意されているメソッド(addGetPath,addPostPath,addPutPath,addDeletePath)を利用してデバイスプラグインの機能を実装します。
DeviceConnectの標準プロファイルであるBatteryやDeviceOrientationなどのモック実装が用意されているので、必要なプロファイルがあれば各自そのプロファイルを実装したクラスを再利用して役立ててください。

#import "ExampleBatteryProfile.h"
@implementation ExampleBatteryProfile

-(instancetype)init
{
    self = [super init];
    if (self) {
        self.delegate = self;
        
        // API登録(Get All Request)
        NSString *getAllRequestApiPath = [self apiPath: nil
                                         attributeName: nil];
        [self addGetPath: getAllRequestApiPath api: ^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) {
            [response setResult:DConnectMessageResultTypeOk];

            return YES;
        }
        
        // API登録(Get Level Request)
        NSString *getLevelRequestApiPath = [self apiPath: nil
                                           attributeName: DConnectBatteryProfileAttrLevel];
        [self addGetPath: getLevelRequestApiPath api: ^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) {
            [response setResult:DConnectMessageResultTypeOk];

            return YES;
        }];
    }
    return self;
}

@end

ExampleDevicePluginは、DConnectDevicePluginを継承しており、そのmファイルの中のinitメソッドの中で以下のようにaddServiceすることによって必要なサービスを登録することができます。この例では最初からサービスを認識させるためにinitで実行していますが、デバイスを接続した時にサービスを認識させる場合は、そのタイミングでaddServiceを実行して下さい。
SystemプロファイルはここでaddProfileメソッドで登録します。

@implementation ExampleDevicePlugin

-(instancetype)init {
    self = [super initWithObject: self];
    if(self){
        self.pluginName = @“Example (Device Connect Device Plug-in)";
        
        // サービス追加
        DConnectService *exampleService = [[ExampleService alloc] initWithServiceId: serviceId plugin: self];
        [self.serviceProvider addService: exampleService];
        
        // プロファイルを追加
        [self addProfile:[ExampleSystemProfile new]];
    }

    return self;
}

ExampleServiceは、DConnectServiceを継承しており、そのmファイルの中のinitメソッドの中で以下のようにaddProfileすることによって必要なプロファイルを追加することができます。
プロファイルを実装した際、addProfileメソッドで登録を行わない限りDeviceConnectでそのプロファイルが認識されませんので気をつけてください。

@implementation ExampleService

-(instancetype) initWithPlugin: (id) plugin {
    self = [super initWithServiceId: @“exampleServiceId” plugin: plugin];
    if(self){
        self.serviceId = @"example_device_id";
        self.online = YES;

        [self addProfile:[ExampleBatteryProfile new]];
        [self addProfile:[ExampleConnectProfile new]];
        [self addProfile:[ExampleDeviceOrientationProfile new]];
        [self addProfile:[ExampleFileDescriptorProfile new]];
        [self addProfile:[ExampleFileProfile new]];
        [self addProfile:[ExampleMediaPlayerProfile new]];
        [self addProfile:[ExampleMediaStreamRecodingProfile new]];
        [self addProfile:[ExamplePhoneProfile new]];
        [self addProfile:[ExampleProximityProfile new]];
        [self addProfile:[ExampleSettingsProfile new]];
        [self addProfile:[ExampleVibrationProfile new]];
    }

    return self;
}

デバイスプラグインは、Framework(Cocoa Touch Static Library)として作成します。


開発を行う上で、Workspaceに追加する場合は、プロジェクトの新規作成時のオプションでプロジェクトの追加先Workspaceを選択するか、xcodeprojファイルを以下の様にWorkspaceにドラッグアンドドロップします。
余談ですが、Xcodeプロジェクトは同時に2つのプロジェクトやワークスペースから変更できないようになっているので、Workspace側で作業する前にWorkspaceに追加されているプロジェクトを別ウィンドウで開いている際は、それら別ウィンドウを全て閉じてください。


ResourcesやClassesといったディレクトリの作成の仕方などは、Frameworkとしての作成の仕方と同じですので、このマニュアルでは解説を割愛します。
 デバイスプラグインは、Frameworkとして作成しますが、設定画面などのResourcesディレクトリにあるファイルに関してはBundleファイルとしてUIアプリに同梱します。
Bundleに関してもiOSの基本的な知識になりますので、このマニュアルでは解説を割愛します。
 

デバイスプラグイン側でプラグイン名などの文字列やUIを多言語化する場合は、Resourcesファイルに各言語の文字列を定義したLocalizable.stringsファイルを作成したり、各言語用にローカライズされた設定画面用UIのStoryboardを作成したりして、それらを同梱したBundleファイルを用意、アプリ側で状況に応じて読み込むようにしてください。
 デバイスプラグインで用意した特定言語のリソースが利用されるには、UIアプリ側も同じ言語で多言語化されていなければなりませんので注意してください。

DConnectSDKにあらかじめ実装されていない新規プロファイルを作成したいときはこの説明を参考に実装して下さい。まずプロファイルクラスを作成してAPIを実装します。次にAPI引数の条件設定(必須/任意、有効範囲など)をJSONファイルを作成して定義します。

独自プロファイルの実装は、Androidと同様にDConnectProfileクラスを継承します。
ここではExamplePressureProfileクラスを作成します。
独自プロファイルの場合も、APIの実装は同じですのでaddGetPath,addPostPath,addPutPath,addDeletePathを使って実装して下さい。
標準プロファイルの実装と違うのはプロファイル名を返す必要があることです。DConnectProfileクラスのprofileNameメソッドをオーバーライドして戻り値で返すように実装して下さい。

@implementation ExamplePressureProfile

- (instancetype) init {
    
    self = [super init];
    if (self) {
        
        NSString *getPressureRequestApiPath = [self apiPath: nil attributeName: nil];
        [self addGetPath:getPressureRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) {
            
            NSString interface = request.interface;
            NSString attribute = request.attribute;
            NSString serviceId = request.serviceId;
            
            NSString *param1 = [request stringForKey:@"param1"];
            NSString *param2 = [request stringForKey:@"param2"];
            
            NSLog(@"GET:interface:%@", interface);
            NSLog(@"GET:attribute:%@", attribute);
            NSLog(@"GET:serviceId:%@", serviceId);
            NSLog(@"GET:param1:%@", param1);
            NSLog(@"GET:param2:%@", param2);

            [response setResult: DConnectMessageResultTypeOk];
            return YES;
        }];
        
        NSString *postPressureRequestApiPath = [self apiPath: nil attributeName: nil];
        [self addPostPath:postPressureRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) {
            
            NSString interface = request.interface;
            NSString attribute = request.attribute;
            NSString serviceId = request.serviceId;
            
            NSString *param1 = [request stringForKey:@"param1"];
            NSString *param2 = [request stringForKey:@"param2"];
            
            NSLog(@"POST:interface:%@", interface);
            NSLog(@"POST:attribute:%@", attribute);
            NSLog(@"POST:serviceId:%@", serviceId);
            NSLog(@"POST:param1:%@", param1);
            NSLog(@"POST:param2:%@", param2);
            
            [response setResult: DConnectMessageResultTypeOk];
            return YES;
        }];
        
        NSString *putPressureRequestApiPath = [self apiPath: nil attributeName: nil];
        [self addPutPath:putPressureRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) {
            
            NSString interface = request.interface;
            NSString attribute = request.attribute;
            NSString serviceId = request.serviceId;
            
            NSString *param1 = [request stringForKey:@"param1"];
            NSString *param2 = [request stringForKey:@"param2"];
            
            NSLog(@"PUT:interface:%@", interface);
            NSLog(@"PUT:attribute:%@", attribute);
            NSLog(@"PUT:serviceId:%@", serviceId);
            NSLog(@"PUT:param1:%@", param1);
            NSLog(@"PUT:param2:%@", param2);
            
            [response setResult: DConnectMessageResultTypeOk];
            return YES;
        }];
        
        NSString *deletePressureRequestApiPath = [self apiPath: nil attributeName: nil];
        [self addDeletePath:deletePressureRequestApiPath api:^BOOL(DConnectRequestMessage *request, DConnectResponseMessage *response) {
            
            NSString interface = request.interface;
            NSString attribute = request.attribute;
            NSString serviceId = request.serviceId;
            
            NSString *param1 = [request stringForKey:@"param1"];
            NSString *param2 = [request stringForKey:@"param2"];
            
            NSLog(@"DELETE:interface:%@", interface);
            NSLog(@"DELETE:attribute:%@", attribute);
            NSLog(@"DELETE:serviceId:%@", serviceId);
            NSLog(@"DELETE:param1:%@", param1);
            NSLog(@"DELETE:param2:%@", param2);
            
            [response setResult: DConnectMessageResultTypeOk];
            
            return YES;
        }];
    }
    return self;
}

#pragma mark - DConnectProfile Methods

- (NSString *)profileName {
    return @"pressure";
}

API引数の条件設定(必須/任意、有効範囲など)をJSONファイルを作成して定義します。
JSONファイル内容は下記の通りで、Swagger仕様に準拠しています。
主要項目の”info”と”paths”については以降の項目で説明します。

{
    "swagger": "2.0",
    "info": {
        "title": "Pressure Profile",
        "version": "2.0.0",
        "description": ""
    },
    "consumes": [
                 "application/x-www-form-urlencoded",
                 "multipart/form-data"
                 ],
    "paths": {
        "/": {
            "get": {
                "x-type": "one-shot",
                "summary": "",
                "description": "",
                "parameters": [
                               {
                               "name": "serviceId",
                               "in": "query",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param1",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param2",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               }
                               ],
                "responses": {
                    "200": {
                        "description": ""
                    }
                }
            },
            "post": {
                "x-type": "one-shot",
                "summary": "",
                "description": "",
                "parameters": [
                               {
                               "name": "serviceId",
                               "in": "query",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param1",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param2",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               }
                               ],
                "responses": {
                    "200": {
                        "description": ""
                    }
                }
            },
            "put": {
                "x-type": "one-shot",
                "summary": "",
                "description": "",
                "parameters": [
                               {
                               "name": "serviceId",
                               "in": "query",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param1",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param2",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               }
                               ],
                "responses": {
                    "200": {
                        "description": ""
                    }
                }
            },
            "delete": {
                "x-type": "one-shot",
                "summary": "",
                "description": "",
                "parameters": [
                               {
                               "name": "serviceId",
                               "in": "query",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param1",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               },
                               {
                               "name": "param2",
                               "in": "formData",
                               "required": true,
                               "type": "string"
                               }
                               ],
                "responses": {
                    "200": {
                        "description": ""
                    }
                }
            }
        }
    }
}

JSONファイルの第一階層の”info”は、プロファイルのtitleやversionなどを記述して下さい。

JSONファイルの第一階層の”paths”は、API毎のパラメータ項目について、必須/任意であるか、設定範囲(最小値、最大値、最小文字数、最大文字数など)を記述します。 まず、APIの指定ですが、下記のように”paths”直下のkey値はAPIパスを示し、その下の階層のkey値はHTTPメソッドを示しています。

{
    "paths": {
        "/": {            // この階層のkey値はAPIパスを定義しています。
            "get": {          // この階層のkey値はHTTPメソッドを定義しています。
                // この中にGET ”/” APIのパラメータ条件を定義しています。
            },
            "post": {
                // この中にPOST ”/” APIのパラメータ条件を定義しています。
            },
            "put": {
                // この中にPUT ”/” APIのパラメータ条件を定義しています。
            },
            "delete": {
                // この中にDELETE ”/” APIのパラメータ条件を定義しています。
            }
        }
    }
}

各APIのパラメータ条件は、以下の例のように定義します。

            "get": {
                "x-type": "one-shot",      // ”one-shot”または”event”
                "summary": "",
                "description": "",
                "parameters": [
                               {
                               "name": "serviceId",  // パラメータ名。
                               "in": "query",        // ”formData”,”query”等。
                               "required": true,     // 必須ならtrue,任意ならfalse
                               "type": "string"      // ”string”,”number”,”integer”,”boolean”等。
                               },
                               
                               ],
                "responses": {
                    "200": {
                        "description": ""
                    }
                }
            },

“name” は、パラメータ名を設定します。
“in”は、クエリとして渡す値の場合は”query”を、入力フォームから渡された値の場合は”formData”を設定して下さい。
“required”は、このパラメータが必須項目の場合はtrueを、必須項目ではない場合はfalseを設定して下さい。
“type”は、パラメータに指定できる値の種類を設定します。設定できるのは”string”,”number”,”integer”,”boolean”等です。

type毎の設定例をまとめましたので、こちらを参考に設定して下さい。

stringの場合

                    {
                        "name": "serviceId",
                        "in": "query",
                        "required": true,
                        "type": "string"
                    },
                    {
                        "name": "direction",
                        "in": "formData",
                        "required": true,
                        "type": "string",
                        "enum": ["in", "out"]
                    },

numberの場合

                    {
                        "name": "angle",
                        "in": "formData",
                        "required": true,
                        "type": "number",
                        "minimum": 0,
                        "maximum": 360
                    },

integerの場合

                    {
                        "name": "temperaturevalue",
                        "in": "formData",
                        "required": true,
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 50
                    }

4.booleanの場合

                    {
                        "name": "airflowauto",
                        "in": "formData",
                        "required": false,
                        "type": "boolean"
                    }

Clone this wiki locally