IVRC2024_Hataage

Arduino - Unity の Wi-Fi 通信

今回の制作では Arduino UNO R4 Wi-Fi を使用した.これにはWi-FiやBluetoothでの通信に対応した ESP32 がオンボードに実装されているため,これらを利用した無線通信が可能となっている.

公式チートシート

全体設計

今回は「Arduinoに接続されたサーボモータをUnityからの命令で制御する」ことが目的である.

そのためにまず,Unity と Arduino がそれぞれ何を担当するのかを明確にした.

※指定時間後にモータを停止する処理は Arduino 側で行うべきだと思うが,delay() 処理を組み込むと割り込み処理の待機が面倒になりそうだったので,Unity から停止命令を送ることにした.

Arduino の設計

以下のスケッチを利用した.

#include "WiFiS3.h"
#include "arduino_secrets.h"
#include "Servo.h"

// Wi-Fi接続情報 (arduino_secrets.h ファイルから読み込む)
char ssid[] = SECRET_SSID;
char pass[] = SECRET_PASS;
WiFiServer server(80); // HTTP通信用ポート

// サーボモーターのインスタンスを作成
Servo myServo;

// PWM信号を出力するピンを設定
const int servoPin = 13;   // 使用するピン番号(例: 13ピン)

// 基本のPWM値
const int basePWM = 1470;
const int minPWM = 700;
const int maxPWM = 2300;

// タイマーの設定
unsigned long previousMillis = 0;
float duration = 0;
bool motorRunning = false;

void setup() {
  // サーボモーターを初期化
  myServo.attach(servoPin);
  myServo.writeMicroseconds(basePWM);
  
  // シリアル通信を開始
  Serial.begin(9600);
  
  // Wi-Fiに接続
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print("Attempting to connect to SSID: ");
    Serial.println(ssid);
    WiFi.begin(ssid, pass);
    delay(500);
  }

  server.begin();
  printWifiStatus();
}

void loop() {
  WiFiClient client = server.available();

  if (client) {
    String message = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        message += c;
        if (c == '\n') {
          handleClientMessage(message, client);
          message = "";
        }
      }
    }
    client.stop();
  }

  // モーターが動作中の場合の割り込み処理
  if (motorRunning) {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= (unsigned long)(duration * 1000)) {
      // モーター停止
      myServo.writeMicroseconds(basePWM);
      motorRunning = false;
      Serial.println("Motor stopped.");
    }
  }
}

void handleClientMessage(String message, WiFiClient client) {
  if (message.startsWith("GET_BASE_PWM")) {
    client.println(basePWM);
    return;
  }

  int comma1 = message.indexOf(',');
  if (comma1 == -1) {
    Serial.println("Invalid data format");
    return;
  }

  duration = message.substring(0, comma1).toFloat();
  int pwmValue = message.substring(comma1 + 1).toInt();

  // デバッグ情報を追加
  Serial.print("Duration: ");
  Serial.println(duration);
  Serial.print("PWM Value: ");
  Serial.println(pwmValue);

  if (pwmValue >= minPWM && pwmValue <= maxPWM) {
    myServo.writeMicroseconds(pwmValue); // サーボモーターにPWM値を送信
    Serial.print("Received PWM: ");
    Serial.println(pwmValue); // 受信したPWM値をシリアルモニタに出力

    // 新しいデータを受信した場合の処理
    previousMillis = millis(); // タイマーをリセット
    motorRunning = true; // モーターを動作中に設定
    Serial.print("Motor running for: ");
    Serial.print(duration);
    Serial.println(" seconds.");
  } else {
    Serial.println("Invalid PWM value"); // 無効なPWM値の場合、エラーメッセージを表示
  }
}

void printWifiStatus() {
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);
  long rssi = WiFi.RSSI();
  Serial.print("Signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
  Serial.print("To control the Arduino, send messages to http://");
  Serial.println(ip);
}

Wi-Fiの接続情報 (SSIDとパスワード) として,スケッチと同じディレクトリ内に arduino_secrets.h ファイルを作成し,以下のように記述する.

//arduino_secrets.h header file
#define SECRET_SSID "ssid"
#define SECRET_PASS "password"

setup()

Wi-Fiに接続できるまで試行を繰り返す.接続できたら printWifiStatus() を利用して,自端末のIPアドレス,Wi-Fi接続強度などをシリアルモニタに表示する.

loop()

WiFiClient型の client としてWi-Fi通信を操作する.

受信可能な状態で受け取った内容を message バッファに書き込んでいき,末端文字 (改行文字 \n) が来たら handleClientMessage(message) で内容の解析を開始する.

handleClientMessage()

受信した内容を解析する.

Unity から送られるメッセージは direction,duration,speed のようにカンマ区切りの文字列で渡されるので,それぞれを改めてスケッチ内で変数に格納する.

ここで用いているサーボモータではPWM値は 700~2300 で定義されているため,その範囲内であればモータにPWM値を書き込み,回転させる.

Serial.print でシリアルモニタに書き出している内容はデバッグ目的なので必要ではない.

なお,GET_BASE_PWM というメッセージは Unity が基本PWM値 (回転の停止に利用する) を要求したときに用いられ,このスケッチで定義されている基本PWM値を送り返す.

Unity の設計

以下のスクリプトを使用した.

using System;
using System.Collections;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
using UnityEngine;

public class ArduinoWiFiController : MonoBehaviour 
{
    private TcpClient client;
    private StreamWriter writer;
    private StreamReader reader;
    public string arduinoIP = "192.168.1.100"; // ArduinoのIPアドレス
    public int arduinoPort = 80; // HTTPポート
    private float basePWM = 1470; // 初期値として設定し、Arduinoから取得した値で上書き
    private Coroutine currentRotationCoroutine;
    public float retryInterval = 5.0f; // 接続失敗時の再試行間隔(秒)
    public int maxRetryAttempts = 10; // 最大再試行回数

    async void Start()
    {
        await TryConnectAsync();
    }

    // 接続が完了するまで非同期処理でループする
    private async Task TryConnectAsync()
    {
        int retryCount = 0;
        bool connected = false;

        while (!connected && retryCount < maxRetryAttempts)
        {
            try
            {
                // Wi-Fi接続
                client = new TcpClient();
                await client.ConnectAsync(arduinoIP, arduinoPort);
                writer = new StreamWriter(client.GetStream());
                reader = new StreamReader(client.GetStream());
                StartListening();
                RequestBasePWM(); // ArduinoからbasePWMを取得
                connected = true; // 接続成功
                Debug.Log("Successed to connect to Arduino.");
            }
            catch (Exception e)
            {
                Debug.LogWarning("Failed to connect to Arduino: " + e.Message);
            }

            if (!connected)
            {
                retryCount++;
                Debug.Log($"Retrying... Attempt {retryCount} of {maxRetryAttempts}");
                await Task.Delay(TimeSpan.FromSeconds(retryInterval)); // 再試行まで待機
            }
        }

        if (!connected)
        {
            Debug.LogError("Failed to connect to Arduino after maximum retries.");
        }
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.R))
        {
            StartNewRotationCoroutine(-1.0f, 2.0f, 0.5f); // direction, duration, speed
        }
        else if (Input.GetKeyDown(KeyCode.L))
        {
            StartNewRotationCoroutine(1.0f, 3.0f, 1.0f);
        }
    }

    private float ConvertToPWM(float direction, float speed)
    {
        float deltaPWM = 800 * speed;
        return Mathf.Clamp(basePWM + (deltaPWM * direction), 700, 2300);
    }

    public void StartNewRotationCoroutine(float direction, float duration, float speed)
    {
        if (currentRotationCoroutine != null)
        {
            StopCoroutine(currentRotationCoroutine); // 現在のコルーチンを停止
        }
        currentRotationCoroutine = StartCoroutine(StartRotation(direction, duration, speed)); // 新しいコルーチンを開始
    }

    private IEnumerator StartRotation(float direction, float duration, float speed)
    {
        float pwmValue = ConvertToPWM(direction, speed);
        SendMessageToArduino($"{duration},{pwmValue}");
        Debug.Log($"Sending to Arduino: {duration},{pwmValue}");

        yield return new WaitForSeconds(duration);

        // モータ停止のために送信
        StopMotor();
    }

    private void SendMessageToArduino(string message)
    {
        if (writer != null)
        {
            writer.WriteLine(message);
            writer.Flush();
        }
    }

    private void StopMotor()
    {
        SendMessageToArduino($"0,{basePWM}");
        Debug.Log($"Sending to Arduino: 0,{basePWM}");
    }

    private async void StartListening()
    {
        while (true)
        {
            if (reader != null && client.Connected)
            {
                string message = await reader.ReadLineAsync();
                if (message != null)
                {
                    OnDataReceived(message);
                }
            }
        }
    }

    void OnDataReceived(string message)
    {
        if (message.StartsWith("GET_BASE_PWM"))
        {
            if (float.TryParse(message.Substring("GET_BASE_PWM".Length), out float value))
            {
                basePWM = value;
                Debug.Log($"Received basePWM from Arduino: {basePWM}");
            }
        }
        else
        {
            Debug.Log("Received from Arduino: " + message);
        }
    }

    private void RequestBasePWM()
    {
        SendMessageToArduino("GET_BASE_PWM");
    }

    private void OnApplicationQuit()
    {
        if (writer != null)
        {
            writer.Close();
        }
        if (reader != null)
        {
            reader.Close();
        }
        if (client != null)
        {
            client.Close();
        }
    }
}

Start()

Arduino へ接続するための非同期コルーチン TryConnectAsync() を呼んでいる.

TryConnectAsync()

Arduino へ接続するための非同期コルーチン.非同期処理にしたのは,接続の試行をループしている間にほかの処理が固まらないようにするため.

予め指定した試行間隔 (retryInterval) と試行回数 (maxRetryAttempts) に基づいて接続を試行する.

接続時,client としてTCPクライアントのインスタンスを作成し,arduinoIParduinoPort にHTTPで接続する.

Update()

デバッグ用.実際はこのスクリプト外から StartNewRotationCoroutine() を呼ぶことで操作している.

StartNewRotationCoroutine()

回転命令を後から上書きできるよう,コルーチンを管理するためのコルーチン.

現在走っているコルーチンがあれば停止してから StartRotation() のコルーチンを開始する.

StartRotation()

回転命令を処理するコルーチン.「回転方向 (direction),回転時間 (duration),回転スピード (speed)」の3つのパラメータを処理する.

回転方向は正回転方向なら 1,逆回転方向なら -1 を指定する.回転時間は秒単位,回転スピードは 0~1 の割合をいずれも浮動小数点数で指定する.

これらのパラメータからPWM値を取得する ConvertToPWM() 関数では,基本PWM値 + 方向 * (800 * スピード) という計算をしている.

処理した値を SendMessageToArduino() に投げることで Arduino へ送信する.

最後に,回転時間だけ待った後に StopMotor() 関数を呼ぶことでモータの静止を命令する.

SendMessageToArduino()

Arduino へメッセージを送信する.

Wi-Fi接続時に作成した StreamWriter(client.GetStream()) を利用して送信している.