大幅なスキルアップ| GameStudyが世界中にある1万4,400社以上の企業から信頼されている理由を知ろう

【コピペで簡単!】Unityシリアルポート自動検索通信の実装方法

こんなクリエイターに見て欲しい

  • Unityでシリアル通信を自動化したい
  • Arduinoとの連携をスムーズに行いたい
  • デバイスの自動検索機能を実装したい
目次

完成したもの

まずは完成サンプルをご覧ください。

using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;

public class SerialHandler : MonoBehaviour
{
    // Deviceのリストを保持するフィールドを追加
    [SerializeField] private List<string> DeviceList = new List<string>();

    private static SerialHandler instance;
    public static SerialHandler Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<SerialHandler>();
                if (instance == null)
                {
                    Debug.LogError("MySerialHandler instance not found in the scene. Please ensure MySerialHandler is attached to a GameObject in the scene.");
                }
            }
            return instance;
        }
    }

    [SerializeField] string[] activePorts = null;
    public string portName = null;
    public int baudRate = 9600;

    private SerialPort serialPort;
    private Thread thread;

    private string message;
    public string Message;//ほかのスクリプトからアクセスするためのプロパティ
    private bool isPortOpen;//シリアルポートが既に開かれているかどうかを示すフラグ

async void Awake()
{

        Debug.Log("Start");

        // instanceがすでにあったら自分を消去する。
        if (instance && this != instance)
        {
            Debug.Log("Destroyed");
            Destroy(this.gameObject);
            return; // この行を追加して、以下のコードが実行されないようにする

        }

        instance = this;
        // Scene遷移で破棄されなようにする。      
        DontDestroyOnLoad(this);

        Thread.Sleep(2000);

        if (!isPortOpen)//ポート名がnullの場合、接続を試みる
        {
            portName = await SearchPort();
        }

        Debug.Log(portName + " is setting to Device");
        if (portName != null)
        {
            OpenToRead();// Deviceのポートを開く
            Thread.Sleep(100);// 100ms待つ
            Write("SetDevice");// 使用するDeviceに設定する
            await ReadAsync();// 非同期にデータを読み取る
           
        }
        else
        {
            Debug.Log("Device is not found but try to open COM3");        
            manualOpen();// ポート名がnullの場合、COM3を開く
            Thread.Sleep(100); // 100ms待つ
            Write("setDevice");// 使用するDeviceに設定する
            await ReadAsync();// 非同期にデータを読み取る
        }

}

    private async Task<string> SearchPort()
    {
        Debug.Log("SearchPort Start");
        activePorts = SerialPort.GetPortNames();

        foreach (string port in activePorts)
        {
            Debug.Log(port + " is active");

        }

        for (int i = 0; i < activePorts.Length; i++)
        {
            //Debug.Log(activePorts[i] + " is active");
            portName = activePorts[i]; // activePortの名前をportNameに代入する

            message = null; // メッセージをクリアする
            try
            {
                OpenToSearch();

                Thread.Sleep(2000);

                Write("areyouDevice");//デバイスにシリアル通信で文字列を送信する

                await ReadAreYouDevice();// 非同期にデータを読み取る

                Close();

                if (message != null && message.Contains("iamDevice"))
                {
                    Debug.Log(portName + " is Device!");

                    

                    DeviceList.Add(portName);// Deviceのリストに追加

                    Debug.Log(portName);

                    return portName;// Deviceのポート名を返す
                }
                else
                {
                    Debug.Log(portName + " is not Device");
                    Close();

                    Thread.Sleep(1000);
                }
            }
            catch (System.Exception e)
            {
                Debug.Log(e);
                Close();
            }

        }

        Debug.Log("Device is not found in active ports");
        return null;
    }

    private void OpenToSearch()
    {
        Debug.Log(portName + " will be opened to search");
        serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
        {
            ReadTimeout = 500//タイムアウトの設定
        };
        
        serialPort.Open();//ポートを開く
        isPortOpen = true; // ポートが開かれたことを示す
    }

    private void OpenToRead()
    {
        Debug.Log(portName + " will be opened to read");
        serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
        {
        };
        serialPort.Open();//ポートを開く
        isPortOpen = true; // ポートが開かれたことを示す
    }

    public void Close()
    {
        if (thread != null && thread.IsAlive)//もしスレッドがあったら
        {
            thread.Join();//リードの処理が終わるまで待つ
            thread = null; // スレッドをクリアする
        }
        if (serialPort != null && serialPort.IsOpen)//もしシリアルポートが開いていたら
        {
            serialPort.Close();//ポートを閉じる
            Debug.Log(portName + " is closed");
            serialPort.Dispose();//リソースの解放
            Debug.Log("Resource is cleared");
            serialPort = null; // シリアルポートをクリアする
            isPortOpen = false; // ポートが閉じられたことを示す
        }
    }

    private async Task<string> ReadAreYouDevice()//areyoudeviceのメッセージを読み取る
    {
        message = null;
        await Task.Run(() =>
        {
                if (serialPort != null && serialPort.IsOpen)
                {
                    try
                    {
                        message = serialPort.ReadLine();
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogWarning("1:" + e.Message);
                        Close();
                    }
                }
        });
        return message; // 読み取ったメッセージを返す
        
    }

    public async Task<string> ReadAsync()//非同期でデータを読み取る
    {
        message = null;
        await Task.Run(() =>
        {
            while (true)
            {
                if (serialPort != null && serialPort.IsOpen)
                {
                    try
                    {
                        message = serialPort.ReadLine();
                        //Debug.Log(message + " is received");
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogWarning("2:" + e.Message);
                        Close();
                        break;
                    }
                }
                else
                {
                    break;
                }
            }
        });
        Message = message;
        return message;
    }

    public void Write(string message)
    {
        try
        {
            serialPort.Write(message);
            serialPort.Write("\n");
            Debug.Log(message + " is sent");

        }
        catch (System.Exception e)
        {

            Debug.LogWarning("3:" + e.Message);
        }
    }

    private void OnApplicationQuit()
    {
        Write("exit");
        Close();
    }

    private void manualOpen()
    {
        if(portName == null)
        {
            portName = "COM3";
            OpenToRead();
        }
    }

    public string SendToAnotherScript()
    {
        return message;
    }

}

UnityとArduinoを使用したシリアル通信システムが完成します。システムは自動的に接続されたデバイスを検索し、適切なポートを特定して通信を確立します。Arduinoに接続したロータリーエンコーダを使ってUnity上の車いすを操作できるような、インタラクティブな福祉アプリケーションが実現可能になります。

Unityシリアルポート自動検索通信とは

Unityシリアルポート自動検索通信とは、利用可能なシリアルポートを自動的にスキャンし、特定のデバイスを識別して通信を確立するシステムです。

今回の記事では

  • API Compatibility Levelを設定してシリアル通信を有効化する仕組み
  • 非同期処理でデバイスを自動検索する方法
  • シングルトンパターンで通信データを管理する使い方

を解説していきます。

Unityシリアルポート自動検索通信の作成手順

手順はこちらです。

STEP
Unity側の設定

Edit → Project Setting → Player → Other Setting → Configuration → API Compatibility Level を .NET Frameworkに変更します。これによりSystem.IO.Ports.SerialPortクラスが使用可能になります。

using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
STEP
SerialHandlerスクリプトの作成

MonoBehaviourを継承したSerialHandlerクラスを作成します。

public class SerialHandler : MonoBehaviour

シリアルポート通信に必要なフィールドを定義します。

[SerializeField] private List<string> DeviceList = new List<string>();
private static SerialHandler instance;
[SerializeField] string[] activePorts = null;
public string portName = null;
public int baudRate = 9600;
private SerialPort serialPort;
private Thread thread;
private string message;
public string Message;
private bool isPortOpen;
  • DeviceList は接続されたデバイスのリストを保持します。
  • instance はシングルトンパターンのインスタンスを保持します。
  • activePorts は利用可能なシリアルポートのリストです。
  • portName は接続するポートの名前です。
  • baudRate は通信速度を設定します。
  • serialPort はシリアルポートのインスタンスです。
  • thread はデータ読み取り用のスレッドです。
  • message は読み取ったメッセージを保持します。
  • Message は他のスクリプトからアクセスするためのプロパティです。
  • isPortOpen はポートが開かれているかどうかを示すフラグです。

シングルトンパターンを実装し、DontDestroyOnLoadでシーン遷移時も破棄されないようにします。

public static SerialHandler Instance
{
    get
    {
        if (instance == null)
        {
            instance = FindObjectOfType<SerialHandler>();
            if (instance == null)
            {
                Debug.LogError("MySerialHandler instance not found in the scene. Please ensure MySerialHandler is attached to a GameObject in the scene.");
            }
        }
        return instance;
    }
}
STEP
デバイス自動検索機能の実装

SearchPortメソッドでSerialPort.GetPortNames()を使用して利用可能なポートを取得。各ポートに”areyouDevice”を送信し、”iamDevice”の返答があるポートを特定します。

private async Task<string> SearchPort()
{
        Debug.Log("SearchPort Start");
        activePorts = SerialPort.GetPortNames();

        foreach (string port in activePorts)
        {
            Debug.Log(port + " is active");

        }

        for (int i = 0; i < activePorts.Length; i++)
        {
            //Debug.Log(activePorts[i] + " is active");
            portName = activePorts[i]; // activePortの名前をportNameに代入する

            message = null; // メッセージをクリアする
            try
            {
                OpenToSearch();

                Thread.Sleep(2000);

                Write("areyouDevice");//デバイスにシリアル通信で文字列を送信する

                await ReadAreYouDevice();// 非同期にデータを読み取る

                Close();

                if (message != null && message.Contains("iamDevice"))
                {
                    Debug.Log(portName + " is Device!");

                    DeviceList.Add(portName);// Deviceのリストに追加

                    Debug.Log(portName);

                    return portName;// Deviceのポート名を返す
                }
                else
                {
                    Debug.Log(portName + " is not Device");
                    Close();

                    Thread.Sleep(1000);
                }
            }
            catch (System.Exception e)
            {
                Debug.Log(e);
                Close();
            }
        }

        Debug.Log("Device is not found in active ports");
        return null;
    }

OpenToSearch メソッドでデバイスを検索するためにシリアルポートを開きます。

private void OpenToSearch()
{
        Debug.Log(portName + " will be opened to search");
        serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
        {
            ReadTimeout = 500//タイムアウトの設定
        };
        
        serialPort.Open();//ポートを開く
        isPortOpen = true; // ポートが開かれたことを示す
    }

デバイスから返答があった場合データを読み取るためにシリアルポートを再度開きます。

private void OpenToRead()
{
        Debug.Log(portName + " will be opened to read");
        serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
        {
        };
        serialPort.Open();//ポートを開く
        isPortOpen = true; // ポートが開かれたことを示す
    }

OpenToSearch メソッドでシリアルポートとスレッドを閉じ、リソースを解放します。ReadAreYouDevice メソッド

public void Close()
{
        if (thread != null && thread.IsAlive)//もしスレッドがあったら
        {
            thread.Join();//リードの処理が終わるまで待つ
            thread = null; // スレッドをクリアする
        }
        if (serialPort != null && serialPort.IsOpen)//もしシリアルポートが開いていたら
        {
            serialPort.Close();//ポートを閉じる
            Debug.Log(portName + " is closed");
            serialPort.Dispose();//リソースの解放
            Debug.Log("Resource is cleared");
            serialPort = null; // シリアルポートをクリアする
            isPortOpen = false; // ポートが閉じられたことを示す
        }
    }

ReadAreYouDevice メソッドでデバイスへ送ったデバイス判定メッセージを非同期で読み取ります。

private async Task<string> ReadAreYouDevice()
{
        message = null;
        await Task.Run(() =>
        {
                if (serialPort != null && serialPort.IsOpen)
                {
                    try
                    {
                        message = serialPort.ReadLine();
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogWarning("1:" + e.Message);
                        Close();
                    }
                }
        });
        return message; // 読み取ったメッセージを返す       
    }
STEP
非同期通信の実装

ReadAsyncメソッドでTask.Runを使用して非同期でデータを読み取ります。whileループで継続的にシリアルポートからのデータを監視し、受信したメッセージをMessageプロパティに格納します。

public async Task<string> ReadAsync()
{
        message = null;
        await Task.Run(() =>
        {
            while (true)
            {
                if (serialPort != null && serialPort.IsOpen)
                {
                    try
                    {
                        message = serialPort.ReadLine();
                        //Debug.Log(message + " is received");
                    }
                    catch (System.Exception e)
                    {
                        Debug.LogWarning("2:" + e.Message);
                        Close();
                        break;
                    }
                }
                else
                {
                    break;
                }
            }
        });
        Message = message;
        return message;
    }

Writeメソッドでデバイスにメッセージを送信します。

public void Write(string message)
{
        try
        {
            serialPort.Write(message);
            serialPort.Write("\n");
            Debug.Log(message + " is sent");

        }
        catch (System.Exception e)
        {

            Debug.LogWarning("3:" + e.Message);
        }
    }

OnApplicationQuit メソッドでアプリケーション終了時にシリアルポートを閉じます。

private void OnApplicationQuit()
    private void OnApplicationQuit()
    {
        Write("exit");
        Close();
    }

manualOpen メソッドでポート名が設定されていない場合に指定したポートを開きます。

private void manualOpen()
   {
        if(portName == null)
        {
            portName = "COM~";//任意のポート名
            OpenToRead();
        }
    }

SendToAnotherScript メソッドでシリアル通信で受信したデータを別のスクリプトからも使用できるようにします。

public string SendToAnotherScript()
    {
        return message;
    }

こうしてwhileループで継続的にシリアルポートからのデータを監視し、受信したメッセージをMessageプロパティに格納します。

STEP
Arduino側の実装

checkConnected関数でSerial.available()を確認し、”areyouDevice”を受信したら”iamDevice”を返答します。

bool checkConnected() {
    if (Serial.available() > 0) { // シリアルバッファにデータがあるか確認
        SerialString = readStringUntil('¥n'); // 改行が送られてくるまでシリアルを読み取る
        if (SerialString == "areyouDevice") { // シリアルからデバイス確認文が送られてきたら
            Serial.println("iamDevice"); // 返答を行う
            return true; // trueを返す
        }
    }
    return false; // デフォルトで false を返す
}

setup関数内でconnectingStatusがtrueになるまで接続を待機します。

void setup() {
    Serial.begin(9600); // シリアル通信を9600ボーレートで開始
    
    while (!connectingStatus) {  // シリアルポートに接続されているかどうか確認
        connectingStatus = checkConnected(); // 接続状態を確認
    }
}

loop関数の メインループで、ここに任意の処理を追加できます。

void loop() {
    // 任意の処理
}

シリアル通信を受信するArduino側のスクリプトの完成

String SerialString = "";       // シリアル通信で受信する文字列
String Commands[3] = { "\0" };  // 分割された文字列を格納する配列
unsigned long searchStartTime = 0;//ポート検索の
unsigned long searchEndTime = 0;

bool connectingStatus = false;//接続状態

void setup() {
	Serial.begin(9600);
	
	  while (!connectingStatus) {  // シリアルポートに接続されているかどうか確認
    connectingStatus = checkConnected();
  }
}

void loop() {

	//任意の処理

}

bool checkConnected() {
	if (Serial.available() > 0) { // シリアルバッファにデータがあるか確認
		SerialString = readStringUntil('¥n');//改行が送られてくるまでシリアルを読み取る
		if (SerialString == "areyouDevice")//シリアルからデバイス確認文が送られてきたら
		{
			Serial.println("iamDevice");//返答を行う
			return true;//trueを返す
		}
	}
	return false;// デフォルトでfalseを返す
}

まとめ

今回はUnityでシリアルポートを自動検索して通信を行う方法を解説しました。とても長い記事になりましたが非同期処理とデバイス識別プロトコルを組み合わせることで、ユーザーが手動でポートを設定する必要がない便利なシステムが構築できましたね。

習得したスキル

  • API Compatibility Levelを設定してシリアル通信を有効化する方法
  • 非同期処理でシリアルポートからデータを読み取る方法
  • シングルトンパターンで通信インスタンスを管理する方法

次に読むべき記事

  • Unityでのスレッド処理の最適化について
  • Arduinoセンサーデータの活用方法について
  • 福祉アプリケーションの開発事例について

以上です。

投稿者プロフィール

黒田 隆史
黒田 隆史
(株)ゲームガム 代表取締役社長 / Roblox開発スタジオ GameGum 責任者 / 現役大学院生 金属×ロケットの研究従事 / ゲーム配信Mirrativ 配信者 / 国内最大級メタバースクリエーター向けテックブログを運営。
「メタバースクリエーターと1兆円の経済圏を作る」ために活動中。Xのアカウントのフォロワーは2000人弱(2025年4月現在)
シェアすると、喜ぶよ!
  • URLをコピーしました!

ぜひ、応援の言葉をお願いします!

コメントする

目次