3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

農工大Advent Calendar 2024

Day 12

Quest上のUnity->ESP32のOSC通信

Last updated at Posted at 2024-12-11

はじめに

この記事は,農工大アドベントカレンダー Advent Calendar 2024 12日目の記事となります.

私が所属するサークルでのゲーム開発時にQuest上のUnityとESP32でOSC通信を担当したのでその実装を話していきます.

本記事ではQuest -> ESP32の一方向通信のみを行います.双方向通信やESP32->Questがしたい方は別の記事を参照してください.

開発環境

  • Windows11

使用環境

  • Meta Quest 3
  • ESP32 DEVKIT V1
  • Unity 2022.3.36f1
  • OSCJack 2.0.0

サンプルリポジトリ

本記事で実装する内容は以下のリポジトリに置いてあります.

Unity

ライブラリ準備

OSCJackというライブラリを用います.

OscCoreというライブラリも存在しますが,少なくとも私の環境ではOscCoreはQuestからOSC通信が飛ばせなかったのでOscJackを使用します.

READMEのHow To Installを参考にインストールしていきます.

Edit>Project Settings>PackageManagerを開いて以下を追加します.

Name: Keijiro
URL: https://registry.npmjs.com
Scope: jp.keijiro

image.png

そうしたらWindow>PackageManagerを開き,PackagesMy Registriesに変更します.
image.png
スクロールしていくとOSC Jackがあると思うのでそれをインストールします.
image.png

これでインストールができました.

OSCメッセージの送信

コンポーネントでも送信できるようですが,今回はスクリプトを用いて送信を行います.
IPアドレスとポートを指定して,スペースキーかプライマリボタンが押されたときにアドレスを/sampleとして文字列を送信するスクリプトを適当な場所に配置します.今回はAssets/Scripts/直下にSendMessage.csという名前で置きます.

今回は文字列を送信しますが,intやfloatなども送信できるようです.

SendMessage.cs
using UnityEngine;
using UnityEngine.XR;
using OscJack;

// OSCメッセージを送信するクラス
public class SendMessage : MonoBehaviour
{
    [SerializeField] private string ipAddress = "ESP's ipaddress"; // OSCメッセージを受信するデバイスのIPアドレス
    [SerializeField] private int port = 8000; // 送信先のポート番号
    private OscClient _client; // OSC通信を行うためのクライアント
    private const XRNode ControllerNode = XRNode.RightHand; // XRコントローラ(右手デバイス)を指定

    // コンポーネントが有効化されたときに呼び出される
    private void OnEnable()
    {
        _client = new OscClient(ipAddress, port); // 指定したIPアドレスとポート番号でOSCクライアントを初期化
    }

    // コンポーネントが無効化されたときに呼び出される
    private void OnDisable()
    {
        _client.Dispose(); // OSCクライアントを解放
    }

    private void Update()
    {
        // Spaceキーが押された場合
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // Debug.Log("Space key was pressed."); // ログを出力.適宜コメントアウトを外してください.
            _client.Send("/sample", "Hello, I am Windows!"); // OSCでメッセージを送信
        }
        // コントローラのプライマリボタン(例: OculusのAボタン)が押された場合
        else if (InputDevices.GetDeviceAtXRNode(ControllerNode) // 指定されたXRデバイス(右手コントローラ)の状態を取得
                     .TryGetFeatureValue(CommonUsages.primaryButton, out bool primaryButtonValue) && primaryButtonValue)
        {
            _client.Send("/sample", "Hello, I am Quest3!"); // OSCでメッセージを送信
        }
    }
}

このスクリプトをシーン上に配置してみましょう.
適当なシーンでCreateEmptyしてOSCSenderというオブジェクトを作って先ほどのSendMessage.csをアタッチしましょう.
image.png

これでUnityから送信ができるようになりました.

テスト

本当に送信できているかを確認してみましょう.この節はただのテストなので飛ばしていただいても構いません.

送信処理

ループバックアドレスに対してOSC通信を飛ばすことで,自分自身でOSC通信を受け取ります.
先ほど配置したOSCSenderSendMessageIPAdress127.0.0.1にします.
image.png
これでゲームを再生してスペースボタンを押すと,自分自身に対してOSCメッセージが送信されます.

受信処理

Unityで受信処理を書いてもいいのですが,せっかく通信を行うので,別の言語で受け取るところを見たいところです.「他の言語で受信処理を試してみたい」という方はPythonでの受信テストを参照ください.一方で,「後で結局ESPで受け取るんだし,環境構築などの手間を省きたいのでUnityで受信したい」という方はUnityでの受信テストを参照ください.

Pythonでの受信テスト

次のスクリプトを動かしましょう.python-oscというライブラリをインストールしてください.

from pythonosc import dispatcher
from pythonosc import osc_server

# メッセージを処理する関数
def message_handler(address, *args):
    print(f"受信したアドレス: {address}")
    print(f"受信したデータ: {args}")

# ディスパッチャを作成してハンドラーを登録
dispatcher = dispatcher.Dispatcher()
dispatcher.map("/sample", message_handler)  # "/sample"アドレスに対応するハンドラーを登録

# サーバーを設定
ip = "127.0.0.1"  # 受信するIPアドレス
port = 8000       # 受信するポート番号

server = osc_server.BlockingOSCUDPServer((ip, port), dispatcher)

print(f"OSCサーバーが {ip}:{port} で開始されました...")
server.serve_forever()  # サーバーを起動

実行し,Unityエディタでゲームを再生してスペースボタンを押すと受信が確認できると思います.
image.png

Unityでの受信テスト

受信するのみなので,コンポーネントを使って実装します.
Assets > Create > ScriptableObjects > OSC Jack > Connectionで新たにOSC Connectionを作成してください.今回はAssets/直下に配置します.
image.png
Host127.0.0.1で,Port8000であることを確認してください.
image.png

次に,わかりやすさのために受信したメッセージを表示したいと思うので,テキストを配置してください.ReceivedTextという名前で中央に配置しておきます.
image.png

最後に,CreateEmptyしてOSCReceiverというオブジェクトを作ってEventReceiverというスクリプトをアタッチしてください.
Connectionを先ほどのOSC ConnectionOSC Adress\sampleData TypeStringにして,String EventReceivedTextTextMeshProUGUI>textを指定してください.

image.png

この状態でゲームを再生してスペースボタンを押すとテキストが変わると思います.
image.png
また,Window>OSC MonitorからもOSCの受信が確認できます.
image.png

ESP32

Unityの準備ができたのでESP32で受信処理を書いていきましょう.
次のコードを書き込んでください.

#include <WiFi.h>
#include <WiFiUdp.h>

// WiFi接続のためのSSIDとパスワード
const char* ssid = "SSID";
const char* password = "PASSWORD";

const int localPort = 8000;  // 受信するポート番号

const int ledPin = 2; // LEDのピン番号

WiFiUDP udp;

// OSCメッセージを表す構造体
struct OscMessage {
  bool isInt = false;
  bool isFloat = false;
  bool isString = false;
  int intValue = 0;
  float floatValue = 0.0f;
  char stringValue[256] = {0};
};

void setup() {
  Serial.begin(115200);

  // WiFi接続
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to WiFi");

  // UDPの初期化
  Serial.println(WiFi.localIP());
  udp.begin(localPort);
  Serial.printf("Listening on UDP port %d\n", localPort);

  // LEDを出力モードに
  pinMode(ledPin, OUTPUT);
  
  Serial.println("setup finished");
}

void loop() {
  // 受信バッファを設定
  char incomingPacket[255];
  
  // UDPでOSCメッセージを受信
  int packetSize = udp.parsePacket();
  if (packetSize) {
    int len = udp.read(incomingPacket, 255);
    if (len > 0) {
      incomingPacket[len] = '\0';  // パケットの末尾をNULLで終了
    }
    
    // OSCメッセージを解析
    OscMessage message = parseOscMessage(incomingPacket, len);
    
    // 受信したらLEDを点灯
    ledOn();
  }
}

// LEDを点灯させる
void ledOn() {
  digitalWrite(ledPin, HIGH);
  delay(250);
  digitalWrite(ledPin, LOW);
  delay(250);
} 

// 4バイト境界に揃えるためのパディング計算
int padSize(int size) {
  return (size + 3) & ~3;  // 4バイト境界に揃える
}

// 受信メッセージを解析して構造体を返す
OscMessage parseOscMessage(char* packet, int packetSize) {
  OscMessage message;

  // アドレス部分の抽出
  String address = String(packet);
  Serial.println("Received OSC address: " + address);

  // アドレス部分の終わりを見つける(4バイト境界に揃える)
  int addressEnd = padSize(address.length() + 1);

  // 型タグの位置を見つける
  int typeTagPos = addressEnd;

  // 型タグが存在し、"i"(整数)であることを確認
  if (packet[typeTagPos] == ',' && packet[typeTagPos + 1] == 'i') {
    // 値の位置を取得して整数として読み取る(型タグの4バイト後が値の開始位置)
    int valuePos = padSize(typeTagPos + 2);
    memcpy(&message.intValue, packet + valuePos, sizeof(message.intValue));
    message.intValue = ntohl(message.intValue);  // ネットワークバイトオーダーをホストバイトオーダーに変換
    message.isInt = true;
    Serial.println("Received int value: " + String(message.intValue));
  } 
  // 型タグが存在し、"f"(浮動小数点)であることを確認
  else if (packet[typeTagPos] == ',' && packet[typeTagPos + 1] == 'f') {
    int valuePos = padSize(typeTagPos + 2);
    uint32_t networkOrderValue;
    memcpy(&networkOrderValue, packet + valuePos, sizeof(networkOrderValue));
    networkOrderValue = ntohl(networkOrderValue);
    memcpy(&message.floatValue, &networkOrderValue, sizeof(message.floatValue));
    message.isFloat = true;
    Serial.println("Received float value: " + String(message.floatValue));
  }
  // 型タグが存在し、"s"(文字列)であることを確認
  else if (packet[typeTagPos] == ',' && packet[typeTagPos + 1] == 's') {
    int valuePos = padSize(typeTagPos + 2);
    strcpy(message.stringValue, (char*)(packet + valuePos));
    message.isString = true;
    Serial.println("Received string value: " + String(message.stringValue));
  } 
  else {
    Serial.println("Unexpected or missing type tag");
  }

  return message;
}

setup関数内でWi-Fiに接続し,loop関数内で受信の処理を行っています.Wi-Fiに接続し終わるとIPアドレスをシリアルモニターに表示するので,それをUnity側で設定してください.

parseOscMessage関数を自作しているためバグがある可能性があります.今回は使用していませんが,OSC受信用のライブラリもあるようなのでそれも検討してください.

動作確認

ESP32を実行して,シリアルモニタに出力されたIPアドレスをUnityのOSCsenderIPAddressに入力してください.
そうしたらUnityでゲームをスタートさせてスペースキーを押してください.それでシリアルモニタにメッセージが表示されたら成功です.
上手くいかない場合,同じWi-Fiにつながっているかや,IPアドレスが間違っていないかなどを確認してください.

Quest->ESP32

最後に,QuestにUnityをビルドしてみましょう.この記事を読んでいる人は大抵セットアップ済みだと思うので,VRに対応させる部分は今回は省略します.サンプルリポジトリや他記事を参考にしてください.
ビルドできたら,実行して右手のプライマリボタン(QuestのAボタン)を押してみてください.それでESP32のシリアルモニタにHello, I am Quest3!というメッセージが届けばOKです.
{5A71C0F4-6C4E-4C8E-B880-698888366918}.png

おわりに

今回の開発時,QuestからESP32に通信しようとしたときに,シリアル通信が上手くいかず,OSC通信に渋々乗り換えてOscCoreで動かなかったとき絶望しました.OscJackでうまくいって良かったです.先人様に大感謝...

参考記事

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?