つくるの大好き。

つくるのが大好きな人の記録。

Intel RealSense D435i のDepth映像を確認してみた

Intelの新しいデプスカメラ D435i が届いたので開封してDepthカメラとしての動作確認をしましたので取り急ぎ共有します。

f:id:peugeot-106-s16:20181214132929j:plain

D435との外観比較

外観は全く一緒なので取り違えに注意です。
本体下部に型番が書いてあるのでそこで判断できます。

f:id:peugeot-106-s16:20181214133055j:plain

D435のDepth映像と赤外線の様子

赤外線パターンはくっきりとした小さな点です。 (ZOZOスーツだ!) f:id:peugeot-106-s16:20181214133142p:plain

f:id:peugeot-106-s16:20181214133208p:plain

D435iのDepth映像と赤外線の様子

これは誤報かもしれません。ファームウェア更新後再度試したところD435同様の赤外線パターンが確認されました。

ステレオカメラなので赤外線は発していません。
赤外線カメラを当ててみればD435との違いがはっきりとわかります。 f:id:peugeot-106-s16:20181214133412p:plain

f:id:peugeot-106-s16:20181214133428p:plain

画質については面についてはD435が奇麗な印象。 ただ黒い髪の毛についてはD435が毛束の間がつぶれてしまうのに対しD435iはきちんと抜けていました。

まとめ

D435とD435iのDepthカメラとしての特性の違いを赤外線映像から探ってみました。 D435iのもうひとつの特徴、IMUについては動作を確認する方法がまだわかっていないのでもう少し探ってみたいと思います。

IMUデータの確認方法

IMUデータを確認するにはIntel RealSense Viewer 2.17.0 以上が必要です。現状 Pre Release状態になっています。

Release Intel® RealSense™ SDK 2.0 (build 2.17.0) · IntelRealSense/librealsense · GitHub

f:id:peugeot-106-s16:20181214143811p:plain


Viewing Intel RealSense D435i IMU Data

Android C++開発 [3] : 多次元配列をJNIでやり取りする

少し間が空きましたがまたモチベーションが出てきたので続きを書きます(笑)

先回はAndroid実機上で実行するInstrumentedTestでUIを表示しました。
今回はJava側で生成した多次元配列をC++側に渡し、C++側でバイナリデータをセットします。そしてそのデータをJava側に受け渡し、ビットマップとしてImageViewに表示します。

実際のユースケースとしては二つのカメラから取得した映像データを画面に表示するような時に利用できます。特殊ですねw

Activityの変更

先回作成したActivityに二つのImageViewを配置します。
f:id:peugeot-106-s16:20181211155019p:plain ImageViewの名称はそれぞれimageView01, imageView02としておきます。

Java側InstrumentedTestの実装

InstrumentedTestでは後述するC++メソッドに二つのバイナリバッファを渡し、データを格納させます。

@Test
public void imageRefreshTest() throws InterruptedException {
    int stride = 320;
    int lines = 240;
    Bitmap[] bitmaps = {
            Bitmap.createBitmap(stride, lines, Bitmap.Config.ARGB_8888),
            Bitmap.createBitmap(stride, lines, Bitmap.Config.ARGB_8888)
    };
    int[][] buffers = {
            new int[stride * lines],
            new int[stride * lines]
    };

    assertTrue(getBufferArray(buffers));

    bitmaps[0].setPixels(buffers[0], 0, stride, 0, 0, stride, lines);
    bitmaps[1].setPixels(buffers[1], 0, stride, 0, 0, stride, lines);
    this.refreshImageOnUiThread(bitmaps);

    Thread.sleep(5000);
}

下記の処理を行っています。

  • UIに表示する2つのBitmapを生成
  • C++に受け渡す2つバイナリバッファをもつ2次元配列 buffresを生成
  • C++関数 getBufferArray()をコール
  • バイナリバッファの値をBitmapにセットする
  • UIスレッドでBitmapをImageViewに表示する

このコードからわかるようにバッファはJava側で生成されたものです。 このバッファをC++でアクセスしてみましょう。

C++ネイティブコードからJavaの多次元配列にアクセスする

C++側は下記のようなコードとなっています。

native-lib.cpp

extern "C" JNIEXPORT jboolean JNICALL Java_com_example_satoshi_1maemoto_myapplication_ExampleInstrumentedTest_getBufferArray(JNIEnv *env, jobject, jobjectArray array) {
    auto elementCount = env->GetArrayLength(array);
    if (elementCount == 0) {
        return false;
    }

    for (auto index = 0; index < elementCount; index++) {
        jintArray element = (jintArray)env->GetObjectArrayElement(array , index);
        auto buffer = env->GetIntArrayElements(element, nullptr);
        auto bufferCount = env->GetArrayLength(element);

        auto color = ((index % 2) == 0) ? 0x00000000 : 0x00FFFFFF;
        auto step = ((index % 2) == 0) ? 1 : -1;
        for (auto pixel = 0; pixel < bufferCount; pixel++) {
            buffer[pixel] = 0xFF000000 | color;
            color += step;
        }

        env->ReleaseIntArrayElements(element, buffer, 0);
        env->DeleteLocalRef(element);
    }
    return true;
}
  • env->GetArrayLengh()で配列の要素数を取得できる
  • env->GetObjectArrayElement()でJava側2次元配列の1次元目の要素を取得する。このサンプルではint配列が格納されている要素[element]が取得できる。
  • env->GetIntArrayElements()でさらに[element]に格納されているint配列[buffer]を取得する。
  • bufferにデータを格納する
  • 最後に参照を開放する

このようにC++側では配列の要素を取得して利用、使い終わったら参照を開放する、という手順でデータアクセスをします。
少し手間ですがJava側は非同期にGCが走ることがあるのでこのような使い方になるようです。

UIスレッドでBitmapを表示する

C++側からデータが取得できたのでUIに表示しましょう。 この処理も先回のテキスト表示と同様UIスレッドで処理を行わせる必要があります。
そのため下記のようなユーティリティ関数を用意しました。

private void refreshImageOnUiThread(Bitmap[] bitmaps)
{
    class ShowImagesAction implements Runnable
    {
        private Bitmap[] bitmaps;
        public ShowImagesAction(Bitmap[] bitmaps)
        {
            this.bitmaps = bitmaps;
        }

        @Override
        public void run() {
            ImageView imageView01 = rule.getActivity().findViewById(R.id.imageView01);
            imageView01.setImageBitmap(this.bitmaps[0]);

            ImageView imageView02 = rule.getActivity().findViewById(R.id.imageView02);
            imageView02.setImageBitmap(this.bitmaps[1]);
        }
    }

    this.rule.getActivity().runOnUiThread(new ShowImagesAction(bitmaps));
}

実際にInstrumentedTestを実行すると下記のようにグラデーションのかかった2つの映像が表示されます。 f:id:peugeot-106-s16:20181211162205j:plain

まとめ

Andoid実機で動作するInstrumentedTestでC++側コードと多次元配列をやり取りする方法をお伝えしました。

プロジェクト一式は下記にアップしています。

github.com

Android C++開発 [2] : InstrumentedTestでUIを表示する

InstrumentedTestを使えばデバイス上でUnitTestができますが、状態や映像を表示したりするにはUIが表示できると便利です。

InstrumentedTestにActivity表示のルールを追加する

Activityのテストをするにはテストクラスに ActivityTestRule というルールを追加します。
つまりテスト実行時にこのActivityを表示するように、といったルールの定義です。

ExampleInstrumentedTest.java

    @Rule
    public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, true);

この記述を行うとビルドエラーが発生します。
なぜならInstrumentedTest実行時にこのクラスを参照する設定がされていないからです。
build.gradle の dependencies 内、 androidTestImplementation の部分に参照を追加するとビルドエラーは解消します。

build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:rules:0.4'  #コレを追加
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

ActivityにUI要素を追加

実施するテストに応じてActivityに必要なUI要素を追加します。
ここでは文字列を表示する TextView を "textMessage" という名前で追加しました。

f:id:peugeot-106-s16:20181107153712p:plain

テストクラスからUI要素にアクセスする

テストクラスからActivity内のUI要素にアクセスするには、先ほど追加したActivityTestRule からActivityを取得し操作します。
ただUI要素はUIスレッドから操作する必要があるので直接操作することはできず、runOnUiThread()メソッドにアクションを渡し、非同期で実行させる必要があるのですこし面倒でした。 このようなユーティリティメソッドを作成しました。

    private void showMessageOnUiThread(String message)
    {
        class ShowTextAction implements Runnable
        {
            private String message;
            public ShowTextAction(String message)
            {
                this.message = message;
            }

            @Override
            public void run() {
                TextView messageText = rule.getActivity().findViewById(R.id.textMessage);
                messageText.setText(this.message);
            }
        }

        this.rule.getActivity().runOnUiThread(new ShowTextAction(message));
    }

テストメソッドからの呼び出し

以上の準備を行えばテストメソッド内から画面に文字列を表示することができます。
応用すれば様々な操作を行うことができます。それだけではなくこのRuleの真価はUIの自動テストが行えるところにあります。
ボタンをプッシュするといった人が行うようなアクションもシミュレートさせることができます。
さて、テストメソッドでC++から取得した文字列を表示するためにはこのような実装を行いました。

ExampleInstrumentedTest.java

    @Test
    public void stringFromJNITest() throws InterruptedException {
        String message = stringFromJNI();
        this.showMessageOnUiThread(message);

        Thread.sleep(5000);

        assertEquals("Hello from C++ to InstrumentedTest", message);
    }

実行結果はこのようになります。
C++から取得したメッセージが表示されています。

f:id:peugeot-106-s16:20181107154710j:plain

まとめ

Andoid実機で動作するInstrumentedTestで画面を表示し、UI要素にアクセスする方法をお伝えしました。

プロジェクト一式は下記にアップしています。

github.com

Android C++開発 [1] : CMakeプロジェクトの作成

お久しぶりです!
この夏はものすごく忙しくてブログを書く時間がとれませんでした。
何をやっていたかというとAndroidで動作するライブラリをC++で作り、Unityラッパーも作成してUnity上でも利用できるようにする、という事をしていました。
C++でのAndroid開発はかなりレアな体験で、色々と学べたのでシェアします。
レアすぎて需要が無さそうだなあ(笑)

プロジェクトの作成

Android StudioC++を扱うプロジェクトを作成するにはプロジェクト作成のウィザードの中で "Include C++ support" のチェックを付けます。 f:id:peugeot-106-s16:20181107110904p:plain

生成されるプロジェクトは下記のものを含みます。

名前 説明
java 画面のActivity等のJavaソースコード
cpp C++ソースコード
build.gradle(Module: app) ビルド設定

f:id:peugeot-106-s16:20181107111814p:plain

build.gradle(Module: app)について

Android のビルドシステムにはGradleが使われます。
C++のビルドで使われるCMakeとの関連付けはこのファイルの中で行われます。

"externalNativeBuild"の設定でC++ビルドのオプションとしてC++11を利用すること、CMakeのビルド設定が書かれているCMakeLists.txtへのパスが指定されています。
独自のカスタマイズを行う場合はこれらを編集します。

f:id:peugeot-106-s16:20181107112526p:plain

とりあえず実行

このまま特に変更をせずにアプリを起動するとC++側から"Hello from C++"という文字列を取得して画面に表示するものが起動します。 f:id:peugeot-106-s16:20181107113007j:plain

コードで何が行われているかを解説します。
native-lib.cpp

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_satoshi_1maemoto_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

C++側ではマネージドのJavaVM環境と連携をとるためJNIの規約に沿った名前のメソッドが用意されています。
規約としてはJavaのパッケージ名の "." を "_" に置き換え、引数でJavaVMへのポインタを受け取るというのがシンプルな説明です。
実際には様々なかたちのデータを引数でやり取りするため様々なテクニックがあります。
例えば配列の配列を受け取るとか、Java側のコールバックメソッドのアドレスを受け取りC++からJavaをコールバックするなど、、この辺りは次回以降扱いたいと思います。

JNIの規約で作ったC++関数は下記のようにJavaから呼び出すことができます。
MainActivity.java

public class MainActivity extends AppCompatActivity {
    // C++ライブラリ(native-lib.so)をロード
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // C++側の stringFromJNI()関数を呼び出し文字列を取得
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    // C++関数は native 指定で宣言が必要
    public native String stringFromJNI();
}

実機でのUnitTest

プロジェクトの何も手を加えない状態でUnitTestのモックも作成されています。 UnitTestには2種類あり、開発環境内で動作させる通常のUnitTestと、デバイス内で動作をさせるInstrumentedTestがあります。
やっぱり事件は現場で起こるのでできるだけ後者を行いたいものです。ビルド時間はかかってしまうのですが。

あらかじめ用意されているモックは特にC++側との連携はされていないものですが、もちろんC++関数のテストも可能です。 次回以降このInstrumentedTest内でUIを表示するにはどうすれば良いかなども扱いたいです。

ExampleInstrumentedTest.java

@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();

        assertEquals("com.example.satoshi_maemoto.myapplication", appContext.getPackageName());
    }
}

InstrumentedTestの実行は、クラスを右クリックしてRunまたはDebugを選択します。
f:id:peugeot-106-s16:20181107115148p:plain

まとめ

AndroidC++開発のファーストステップをまとめました。

MRTKのAppBarで簡単視野追従フローティングメニューを作ってみた

HoloLensアプリを作っていると、ちょっとしたシンプルなメニューを呼び出して処理を選択したいということがよくあります。
そしてそれがあまり頻繁には使わない機能だったり、見栄えをスッキリさせたいという場合、使わないときはメニューを非表示にしておきたいですよね。
でもエアタップを他のアクションに割り当ててしまっている場合、HoloLensにはマウスの右クリックに相当するジェスチャーがないのでどのようにしてメニューを呼び出すか悩んでしまいますよね。
そこでこんなUIを作ってみました。


FloatingAppBar

  • 空間を数秒ホールドするとフローティングメニュー(AppBar)が現れる
  • メニューは視線を移動しても視野内に追従して付いてくる
  • 項目はフォーカス時、選択時に効果音を鳴らし操作感を上げる

このメニューを適用したプロジェクトはGitHubで公開しています。
エアタップはUnityちゃんの呼び出しに割り当ててあり、フローティングメニューでSpatialMapping表示有無の切り替え、BGMのミュート/ミュート解除を用意しています。

github.com

AppBarの表示、アクションの設定

メニューはAppBarを利用します。
AppBarを利用することで簡単にボタンが並んだメニューを作ることができます。
ボタンのアクションはInteractiveReceiverにリンクさせることで一つのコンポーネントで集中管理することができます。

f:id:peugeot-106-s16:20180528220752p:plain

  • SquareButtonPrefab
    指定先のPrefabでボタンの表示などをカスタマイズできます。今回はアイコンなし、テキストを中央表示にしたPrefabを指定しています
  • Use XXX
    デフォルトで用意されている Remove Adjust Hide ボタンを非表示にします
  • Display Type
    独立型のAppBarとします

f:id:peugeot-106-s16:20180528221642p:plain

  • Type
    Customとします
  • Name ボタン名、アクションを実際に処理するInteractiveReceiverのコードでアクション種別の識別に利用される識別子です
  • Text
    ボタンに表示されるテキスト
  • EventTarget
    アクションを実際に処理するInteractiveReceiverの派生コンポーネントを指定します

AppBarの自動移動の設定

視線を移動した際にメニューが視野内に自動追従してくる機能はSolver関連のRadial Viewコンポーネントを利用することで簡単に実現できます。

f:id:peugeot-106-s16:20180528222002p:plain

カスタマイズポイントとしては、追従時に視野のどの程度の範囲内に留まるかの指定があります。
デフォルトではHoloLensの場合ほとんど視野に入ってこないので小さめにしています。
ただImmersive Headsetで見た場合はかなり狭い範囲に収まってしまって逆に不自然だったので双方で不自然でないギリギリの設定をしました。 ここはもう少し丁寧に作りこむ場合、プラットフォームによって設定値を変えた方が良いと思います。

  • Max View Degrees
    オブジェクトが留まる視野内の範囲を度数で指定します
  • Aspect V
    オブジェクトが留まる範囲の縦横比。横1.0に対する縦の比率。

InteractiveReveiverでのアクション処理

ボタンのタップ処理は指定したInteractiveReceiverに通知され、ボタンのNameを識別子とすることで下記のように振り分け処理を行えます。

HoloLensUnityChan/Control.cs at b4c824c82efffd2b68f77ca3e4e74acc61f2fe8d · SystemFriend/HoloLensUnityChan · GitHub

    protected override void InputClicked(GameObject obj, InputClickedEventData eventData)
    {
        switch (obj.name)
        {
            case "Close":
                this.appBar.gameObject.SetActive(false);
                break;
            case "Mapping":
                this.IsDrawSpatialMappingWireframe = !this.IsDrawSpatialMappingWireframe;
                this.spatialMappingManager.SurfaceMaterial = this.IsDrawSpatialMappingWireframe ? this.spatialMappingMaterialWireframe : this.spatialMappingMaterialOcclusion;
                break;
            case "BGM":
                this.audioSource.volume = (audioSource.volume > 0f) ? 0f : 0.5f;
                break;
            default:
                base.InputClicked(obj, eventData);
                break;
        }
    }

Profileによるボタンの体裁や効果音の変更

Profileを編集することで一括してUIの体裁などを変更することができます。
MRTKにデフォルトで用意されているProfileを変更してしまうと、MRTKのアップデートなどでファイルを上書きした際に設定が戻ってしまうので、コピーを作成しそこに独自の設定を行うことをお勧めします。 また、下記画像で示しているProfileを参照しているHolographicButton Prefab も同様に直接変更せずコピーを作成して編集することをお勧めします。
ここでは独自のProfileを参照することと、アイコン表示をdisableとしています。

f:id:peugeot-106-s16:20180528224823p:plain

ボタンの体裁は AppButtonTextProfile にてテキストを中央揃えとしました。

f:id:peugeot-106-s16:20180528225228p:plain

効果音は AppButtonSoundsProfile にてクリック時の音とフォーカス移動時の音を独自のものに変更しています。

f:id:peugeot-106-s16:20180528225325p:plain

ホールドでのメニュー呼び出し

最後に空間ホールドでのメニュー呼び出しです。
これは、InputManagerにGlobalListnerにハンドラを追加することで実現します。

HoloLensUnityChan/GlobalInputHandler.cs at master · SystemFriend/HoloLensUnityChan · GitHub

public class GlobalInputHandler : MonoBehaviour, IHoldHandler, IInputClickHandler
{
    void Start ()
    {
        InputManager.Instance.AddGlobalListener(this.gameObject);
    }

    void IHoldHandler.OnHoldCompleted(HoldEventData eventData)
    {
        this.controller.appBar.gameObject.SetActive(true);
    }

まとめ

以上で簡単に視野追従型フローティングメニューをアプリに組み込むことができます。

f:id:peugeot-106-s16:20180528231547p:plain

ちょまどさんの筋肉ボイスを動かしてみた

Google Home Miniを貸していただく機会があったので、Microsoftエヴァンジェリストの千代田まどかさんが公開している「筋肉ボイス」を動かしてみました!

ちょまどさん作成のオリジナルソースに少し手を加えたソース一式はGitHubに公開しています。

github.com

ソース以外のAzure FunctionsやActions on Googleの設定を残しておきます。 両方ともほとんどはじめて触ったのでもっと良いやり方があれば教えください。

Azure Functions の設定

Azureへのデプロイ

ソースコードのデプロイはVisual Studioのソリューションエクスプローラーでプロジェクトの右クリックメニュー、「発行...」で行いました。

f:id:peugeot-106-s16:20180512071021p:plain

初めAzure Portalで手動でApp Service等を作ってデプロイしてみたのですが変なエラーが出て動かなかったのですが、ウィザードにおかませしたところ一発でした(笑)

アプリケーション設定

Azure PortalでAzure Functionアプリの「アプリケーション設定」→「アプリケーション設定」にVoiceTextのAPIキーを設定します。

f:id:peugeot-106-s16:20180512072008p:plain f:id:peugeot-106-s16:20180512072137p:plain

関数URLの控え

Azure Portal で「GoogleHome」関数の「関数のURLの取得」からURLを控えておきます。これはGoogle側に後ほど設定します。

f:id:peugeot-106-s16:20180512072727p:plain

ストレージのアクセス許可の変更

デプロイ時に自動作成されたストレージアカウントのBLOB内にmp3ファイルを格納するコンテナを作成し、アクセス許可を与えておきます。

f:id:peugeot-106-s16:20180512073421p:plain

Actions on Googleの設定

Actions on Googleはほんと初めて触ってなんかできたくらいの感じなのですが、一応設定をぺたぺた張っておきます。

  • 言語を日本語に f:id:peugeot-106-s16:20180512073807p:plain

  • 起動フレーズを設定 f:id:peugeot-106-s16:20180512073913p:plain

  • Actionの設定はデフォルトのままで f:id:peugeot-106-s16:20180512074036p:plain

  • DialogFlowではデフォルトIntentにfollow-upとしてfallbackのIntentを追加 f:id:peugeot-106-s16:20180512074357p:plain

  • Contextの設定とWebhookを有効に f:id:peugeot-106-s16:20180512081753p:plain

  • Webhookの設定にAzure Functionsの関数URLを設定 f:id:peugeot-106-s16:20180512080029p:plain

うごいた

f:id:peugeot-106-s16:20180512082439p:plain なにこれめっちゃたのしい

うんこボタンとHoloLensを繋げてみた <後編>

うんこボタンはこの度Makuakeでのクラウドファンディングに成功したインターネットボタンです。 前編ではこのうんこボタンに独自のプログラムを送り込むところまでを扱いました。

satoshi-maemoto.hatenablog.com

ソースコードはこちらで公開しています。

github.com

うんこサーバーの構築

システム構成図におけるサーバーサイドの構築を始めましょう。

f:id:peugeot-106-s16:20180415100423p:plain

サーバーサイドはASP.Net Core 2.0をフレームワークとし、RESTのAPIとWebSocketを実装します。 VisualStudio2017のプロジェクトの作成では[Visual C#]-[Cloud]-[ASP.NET Core Web アプリケーション]をテンプレートとして選択します。

f:id:peugeot-106-s16:20180415213138p:plain

次のダイアログでは [API] を選択し、REST API用コントローラーを備えたプロジェクトを作成します。

f:id:peugeot-106-s16:20180415213408p:plain

さらに、WebSocketを備えたサービスにしてゆきます。
基本的にはこちらの記事とサンプルを参考にさせて頂きました。

tamafuyou.hatenablog.com

幾つか変更点に言及してゆきしょう。

コントローラーで使いたいSingletonオブジェクトを追加

アプリ全体で共有したいシングルトンなオブジェクトはStartup時のConfigureServices()で追加できます。 うんこぼたんからのRESTリクエストをコントローラーが受けた際、WebSocketのサーバークラス[WSServer]にアクセスし、接続中全クライアントへの一斉通知をさせるためにこのようにしています。

UnkoButton/Startup.cs at master · satoshi-maemoto/UnkoButton · GitHub

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<WSServer>();
}

するとコントローラーのコンストラクタでオブジェクトを受け取ることができます。

UnkoButton/UnkosController.cs at master · satoshi-maemoto/UnkoButton · GitHub

public UnkosController(WSServer wsServer)
{
    this.WSServer = wsServer;
}

このUnkosCotrollerのPost()メソッドでWSServerに全クライアントへのdidUnko通知を依頼しています。

public void Post([FromBody]string value)
{
    this.WSServer.BroadcastMessage(new Message()
    {
        MessageType = "DidUnko",
        ClientName = "UnkoButton",
        What = "Unko",
    });
}

WebSocketクライアント間のオブザーバーパターン

元々参考にさせて頂いたチャットサンプルがWebSocketクライアント間のイベント通知をReactを使ったオブザーバーパターンで行うようになっており、とてもスマートな感じになっています。
はじめはうんこボタンもWebSocketクライアントの一つとして、双方向リアルタイムにやりとりをしたいと思ったのですが、Wi-Fi接続やコネクションを保持したままスリープしないのは電池消費につながるのでワンショットのREST方式としました。
なので、オブザーバーパターンは現状それほど生きていないのですが、今後HoloLens複数台やその他のリッチなデバイスがうんこクライアントになった際には双方向にうんこを投げ合うなどの楽しいことができそうです。
HoloLens側でうんこを投げたらうんこボタンが光る、というのをまたそのうちやりたいです。

なお、今のコードではうんこを送ると他クライアントすべてにブロードキャストしつつ自分にもエコーバックでうんこが投げ返されてくるクソ仕様となっています(笑)

HoloLensアプリ

HoloLensアプリはうんこサーバーにWebSocket接続を行い、didUnko通知を待ち受けます。 UWPでWebSocketを利用するため、下記のようなクラスを作成しました。 Unityへの依存はないので、どのようなUWPアプリでも利用できます。

UnkoButton/UnkoServiceClient.cs at master · satoshi-maemoto/UnkoButton · GitHub

using System;

#if NETFX_CORE
using System.Threading.Tasks;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;
#endif

namespace UnkoService
{
    public class UnkoServiceClient
    {
        public delegate void MessageReceivedEventHandler(object sender, string message);
        public event MessageReceivedEventHandler OnMessageReceived;

#if NETFX_CORE
        private MessageWebSocket WebSocket { get; set; }

        public void Initialize()
        {
            this.WebSocket = new MessageWebSocket();
            this.WebSocket.Control.MessageType = SocketMessageType.Utf8;
            this.WebSocket.MessageReceived += this.MessageReceived;
            this.WebSocket.Closed += this.Closed;
        }

        public void Connect(Uri uri, string connectedMessage)
        {
            Task.Run(async () => {
                await Task.Run(async () =>
                {
                    await this.WebSocket.ConnectAsync(uri);
                    await this.SendMessage(connectedMessage);
                });
            });
        }

        public async Task SendMessage(string message)
        {
            await this.SendMessage(this.WebSocket, message);
        }

        private async Task SendMessage(MessageWebSocket webSocket, string message)
        {
            var messageWriter = new DataWriter(webSocket.OutputStream);
            messageWriter.WriteString(message);
            await messageWriter.StoreAsync();
        }

        private void MessageReceived(MessageWebSocket sender, MessageWebSocketMessageReceivedEventArgs args)
        {
            var messageReader = args.GetDataReader();
            messageReader.UnicodeEncoding = Windows.Storage.Streams.UnicodeEncoding.Utf8;
            var messageString = messageReader.ReadString(messageReader.UnconsumedBufferLength);
            this.OnMessageReceived?.Invoke(this, messageString);
        }

        private void Closed(IWebSocket sender, WebSocketClosedEventArgs args)
        {
        }
#endif
    }
}

リポジトリには入れていませんが、このUnkoServiceClientをUnityのスクリプトからは下記のように利用します。

    private UnkoServiceClient unkoServiceClient;

    void Start()
    {
  #if NETFX_CORE
        var context = SynchronizationContext.Current;

        this.unkoServiceClient = new UnkoServiceClient();
        this.unkoServiceClient.OnMessageReceived += (s, m) =>
        {
            context.Post((state) =>
            {
                this.GenerateUnko();
            }, null);
        };
        this.unkoServiceClient.Initialize();
        this.unkoServiceClient.Connect(new Uri("ws://xxxx.azurewebsites.net/ws"),
        JsonConvert.SerializeObject(new JoinMessage() { ClientName = "HoloLens", MessageType = "JoinMessage" }));
#endif
    }

まとめ

以上でうんこボタンを押すとうんこが出せるようになりました。 なんかうんこうんこ書きすぎて気分が悪くなってきましたww


Unko button feat HoloLens

IoTデバイスとHoloLensを連携させる際の参考にしていただければ幸いです。