つくるの大好き。

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

HoloLensをオフラインでコントロールする

状況は変わりつつありますが、諸事情でHoloLensアプリをUSB接続のみでコントロールしたいというニーズがあります。
ただHoloLensのUSBはDevicePortalへのアクセスと充電程度にしか使うことができず、通信用途には使えません。

しかし、DevicePortalのFileExprolerにはファイル転送機能があるので、この機能を工夫すればUSBでアクションを送れるのではないかと考えました。
つまりファイルをUSB経由でアップロードし、HoloLens側はファイルのアップロードを検知して何らかのアクションを実行します。
ファイル内にコマンドを書いておいて、それを解釈するようにすればなんでもできるというわけです。

ファイルの変更検知

HoloLensというかUWPアプリではファイルの変更検知を行うことができます。
下記コードで検知をおこなえます。

Controller.cs : メインスクリプト

using UnityEngine;

#if NETFX_CORE
using System.Threading;
using System.Threading.Tasks;
#endif

public class Controller : MonoBehaviour
{
    public bool isChanged = true;
    public GameObject cube;
#if NETFX_CORE
    private CommandReceiver CommandReveicer { get; set; }
#endif

    void Start()
    {
#if NETFX_CORE
            this.CommandReveicer = new CommandReceiver();
            this.CommandReveicer.OnCommandReceived += (s, o) =>
            {
                Debug.Log(string.Format("RECEIVE: {0}", o.Command));
                this.isChanged = !this.isChanged;
            };
            this.CommandReveicer.Start();
#endif
    }

    void Update()
    {
        this.cube.SetActive(this.isChanged);
    }
}

HoloLens実機で動作するときのみCommandReceiverクラスのインスタンスを生成し、実行します。
ファイル検知があった場合、Cubeの表示をオンオフして、それを知らせるようにしています。

CommandReceiverは下記のとおりです。

#if NETFX_CORE

using System;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Search;

public class CommandReceiver
{
    public delegate void CommandReceivedEventHandler(object sender, CommandReceivedEventArgs e);
    public event CommandReceivedEventHandler OnCommandReceived;
    private StorageFileQueryResult FileQuery { get; set; }

    public void Start()
    {
        List<string> fileTypeFilter = new List<string>()
        {
            ".txt",
        };

        var options = new QueryOptions(CommonFileQuery.OrderByName, fileTypeFilter);
        this.FileQuery = ApplicationData.Current.LocalFolder.CreateFileQueryWithOptions(options);
        this.FileQuery.ContentsChanged += this.Query_ContentsChanged;
        Task.Run(() =>
        {
            var files = this.FileQuery.GetFilesAsync();
        });
    }

    public void Stop()
    {
        this.FileQuery.ContentsChanged -= this.Query_ContentsChanged;
    }

    private async void Query_ContentsChanged(IStorageQueryResultBase sender, object args)
    {
        try
        {
            var file = await sender.Folder.GetFileAsync("command.txt");        
            var json = await FileIO.ReadTextAsync(file);
            this.OnCommandReceived?.Invoke(this, new CommandReceivedEventArgs(json));
        }
        catch (FileNotFoundException e)
        {
            Debug.WriteLine(e.ToString());
        }
    }
}

#endif

command.txtというファイルの変更を検出するとイベントを発生させます。
CommandReceivedEventArgsクラスは下記のようなシンプルなものです。

public class CommandReceivedEventArgs
{
    public string Command { get; private set; }

    public CommandReceivedEventArgs(string command)
    {
        this.Command = command;
    }
}

このアプリをUSB接続したHoloLensで起動し、DevicePortalのFileExplorerからcommand.txtをアップロードするとCubeの表示がオンオフされ、USB経由でアクションが伝わっていることが確認できます。

HoloLensのREST API

HoloLensのDevicePortal機能はREST APIで公開されています。

https://developer.microsoft.com/ja-jp/windows/holographic/device_portal_api_reference

しかしこの中にはFileExplorerの機能は明記されていません。

ではちょっとChromeであればF12キーでNetworkの様子を調べてみましょう。

これはファイルリストを取得したもの
f:id:peugeot-106-s16:20161101230200p:plain

こちらはファイルをアップロードしたもの
f:id:peugeot-106-s16:20161101230309p:plain

それぞれ下記のようなAPIとなっていました。

ファイルリスト取得 GET http://127.0.0.1:10080/api/filesystem/apps/files?knownfolderid=LocalAppData&packagefullname=HoloFileWatchTest_1.0.0.0_x86__pzq3xp76mxafg&path=%5CLocalState&_=1478006852494
ファイルアップロード POST http://127.0.0.1:10080/api/filesystem/apps/file?knownfolderid=LocalAppData&packagefullname=HoloFileWatchTest_1.0.0.0_x86__pzq3xp76mxafg&path=%5CLocalState

フォルダ名、アプリパッケージ名、フォルダからのパスを指定します。
ファイルアップロードはMultipart形式でファイルを添付してPOSTリクエストを行えばOKです。
なおDevicePortalはBASIC認証がかかっているので、リクエストする際にはBASIC認証のヘッダをつける必要があります。

using RestSharp;
using RestSharp.Authenticators;
using System;
using UnityEngine;
using UnityEngine.UI;

public class Controller : MonoBehaviour {

    public InputField idText;
    public InputField passwordText;
    public InputField baseUrlText;
    public InputField knownFolderIdText;
    public InputField packageFullNameText;
    public InputField pathText;
    public Text logText;

	void Start ()
    {
	}
	
	void Update ()
    {
	}

    public void GetFiles()
    {
        var url = this.baseUrlText.text;

        Debug.Log("GET URL=" + url);
        var client = new RestClient();
        client.BaseUrl = new Uri(url);
        client.Authenticator = new HttpBasicAuthenticator(this.idText.text, this.passwordText.text);

        var request = new RestRequest("api/filesystem/apps/files", Method.GET);
        request.AddParameter("knownfolderid", this.knownFolderIdText.text);
        request.AddParameter("packagefullname", this.packageFullNameText.text);
        request.AddParameter("path", this.pathText.text);

        var response = client.Execute(request);
        var log = string.Empty;
        if (response.ErrorException != null)
        {
            log = "ERROR: " + response.StatusCode + "  " + response.ErrorException.ToString();
        }
        else
        {
            log = "RESPONSE: " + response.StatusCode + "  " + response.Content;
        }
        this.logText.text = log;
        Debug.Log(log);
    }

    public void PostFile()
    {
        var url = this.baseUrlText.text;

        Debug.Log("POST URL=" + url);
        var client = new RestClient();
        client.BaseUrl = new Uri(url);
        client.Authenticator = new HttpBasicAuthenticator(this.idText.text, this.passwordText.text);

        var resource = "api/filesystem/apps/file" + 
            string.Format("?knownfolderid={0}&packagefullname={1}&path={2}", 
            this.knownFolderIdText.text, 
            this.packageFullNameText.text, 
            WWW.EscapeURL(this.pathText.text));
        var request = new RestRequest(resource, Method.POST)
        {
            AlwaysMultipartFormData = true
       
        };
        request.AddHeader("Content-Type", "multipart/form-data");

        var filePath = Application.streamingAssetsPath + "/command.txt";
        Debug.Log("FILE PATH=" + filePath);
        request.AddFile("file", filePath);

        var response = client.Execute(request);
        var log = string.Empty;
        if (response.ErrorException != null)
        {
            log = "ERROR: " + response.StatusCode + "  " + response.ErrorException.ToString();
        }
        else
        {
            log = "RESPONSE: " + response.StatusCode + "  " + response.Content;
        }
        this.logText.text = log;
        Debug.Log(log);
    }
}

画面にはIDやパスワードなどの入力項目を設け、入力値を上記クラスから参照するかたちです。
f:id:peugeot-106-s16:20161101234409p:plain

上記を実装すれば、PCのプログラムからUSB接続のHoloLensアプリを自在にコントロールできるようになります。
わかりにくい動画ですが、PCでボタンを押すとホログラムのCUBEの表示がオンオフします。

youtu.be