Re: note

技術的な知見やポエムなど役に立たない情報を書き連ねる場所

【UniRx & Coroutine & async/await】Unityでシリアルポート読み取り

f:id:hik0leaf:20190831160452p:plain

Unityでシリアル通信(受信)する方法です。UniRx版、コルーチン版、async/await版を記載します。

1. 準備

スクリプトからシリアルポートを操作できるようにするために、Unityの Project Settings > Player > Other Settings の項目の Api Compatibility Level.NET 4.x に設定します。

f:id:hik0leaf:20190831161258p:plain

また、受信データ表示用にヒエラルキーにTextを追加してください。 Debug.Logで確認する場合は不要です。

2. スクリプト (UniRx版)

以下のスクリプトを適当なゲームオブジェクトにアタッチします。 ポート名は環境に合わせて変更してください。

using System.IO.Ports;
using UnityEngine;
using UniRx;
using UnityEngine.UI;
using System;

public class MainScript : MonoBehaviour
{
    private string portName = "COM4";
    private int baurate = 9600;
    private SerialPort serial;
    private bool isLoop = true;
    [SerializeField] Text countText = default;

    void Start() {
        serial = new SerialPort (portName, baurate, Parity.None, 8, StopBits.One);
        serial.Open();

        Observable.Create<string>(ReadData)
            .SubscribeOn(Scheduler.ThreadPool)
            .ObserveOn(Scheduler.MainThread)
            .Subscribe(data => countText.text = data)
            .AddTo(this);
    }

    private IDisposable ReadData(IObserver<string> observer) {
        while (isLoop) {
            observer.OnNext(serial.ReadLine());
        }
        return Disposable.Empty;
    }
    
    private void OnDestroy() {
        isLoop = false;
        serial.Close();
    }
}

内容としてはシリアルポートの読み取りをThreadPoolで、結果をTextに表示するために読み取ったデータをMainThreadで処理しています。 (GUIを操作する場合はMainThreadで行う必要があります。Debug.Logするだけであればスレッドの切り替えは不要です)

3. スクリプト (コルーチン版)

以下のスクリプトを適当なゲームオブジェクトにアタッチします。 ポート名は環境に合わせて変更してください。

using System.Collections;
using UnityEngine;
using System.IO.Ports;
using UnityEngine.UI;

public class MainScript : MonoBehaviour
{
    private string portName = "COM4";
    private int baurate = 9600;
    private SerialPort serial;
    private bool isLoop = true;
    [SerializeField] Text countText = default;

    void Start() {
        serial = new SerialPort (portName, baurate, Parity.None, 8, StopBits.One);
        serial.Open();

        StartCoroutine(ReadData());
    }
    
    private IEnumerator ReadData() {
        while (isLoop) {
            countText.text = this.serial.ReadLine();
            yield return null;
        }
    }

    private void OnDestroy() {
        isLoop = false;
        serial.Close();
    }
}

4. スクリプト (async/await版)

(2019.12.07 async/await版も追加しました)

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

public class MainScript : MonoBehaviour
{
    private string portName = "COM4";
    private int baurate = 9600;
    private SerialPort serial;
    private bool isLoop = true;
    [SerializeField] Text countText = default;

    async void Start() {
        serial = new SerialPort (portName, baurate, Parity.None, 8, StopBits.One);
        serial.Open();

        await ReadData();
    }

    private async Task ReadData() {
        await Task.Run(() => {
            while (isLoop) {
                countText.text = this.serial.ReadLine();
            }
        });
    }

    private void OnDestroy() {
        isLoop = false;
        serial.Close();
    }
}

5. 動作確認準備

今回、動作確認用にArduino Unoを使用します。Arduinoに以下のコードを書き込みます。 ボーレートはUnity側で設定した値と同じにします。(例:9600bps)

byte a = 0;

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println(a++);
  delay(500);
}

6. 結果

正常に動作していれば、0.5秒間隔で数字がカウントアップしていきます。 f:id:hik0leaf:20190831170432g:plain

7. Tips

今回、動作確認用にArduino Unoを使用しましたが、Mac OS環境でかつArduino Leonardo(ATmega32u4チップ)を使用する場合は、SerialPortクラスを以下のように設定することでUnityのフリーズを防ぐことができます。

serial = new SerialPort(portName, baurate, Parity.None, 8, StopBits.One);
serial.Open();
serial.DtrEnable = true;
serial.RtsEnable = true;
serial.DiscardInBuffer();
serial.ReadTimeout = 5000;// msec
  • DtrEnable / RtsEnableのどちらかをEnableにするだけでフリーズは回避できます。
  • DiscardInBuffer()することで接続前のバッファをクリアしています。
  • ReadTimeoutを設定しておくとSerialPortのトラブルでUnityのプログラム全体がフリーズするのを防ぐことができます。

8. 参考サイト

nn-hokuson.hatenablog.com

nn-hokuson.hatenablog.com