Meta Questを使ってMR - 現実と仮想を融合する デバイス連動編 -

5.3K Views

August 31, 24

スライド概要

Meta Questを使い現実世界と仮想世界を融合させる。
現実世界のデバイスの操作や情報の表示を、MRデバイスを使い仮想のインターフェース上で実現する方法を解説します。
・現実世界の扇風機をMRのインターフェースから操作(コンセントのオンオフ)
・現実世界の温度/湿度センサーの情報をMRで表示
・現実世界のスイッチを押すと、仮想世界が変化する

profile-image

XR(VR/AR)のエバンジェリスト。XR技術で世界を変えていきましょう。 https://majimajiwaroze.connpass.com/

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

(ダウンロード不可)

関連スライド

各ページのテキスト
1.

MRで現実と仮想の境界をなくす - META QUEST ⇔ デバイス連動 - 2025.01.18

2.

大久保 聡 Mail [email protected] Twitter @followapp

3.

1.それほど遠くない未来 2.仮想を通じて現実を操作する 3.仮想を通じてみえない情報を見る 4.現実を通じて仮想を操作する

4.

1.それほど遠くない未来

5.

インターフェース がMRデバイスに統合される 1 Human Interface 2 Sensor Device Life Log 新しいデータが蓄積され 3 Spatial Map +Data データが現実の位置情報と 連動できる

6.

必要な情報が 必要な時に 必要な場所で利用でき 現実のものに 仮想のIFを通じて働きかけることができる

7.

仮想というIFを通じて現実を操作 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 現実の操作 情報の利用

8.

2.仮想を通じて現実を操作する

9.

仮想を介して現実(デバイス)を操作する 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 現実の操作

10.

META QUESTを用い、現実の操作を行う  Meta Questの仮想スイッチから、現実空間の扇風機(コンセントのオン・オフ)を操作する。  今回は家電操作のPythonのライブラリが充実していることもあり、現実へのIF部分はPythonを用いる。  Meta QuestからはWeb APIを経由してアクセスさせる。

11.

スマートコンセント https://www.tp-link.com/jp/smart-home/tapo/tapo-p105/  ネットワークからオン・オフできるスマートコンセント「tp-lin Tapo P105」を使って家電を操作す る。  1個1000-1500円くらい。  スマホのアプリ、音声アシスタント、IFTTTからコンセントのオン・オフができる。  Wifi(ローカルエリアネットワーク)で直接操作もできる。。 Tapo P105

12.

システム構成  コンセントオンオフで制御できる家電ということで、扇風機をP105 経由で操作します。複雑な操作 が必要な場合は、IFTTTや赤外線リモコンを経由して操作すれば良いと思います。  Webサーバー 兼 家電とのIFはRaspberryPiを利用します。 Web Server (fastapi) Client API (tapo) Python 3 Tapo P105 Quest3 WebAPI RaspberryPI 5

13.

RASPBERRY PIの環境構築  Python仮想環境の作成し、仮想環境に切替ます。 python3 -m venv .venv source .venv/bin/activate  Webサーバーをインストールします。 pip install --upgrade fastapi  開発環境はVS Codeを使います。 Pythonのプログラム開発用に、Visual Studio Codeをインストールします。 sudo apt update sudo apt install code

14.

開発環境(VS CODE)の設定  Pythonの実行環境のエクステンションをインストールします。

15.

環境構築  Tp-Link のスマートプラグ操作用のライブラリインストールします。 pip install --upgrade tapo

16.

準備、スマホでTAPOのIPアドレスの確認  TAPOのスマホアプリを起動し、デバイス一覧からTapo P105を選択します。  詳細ボタンを押し、割り当てられているIPアドレスを確認します。

17.
[beta]
TAPOへのアクセスとWEB API
 onplug/{name}とoffplug/{name}というIFを作成し実装します。
 Tapoの利用時に作成したユーザーのIDとPWが接続時に必要となります。
 調べたIPアドレスとnameを対応させ、それぞれにアクセスできるようなIFにします。
Import uvicorn
From fastapi import FastAPI, HTTPException, Request
From starlette.exceptions import HTTPException as StarletteHTTPException
Import asyncio
Import os
From tapo import ApiClient
From fastapi.responses import JSONResponse
Import struct

@app.get("/offplug/{name}")
async def off(
name: str
):
ip = plugs[name]
device = await client.p105(ip)
await device.off()
return {"Result": "OK"}

app = FastAPI()
Client = ApiClient(“id", “password")
Plugs = {'Plug1':'192.168.11.10', 'Plug2':'192.168.11.11’}

if __name__ == "__main__":
uvicorn.run(app, host="192.168.11.9", port=8000)

@app.get("/onplug/{name}")
async def on(
name: str
):
ip = plugs[name]
device = await client.p105(ip)
await device.on()
return {"Result": "OK"}

18.
[beta]
QUESTからWEB APIを叩く
 トグルボタンのオン/オフでコンセントと、仮想空間内の風のオン/オフを切り替えます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading;
public class DeviceControll : MonoBehaviour
{
private Toggle toggle;
[SerializeField]
WindZone windZone;

public void OnToggleChanged()
{
if(toggle.isOn)
{
SendWebRequest("onplug", this.GetCancellationTokenOnDestroy()).Forget();
windZone.windTurbulence = 0.5f;
}
else
{
SendWebRequest("offplug", this.GetCancellationTokenOnDestroy()).Forget();
windZone.windTurbulence = 0;
}
}

[SerializeField]
string url = "http://192.168.11.9:8000";

async UniTask SendWebRequest(string command, CancellationToken ct = default)
{
var uriBuilder = new System.UriBuilder(url + "/" + command + "/" + deviceName);
var request = UnityWebRequest.Get(uriBuilder.Uri);

[SerializeField]
string deviceName = "Plug1";

await request.SendWebRequest().ToUniTask(cancellationToken: ct);
// Start is called before the first frame update
void Start()
{
toggle = GetComponent<Toggle>();
}

}
}

19.

動作イメージ  現実世界の扇風機が動き出すと、仮想の世界の木が風で揺れるデモです。 Youtube動画をご覧ください。 https://youtu.be/kfgXEEcWHXQ

20.

3.仮想を通じてみえない情報を見る

21.

仮想を通じてみえない情報(デジタル情報)を見る 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 情報の利用

22.

META QUESTを用い、現実の可視化を行う  温度湿度(IoT機器のセンサー情報)を、Meta Questの仮想空間に表示する。  今回はIoT機器のPythonのライブラリが充実していることもあり、温湿度計へのIF部分はPythonを用 いる。  Meta QuestからはWeb APIを経由してアクセスさせる。

23.

BLE温度・湿度計 https://inkbird.com/collections/thermometers-and-hygrometers  Bluetooth Low Energy(無線)で温度・湿度が取得できる「INK BIRD IBS-TH1 MINI 」を使って、 環境情報を可視化する。  IBS-TH2が2000円くらい。  スマホアプリ、Bluetooth接続で値の取得ができる。 IBS-TH2 IBS-TH1 MINI

24.

システム構成  Webサーバー 兼 家電とのIFはRaspberryPiを利用します。 Web Server (fastapi) BLE Library (bluepy3) Python 3 Quest3 IBS-TH1 MINI WebAPI RaspberryPI 5

25.

RASPBERRY PIの環境構築  Python仮想環境の作成し、仮想環境に切替ます。 python3 -m venv .venv source .venv/bin/activate  Webサーバーをインストールします。 pip install --upgrade fastapi  開発環境はVS Codeを使います。 Pythonのプログラム開発用に、Visual Studio Codeをインストールします。 sudo apt update sudo apt install code

26.

開発環境(VS CODE)の設定  Pythonの実行環境のエクステンションをインストールします。

27.

準備、IBS-TH1 MINI のMACアドレス確認  MACアドレスを調べる Bluetoothデバイスの一覧を表示し、デバイス名がspsのものを確認する。 $ sudo hcitool lescan LE Scan ... DE:FF:EF:47:C8:C8 (unknown) B0:7E:11:EE:71:51 sps 67:AA:3D:83:1D:34 (unknown) 67:AA:3D:83:1D:34 (unknown) A8:10:87:50:F7:96 (unknown) A8:10:87:50:F7:96 sps 45:11:89:9C:19:89 (unknown) CF:9B:A7:81:69:62 (unknown)

28.

環境構築  BLEアクセス用のライブラリをインストール pip install bs4 lxml requests sudo apt-get install libglib2.0-dev libbluetooth-dev pip install --upgrade bluepy3  bluepy3-helperに権限付与 sudo setcap cap_net_raw,cap_net_admin+ep .venv/lib/python3.11/sitepackages/bluepy3/bluepy3-helper

29.
[beta]
BLEへのアクセスとWEB API
 gettemp/{name}というIFを作成し実装します。
 調べたMACアドレスとnameを対応させ、それぞれにアクセスできるようなIFにします。
import uvicorn
from fastapi import FastAPI, HTTPException, Request
from starlette.exceptions import HTTPException as StarletteHTTPException
import asyncio
import os
from fastapi.responses import JSONResponse
from bluepy3 import btle
import struct
app = FastAPI()
Thermometers
= {'Thermometer1':'B0:7E:11:EE:71:51', 'Thermometer2':'A8:10:87:50:F7:96'}

@app.get("/gettemp/{name}")
async def off(
name: str
):
mac = Thermometers[name]
sensorValue = await get_ibsth1_mini_data(mac)
return {
"result": "OK",
"temp": sensorValue['Temperature’],
"humidity": sensorValue['Humidity’]
}

if __name__ == "__main__":
uvicorn.run(app, host="192.168.11.9", port=8000)
async def get_ibsth1_mini_data(macaddr):
peripheral = btle.Peripheral(macaddr)
characteristic = peripheral.readCharacteristic(0x002d)
(temp, humid, unknown1, unknown2, unknown3) = struct.unpack('<hhBBB', characteristic)
sensorValue = {
'Temperature': temp / 100,
'Humidity': humid / 100,
'unknown1': unknown1,
'unknown2': unknown2,
'unknown3': unknown3,
}
peripheral.disconnect()
return sensorValue

30.
[beta]
QUESTからWEB APIを叩く
 一定間隔ごとに、温度と湿度を取得しTextの文字を更新します。
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using System.Threading;
using TMPro;
[System.Serializable]
public class Result
{
public string result;
public string temp;
public string humidity;
}
public class GetTemp : MonoBehaviour
{
[SerializeField]
string url = "http://192.168.11.9:8000";
[SerializeField]
string deviceName = "Thermometer1";
[SerializeField]
TextMeshProUGUI temperatureText;

[SerializeField]
TextMeshProUGUI humidityText;
[SerializeField]
Image image;
private async void Start()
{
while (true)
{
// n秒ごとに処理を行う
await UniTask.Delay(5000);
SendWebRequest("gettemp", this.GetCancellationTokenOnDestroy()).Forget();
}
}

31.
[beta]
QUESTからWEB APIを叩く
async UniTask SendWebRequest(string command, CancellationToken ct = default)
{
var uriBuilder = new System.UriBuilder(url + "/" + command + "/" + deviceName);
var request = UnityWebRequest.Get(uriBuilder.Uri);
await request.SendWebRequest().ToUniTask(cancellationToken: ct);
// Result:{"Result":"OK","Temp":27.25,"Humidity":62.66}
Result result = JsonUtility.FromJson<Result>(request.downloadHandler.text);
if (double.TryParse(result.temp, out double temp))
{
temperatureText.text = temp.ToString("F1") + "℃";
}
else
{
temperatureText.text = "- ℃";
}
if (double.TryParse(result.humidity, out double humidity))
{
humidityText.text = humidity.ToString("F1") + "%";
}
else
{
humidityText.text = "- %";
}
}
}

32.

動作イメージ  現実の温度と湿度が、仮想世界に重畳して表示されるデモです。 Youtube動画をご覧ください。https://youtu.be/XnPf5r7HTsM

33.

4.現実を通じ仮想を操作する

34.

現実(スイッチ)を通じ仮想を操作する 現実 IoT機器 家電 スマホ 車 etc. デジタル の 情報 仮想 という IF 情報の利用

35.

現実(M5STACK)のスイッチを用い、仮想の操作を行う  M5Stack AtomS3Fの現実のスイッチから、仮想空間のオブジェクトを操作する。 Atomのボタンを押すとオンとオフが交互に切り替わる。そのボタンのオンとオフの状態をQuest側で 受け取り、リアクションを行う。  今回はMQTT(パブサブモデル)を使い、スイッチのオン・オフの情報をパブリッシュして、Quest側で その情報をサブスクライブさせ仮想のオブジェクトを変化させる。

36.

スイッチに使うデバイス  M5Stack AtomS3Rをスイッチとして利用する。小型でWifiにつながりスイッチとディスプレイもつ いており、充電式ボタン電池(3.7V)で駆動させることも可能で携帯性にすぐれる。  AtomS3Rが2800円くらい。電池基盤が1600円くらい。バッテリーと充電器が2000円くらい。 M5ATOM用LIR2032コイン形 リチウムイオン電池基板

37.

システム構成  IoT通信に広く使われるMQTT(Message Queuing Telemetry Transport)を使いメッセージを送受 信させる。MQTTの実装は、AtomはPubSubClient、QuestはMQTT for Unity($32.99)を利用。  メッセージを仕分けするブローカーは、パブリックブローカーのEMQX(サーバーレス無料枠)を利用す る。 MQTT for Unity Quest3 Subsctiber パブリッシュ Topic:Quest/Button Topic:Quest/Button サブスク ライブ MQTT for Unity M5Stack AtomS3R Subsctiber Broker PubSubClient Publisher Quest3

38.

開発環境(VS CODE)の設定  開発環境はVS Codeを使います。WindowsでもMacでもお好きなプラットフォームにインストール してください。 (Arduino IDEを利用していただいても構いませんが、コンパイル時間がかなり遅かっ たためPlatfromIOを利用しました。)  M5Stack開発用のPlatformIO IDEとJapanise Language Packの拡張機能(エクステンション)をイ ンストールします。拡張機能のアイコンを選択し、拡張機能を検索してインストールします。

39.

プロジェクトを作成する  PlatformIOのアイコンを選択し、Create New Projectを選択します。  プロジェクト名、BoardにAtom、FrameworkにArduinoを選択し作成します。

40.

設定ファイルの作成  platformio.iniを開き、iniファイルを下記のように書き換える。必要なライブラリなどの依存関係と、 M5Stackと接続するためのシリアル通信のポートが記載されています。ポート番号はご自分の環境に あわせて書き換えてください。

41.

設定の確認  この状態で一度ビルドしてみます。ビルドアイコンをクリックします。SUCCESSが表示されたらビ ルド成功です。 シリアルモニター ビルドしてデバイスへプログラムを転送 ビルド platformIOのHome画面表示

42.
[beta]
プログラムの作成(ATOM側) 接続に必要な情報
 接続に必要なクラス、設定回り。
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
WiFiClientSecure httpsClient;
PubSubClient mqttClient(httpsClient);
// Wi-FiのSSID
const char* ssid = "Shinkansen_Free_Wi-Fi";
// Wi-Fiのパスワード
const char* password = "Password";
// MQTTの接続先のIP
const char* endpoint = "broker.emqx.io";
// MQTTのポート
const int mqttPort = 8883;
const char* mqttUsername = "emqx";
const char* mqttPassword = "public";
// メッセージを知らせるトピック
const char* pubTopic = "Quest/Button";
// メッセージを待つトピック
const char* subTopic = "Quest/+";

// Root CA Certificate
// Load DigiCert Global Root G2, which is used by EMQX Public Broker: broker.emqx.io
const char* caCert = ¥
"-----BEGIN CERTIFICATE-----¥n" ¥
"MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh¥n" ¥
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3¥n" ¥
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD¥n" ¥
"QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT¥n" ¥
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j¥n" ¥
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG¥n" ¥
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB¥n" ¥
"CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97¥n" ¥
"nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt¥n" ¥
"43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P¥n" ¥
"T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4¥n" ¥
"gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO¥n" ¥
"BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR¥n" ¥
"TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw¥n" ¥
"DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr¥n" ¥
"hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg¥n" ¥
"06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF¥n" ¥
"PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls¥n" ¥
"YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk¥n" ¥
"CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=" ¥
"-----END CERTIFICATE-----¥n";

43.
[beta]
プログラムの作成(ATOM側)

WIFI接続

 SSIDとパスワードを指定して接続する。接続するまでひたすら待ち続ける。
void wifiLoop() {
if (WiFi.status() != WL_CONNECTED) {
connectToWiFi();
}
}
void connectToWiFi() {
// Start WiFi
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.println("connecting ...");
delay(1000);
}
// WiFi Connected
Serial.println("¥nWiFi Connected.");
}

44.
[beta]
プログラムの作成(ATOM側)

MQTT接続

 setServerで接続情報、setCallbackで購読しているトピックが届いた際に呼び出す関数を指定する。
 購読するトピックは、接続完了後にsubscribeメソッドで指定する。
void mqttLoop() {
if (!mqttClient.connected()) {
connectMQTT();
}
mqttClient.loop();
}
void connectMQTT() {
// Set Root CA certificate
httpsClient.setCACert(caCert);
mqttClient.setServer(endpoint, mqttPort);
mqttClient.setKeepAlive(60);
mqttClient.setCallback(mqttCallback);
}

String deviceID = "AtomS3R-";
deviceID += String(random(0xffff), HEX); // 接続デバイスごとにユニークなID
while (WiFi.status() == WL_CONNECTED && !mqttClient.connected()) {
if (mqttClient.connect(deviceID.c_str(), mqttUsername, mqttPassword)) {
Serial.println("MQTT Broker Connected.");
int qos = 0;
mqttClient.subscribe(subTopic, qos);
Serial.println("Subscribed.");
} else {
Serial.print("Failed. Error state=");
Serial.println(mqttClient.state());
char buf[256];
httpsClient.lastError(buf, 256);
Serial.print("SSL error: ");
Serial.println(buf);
// Wait 5 seconds before retrying
delay(5000);
}
}

45.
[beta]
プログラム作成(ATOM側) SETUP関数
 初期画面表示と、WifiとMQTTブローカーへの接続を行う。
void setup() {
auto cfg = M5.config(); // 本体初期設定
M5.begin(cfg);
Serial.begin(115200);
delay(1000);
// シリアル出力開始待ち
pinMode(BTN_GPIO, INPUT_PULLUP); // 本体ボタン端子入力設定
// 液晶初期化
M5.Lcd.init();
// Wifi接続
connectToWiFi();
// 画面表示
M5.Lcd.fillRect(0, 30, 128, 128, TFT_BLACK);
M5.Lcd.setTextColor(M5.Lcd.color565(90, 90, 90));
M5.Lcd.drawString("MQTT", 12, 2, &fonts::Font4);
toggle(isOn);
// MQTTサーバー接続
connectMQTT();
}

46.
[beta]
プログラム作成(ATOM側) LOOP関数
 Atomのボタン押下状態を監視、ボタンを押すとオンとオフが交互に切り替わるようにする。
void loop() {
// 常にチェックして切断されたら復帰できるように
wifiLoop();
mqttLoop();

// ボタンOFF
if (digitalRead(BTN_GPIO) == true) { // 本体ボタンがOFFなら
btn_state = false;
// ボタン単押し状態をfalseへ
press_state = false;
// ボタン長押し状態をfalseへ
}

// ボタン長押しカウント値リセット
if (digitalRead(BTN_GPIO) == LOW) {
// 本体ボタンONなら
If (press_state == false) {
// ボタン長押し状態でなければ
press_time = millis();
// ボタン長押し時間初期値セット
press_state = true;
// ボタン長押し状態に変更
}
if (millis() >= (press_time + 800)) { // ボタン長押し時間初期値 + 800msなら
// 長押しの処理
Serial.println("Long press");
}
}
// ボタンON
if (digitalRead(BTN_GPIO) == LOW && btn_state == false) {
btn_state = true;
// ボタン単押し状態をtrueへ
Serial.println("Press");
// トグルの反転
isOn = !isOn;
toggle(isOn);
}

// ボタン押下の処理

}

47.
[beta]
プログラム作成(ATOM側) パブリッシュ部分
 ボタンのオンとオフの切り替わりで、その状態をQuest/Buttonとうトピックでパブリッシュする。
void toggle(bool button) {
if (button) {
// オン
Serial.println("On");
mqttClient.publish(pubTopic, "ON");
M5.Lcd.fillRect(0, 30, 128, 128, TFT_WHITE);
M5.Lcd.setTextColor(M5.Lcd.color565(0, 0, 0));
M5.Lcd.drawString("ON", 43, 65, &fonts::Font4);
} else {
// オフ
Serial.println("Off");
mqttClient.publish(pubTopic, "OFF");
M5.Lcd.fillRect(0, 30, 128, 128, M5.Lcd.color565(30, 30, 30));
M5.Lcd.setTextColor(M5.Lcd.color565(90, 90, 90));
M5.Lcd.drawString("OFF", 38, 65, &fonts::Font4);
}
}

48.

UNITY側設定  MQTT for UnityをAsset Storeで購入。Assetをインストール。  SimpleMqttフォルダのPrefabsにある、MqttClientをシーンに配置する。

49.

MQTT CLIENTの設定  Inspectorで、Mqtt ClientのHost/Portの設定。どのメッセージを購読するかサブスクライブの設定。  購読したメッセージ受信時に呼び出す関数を指定。

50.

購読したメッセージに応じた処理を記載  受け取ったメッセージをハンドリングする処理。オン・オフに合わせてGameObjectのアクティブを 切り替え。今回は、雨雲の雨のパーティクルのアクティブを切り替えるように設定しました。 public void OnMessageArrived(MqttMessage m) { string value; switch (m.GetTopic()) { case "Quest/Button": Debug.Log(m.GetString()); value = m.GetString(); break; default: value = ""; break; } switch (value) { case "ON": ObjectToHandle.SetActive(true); break; case "OFF": ObjectToHandle.SetActive(false); break; default: break; } }

51.

パーミッションの設定  Player SettingsでCustom Main Manifestを有効にし、通信回りのパーミッション設定を行います。  usesCleartextTrafficは、Android9以降はデフォルトがfalseになっているのでHTTP通信の場合は Trueに変更が必要です。INTERNET/ACCESS_NETWORK_STATEを追記しました。 <?xml version="1.0" encoding="utf-8" standalone="no"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:installLocation="auto"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <application android:label="@string/app_name" android:icon="@mipmap/app_icon" android:allowBackup="false"> <activity android:theme="@style/Theme.AppCompat.DayNight.NoActionBar" android:configChanges="locale|fontScale|keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touc hscreen|uiMode" android:launchMode="singleTask" android:name="com.unity3d.player.UnityPlayerGameActivity" android:excludeFromRecents="true" android:exported="true" android:usesCleartextTraffic="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="com.oculus.intent.category.VR" /> </intent-filter> <meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> <meta-data android:name="com.oculus.vr.focusaware" android:value="true" /> </activity> <meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="false" /> <meta-data android:name="com.samsung.android.vr.application.mode" android:value="vr_only" /> <meta-data android:name="com.oculus.ossplash.background" android:value="black" /> <meta-data android:name="com.oculus.telemetry.project_guid" android:value="11f20e45-8162-4c89-9cf8-003f285cbdb3" /> <meta-data android:name="com.oculus.supportedDevices" android:value="quest|quest2|questpro|quest3|quest3s" /> </application> <uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="true" /> </manifest>

52.

動作イメージ  QuestとAtomがネットワークにつながる状態で起動。  現実世界のAtomのボタンを押すと、Questの仮想環境に雨が降るデモです。 https://youtu.be/s0xmgifXtqs