2023年5月12日 星期五

Unity 串列埠連接Arduino:測試(三)

 在上一篇文章Unity 串列埠連接Arduino:測試(二),已經將Arduino與Unity的專案連接成功,可以透過Arduino端的搖桿來控制Unity中的物件移動與旋轉。接著在本篇文章中我們兩個目標要達成:

(1)將UI移到一個新場景中,利用DontDestroyOnLoad的功能,讓Unity即使在場景變更中,串列埠的設定介面仍然存在、連線不會中斷。
(2)Unity端不再採用FixedUpdate,改成利用Thread來進行串列埠資料讀取。

首先在Unity中新增一個場景UI_0,場景中只放一個空物件,將UI(包括Canvas和EventSystem)移到這空物件下。並在Canvas下新增三個Button:

對應的函數如下:
using UnityEngine.SceneManagement;
    public void S1()
    {
        SceneManager.LoadScene("Scene1");
    }
    public void S2()
    {
        SceneManager.LoadScene("Scene2");
    }
    public void GameQuit()
    {
        Application.Quit();
    }
與第一篇方式相同,將對應的事件與執行的函數設定好。

接著將第一個場景改名為Scene1,並新增一個場景Scene2,在Scene2放入一個3D物件。
新增一個腳本S2motio.cs,附加在該物件上:
------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class S2motion : MonoBehaviour
{
    public float rspeed = 30f;
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        transform.position = new Vector3((-ComData.X + 512) / 10 * 0.1f, 0, (ComData.Y - 512) / 10 * 0.1f);
        if (ComData.R == 0)
        {
            transform.Rotate(Vector3.forward * Time.deltaTime * rspeed);
        }
    }
}
----------------------------------------------------------------------------------------

接著新增一個腳本DDO.cs, 附加在UI_0場景中的GameObject(底下有Canvas和EventSystem等)
-------------------------------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class DDO : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        DontDestroyOnLoad(gameObject);
        SceneManager.LoadScene("Scene1");
    }

    // Update is called once per frame
    void Update()
    {
    }
}
-------------------------------------------------------------------------------
該腳本執行時,會將物件移到DontDestroyOnLoad,並且載入Scene1。

接著將Serialsetup.cs修改如下:
--------------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO.Ports;
using System;
using UnityEngine.SceneManagement;
using System.Threading;

public class Serialsetup : MonoBehaviour
{
    public static SerialPort sp = new SerialPort();
    private string V = "1";
    private string[] words = null;
    public GameObject Canv, B_C, B_D;
    public Dropdown Dp1;
    private string PN = null;
    private string[] Com_name=null;
    private List<string> list;
    private bool b_conv = true;
    private Thread t;
    private Boolean receiving;
    public void GameQuit()
    {
        Application.Quit();
    }
    public void S1()
    {
        SceneManager.LoadScene("Scene1");
    }
    public void S2()
    {
        SceneManager.LoadScene("Scene2");
    }
    public void AddSerial()
    {        
        PN = Dp1.options[Dp1.value].text;
        if (!sp.IsOpen)
        {
            B_C.SetActive(true);
            B_D.SetActive(false);
        }
    }

    public void Connect()
    {
        try
        {
            sp.BaudRate = 115200;
            sp.Parity = Parity.None;
            sp.DataBits = 8;
            sp.StopBits = StopBits.One;
            sp.PortName = PN;
            sp.ReadTimeout = 2;  
            B_D.SetActive(true);
            B_C.SetActive(false);
            if (!sp.IsOpen)
            {
                sp.Open();
                receiving = true;
                t = new Thread(DoReceive);
                t.IsBackground = true;
                t.Start();
            }
        }
        catch (System.Exception)
        {
        }
    }
    private void DoReceive()
    {
        while (receiving)
        {
            try
            {
                if (sp.BytesToRead > 0)
                {
                    ReadandSplit();
                }
            }catch (System.Exception)
            {
                    Debug.Log("not good");
            }
            Thread.Sleep(5);
        }
    }
    public void Disconnect()
    {
        receiving = false;
        t.Abort();
        sp.Close();
        B_C.SetActive(true);
        B_D.SetActive(false);
    }
    void Start()
    {

        B_C.SetActive(false);
        B_D.SetActive(false);
        Com_name = SerialPort.GetPortNames();
        list= new List<string>(Com_name);
        Dp1.AddOptions(list);
    }
    // Update is called once per frame
    void Update()
    {
        if (sp.IsOpen)
        {
            if (Input.GetKeyDown(KeyCode.Escape))
            {
                b_conv = !b_conv;
                Canv.SetActive(b_conv);
            }
        }
        else
        {
            Canv.SetActive(true);
        }        
    }
    private void ReadandSplit()
    {
        for (int i=1; i<5; i++)
        {
            V = sp.ReadLine();
            while (sp.BytesToRead > 30)
            {
                V = sp.ReadLine();
                Debug.Log("R");
            }
            words =V.Split(',');
            if (V.StartsWith("B") && V.EndsWith("D"))
            {
                ComData.R = Int32.Parse(words[1]);
                ComData.X = Int32.Parse(words[2]);
                ComData.Y = Int32.Parse(words[3]);
                Debug.Log(i);
                break;
            }
        }
    }
    void OnDisable()
    {
        t.Abort();
        sp.Close();
    }
    void OnApplicationQuit()
    {
        t.Abort();
        sp.Close();
    }
}
----------------------------------------------------------------------------------
主要是Connect的修改,其新增一個Thread,並以DoReceive來讀取資料。
    public void Connect()
    {
        try
        {
            sp.BaudRate = 115200;
            sp.Parity = Parity.None;
            sp.DataBits = 8;
            sp.StopBits = StopBits.One;
            sp.PortName = PN;
            sp.ReadTimeout = 2;  
            B_D.SetActive(true);
            B_C.SetActive(false);
            if (!sp.IsOpen)
            {
                sp.Open();
                receiving = true;
                t = new Thread(DoReceive);
                t.IsBackground = true;
                t.Start();
            }
        }
        catch (System.Exception)
        {
        }
    }
    private void DoReceive()
    {
        while (receiving)
        {
            try
            {
                if (sp.BytesToRead > 0)
                {
                    ReadandSplit();
                }
            }catch (System.Exception)
            {
                    Debug.Log("not good");
            }
            Thread.Sleep(5);
        }
    }
-------------------------------------------------------------
在DoReceive()中,BytesToRead > 0即進行資料讀取(ReadLine)與拆解。
在ReadandSplit(),也做了一些修正:
-----------------------------------------------------
    private void ReadandSplit()
    {
        for (int i=1; i<5; i++)
        {
            V = sp.ReadLine();
            while (sp.BytesToRead > 15)
            {
                V = sp.ReadLine();
                Debug.Log("R");
            }
            words =V.Split(',');
            if (V.StartsWith("B") && V.EndsWith("D"))
            {
                ComData.R = Int32.Parse(words[1]);
                ComData.X = Int32.Parse(words[2]);
                ComData.Y = Int32.Parse(words[3]);
                Debug.Log(i);
                break;
            }
        }
    }
---------------------------------------------------
之前我們使用DiscardInBuffer,在丟棄資料時,可能會造成一行資料被丟棄一部分,產生錯誤。
另外,利用Split拆解字串,拆解後檢查第一個字串(words[0])和第五個字串(words[4]),這個方法有疑慮,因為當資料不正確時,words[4]可能不存在。
針對第一個問題,我們改成先讀取一次(ReadLine),接著如果BytesToRead大於某個數字(依資料量而定),則再繼續讀取,這樣可以達到丟棄資料的效果,也不會破壞資料整行的完整性。
對於第二個問題,則改用StartsWith("B") 和EndsWith("D")來檢查資料的正確性。

在Disconnect()也需加入
        receiving = false;
        t.Abort();

最後再加入
    void OnDisable()
    {
        t.Abort();
        sp.Close();
    }
    void OnApplicationQuit()
    {
        t.Abort();
        sp.Close();
    }
當應用程式關閉時,關閉這個Thread和串列埠連線。
測試時,Arduino端的dealy 1ms,Unity端的Thread.Sleep 5ms。
執行結果如下:

讀取資料成功50165行時,丟棄資料124029次,大約2:5。沒有讀第二次才成功的狀況產生,not good的狀況也只產生一次。場景切換也成功。

Build時產生一個警告訊息
System.Windows.Forms.dll assembly is referenced by user code, but is not supported on StandaloneWindows64 platform. Various failures might follow.
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
但執行Build出來的執行檔,可執行且測試成功。

上面這個警告訊息,經過追蹤,似乎在Api Compatibility Level設定為.Net 4.x之後就出現了!

沒有留言:

張貼留言