はじめに
こんにちは!とある大学でUnityを使用した福祉関係の研究を行っている学部4年の大学生です。
今回は前回に引き続きシリアル通信周りの記事を書いていこうと思います。
前回の記事:https://qiita.com/Datwi/items/2b372c92d483b917d9e9
目的・背景
Arduinoに接続したロータリーエンコーダを2つ使って、Unity上の車いすを動かして遊べるようにすることが目的です。
ソースコード
Unityとarduinoのシリアル通信には4月の配属当初から取り組んでおり、Unity側のスクリプトの修正にはかなりの時間を費やしました💦
C#やUnityなんでほとんど触ったことがない中でのSerial通信、参考サイトのスクリプトをいくつか参考にさせていただき、自分用に同期処理を追加して別のスクリプトからもその値を参照できるようにしました。
※参考サイト
Unity側
実行環境
OS : Windows11 23H2
Unity Version : Unity5 2022.3.33f1
スクリプトを書く前に
Unityでシリアル通信を行うにはまずAPI Compatibilityの設定を変更する必要があります。
Edit → Project Setting → Player → Other Setting → Configuration → API Compatibility Level を .NET Frameworkに変更してください。
これでシリアル通信を行うクラスである System.IO.Ports.SerialPortが使用できるようになります。
スクリプト
Unityでシリアル通信を行うための設定が完了したので次はスクリプトを記述していきます。
今回のスクリプトは自動で使用したいデバイスを特定して接続し、シリアルから送られてくる信号を非同期で待ち続けてくれるスクリプトになっています。
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;
}
}
インポート
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
- 必要な名前空間をインポートしています。シリアルポート通信、スレッド、非同期処理、Unityの機能を使用します。
クラス定義
public class SerialHandler : MonoBehaviour
-
MonoBehaviour
を継承したSerialHandler
クラスを定義しています。
フィールド
[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
はポートが開かれているかどうかを示すフラグです。
シングルトンパターン
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;
}
}
- シングルトンパターンを実装し、クラスのインスタンスを一つだけに制限します。
Awake メソッド
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();// 非同期にデータを読み取る
}
}
- シーンのロード時に呼び出されるメソッドです。
- インスタンスが既に存在する場合は自身を破棄します。
- シリアルポートの接続を試み、デバイスを設定します。
SearchPort メソッド
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; // ポートが開かれたことを示す
}
- デバイスを検索するためにシリアルポートを開きます。
- シリアルポートから返答がない場合はタイムアウトして次のポートに再接続するようにしています。
OpenToRead メソッド
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; // ポートが開かれたことを示す
}
- デバイスから返答があった場合データを読み取るためにシリアルポートを再度開きます。
Close メソッド
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; // 読み取ったメッセージを返す
}
- デバイスへ送ったデバイス判定メッセージを非同期で読み取ります。
ReadAsync メソッド
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;
}
- シリアル通信で受信したデータを別のスクリプトからも使用できるようにします。
Arduino側
シリアル通信を受信するArduino側のスクリプトです。
Unityから送られくるAreyouDeviceに返答する処理が必要になってくるので、その処理を記述します。
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を返す
}
変数の定義
String SerialString = ""; // シリアル通信で受信する文字列
String Commands[3] = { "\0" }; // 分割された文字列を格納する配列
unsigned long searchStartTime = 0; // ポート検索の開始時間
unsigned long searchEndTime = 0; // ポート検索の終了時間
bool connectingStatus = false; // 接続状態
setup関数
void setup() {
Serial.begin(9600); // シリアル通信を9600ボーレートで開始
while (!connectingStatus) { // シリアルポートに接続されているかどうか確認
connectingStatus = checkConnected(); // 接続状態を確認
}
}
-
Serial.begin(9600)
でシリアル通信を9600ボーレートで開始します。 -
connectingStatus
がtrue
になるまでcheckConnected
関数を呼び出して接続状態を確認します。
loop関数
void loop() {
// 任意の処理
}
• メインループで、ここに任意の処理を追加できます。
checkConnected関数
bool checkConnected() {
if (Serial.available() > 0) { // シリアルバッファにデータがあるか確認
SerialString = readStringUntil('¥n'); // 改行が送られてくるまでシリアルを読み取る
if (SerialString == "areyouDevice") { // シリアルからデバイス確認文が送られてきたら
Serial.println("iamDevice"); // 返答を行う
return true; // trueを返す
}
}
return false; // デフォルトで false を返す
}
- シリアルバッファにデータがあるかを確認し、データがあれば改行まで読み取ります。
- 受信した文字列が
"areyouDevice"
であれば、"iamDevice"
と返答し、true
を返します。 - それ以外の場合は
false
を返します。
さいごに
いかがだったでしょうか。
上記のUnityとArduinoそれぞれのスクリプトを使用することでUnity上から自動で使用するデバイスを検索し、シリアル通信を行うことができるようになります。
かなり長時間ハマってしまったことなので、今後困る誰かの役に立てれば幸いです。
最後までご覧いただきありがとうございました!まだまだ勉強不足なため、不備等ありましたらご指摘いただけますと幸いです。