VIVE討論
瀏覽 : 819
分享

[教程] 彎曲傳感手套(二) - Bluetooth

新手會員2018-10-6 01:10

【前言】

    藍牙(英語:Bluetooth),是無線通訊技術的一種標準,主要功用是讓兩台裝置在短距離之間作資料交換,這個技術是在1994年由電信商愛利信(Ericsson)發展出的技術,發展至今已經到了第五代,我們這次要整合的HC-05模組是採用英國劍橋的CSR (Cambridge Silicon Radio) 公司的BC417143晶片,支援藍牙2.1+EDR規範,可以讓"彎曲傳感手套"達到無線傳輸的功用,至於如何做呢?請看以下教學。。。
【準備材料】
  • HC-05 藍芽模組         X1
  • 9V電池扣                X1
  • 9V電池                        X1
  • 3M超強力雙面膠        X1
  • 藍牙接收器                X1

【使用軟體】
  • Arduino 1.82
  • Unity 2018.2.4f1 (64-bit)

【程式下載】【電路圖】
  • "彎曲傳感手套"的硬體製作方式請參考上篇教學(彎曲傳感手套(一) - 硬體製作)。
  • 本篇教學線路圖與上篇教學不同之處只是新增了HC-05與9V電池扣的配線,其餘與上篇教學並無不同。
  • 依照線路圖將電線路插入麵包版上,並將所有零件使用3M超強力雙面膠粘貼到手套適當位置,如下圖:

【在電腦安裝藍牙接收器】
  • 以下是Windows 10安裝藍牙接收器的操作步驟。
  • 請將藍牙接收器插入電腦的USB插孔並安裝驅動程式。
  • 開啟"藍牙與其他裝置介面",如下圖:

  • 點擊"新增藍牙或其他裝置",選"藍牙",如下圖:

  • 將"彎曲傳感手套"插上9V電池,讓電腦找到"彎曲傳感手套"上的HC-05,並新增至電腦裝置內,如下圖:

  • 在將HC-05新增至電腦裝置內時一般都會需要輸入密碼,請先試著輸入1234或0000看看,如果都無法成功登入的話就請詢問您購買的HC-05硬體元件開發商。

  • 查看HC-05分配到的COM,請打開”裝置和印表機”然後找在HC-05裝置,在上面連點滑鼠左鍵兩下。

  • 選到”硬體”標籤就可以看到分配到的COM了,以小弟的電腦為主HC-05分配到的為COM7,請先記住分配到的COM,等等需要填入UNITY的程式碼內。

【Arduino】
  • 請將從GitHub下載的檔案解壓縮並使用Arduino軟體開啟位於[Tutorials_2\Arduino\CurvedSensingGlovesTutorial_2\CurvedSensingGlovesTutorial_2.ino]的檔案,程式碼說明如下:

#include <SoftwareSerial.h>

#define PIN_THUMB   A0    // 拇指.
#define PIN_POINTER A1    // 食指.
#define PIN_MIDDLE  A2    // 中指.
#define PIN_RING    A3    // 無名指.
#define PIN_LITTLE  A6    // 小指.

// HC-05.
SoftwareSerial BT(10, 11);

// 拇指數值.
int thumbValue = 0;
// 食指數值.
int pointerValue = 0;
// 中指數值.
int middleValue = 0;
// 無名指數值.
int ringValue = 0;
// 小指數值.
int littleValue = 0;

// 手套數值.
String InfinityStr = "";

// 時脈相關變數.
unsigned long timer = 0;
float timeTick = 0;

//-------------------------------------------------------
// 初始.
//-------------------------------------------------------
void setup() {   
  BT.begin(9600);  
}

//-------------------------------------------------------
// 主迴圈.
//-------------------------------------------------------
void loop() {  
  // 紀錄時脈.      
  timer = millis();
  
  //-------------------------------------------------------
  // 手指彎曲數值.  
  //-------------------------------------------------------
  // 拇指數值.
  thumbValue = analogRead(PIN_THUMB);
  // 食指數值.
  pointerValue = analogRead(PIN_POINTER);
  // 中指數值.
  middleValue = analogRead(PIN_MIDDLE);
  // 無名指數值.
  ringValue = analogRead(PIN_RING);
  // 小指數值.
  littleValue = analogRead(PIN_LITTLE);

  // 判斷時脈傳送.
  if(timeTick > 25){
    // 組合手指數值指令.
    InfinityStr = "inf|" + (String)thumbValue + "," + (String)pointerValue + "," + (String)middleValue + "," + (String)ringValue + "," + (String)littleValue + "|~";   
    // 將組合字串透過HC-05傳送給PC.
    BT.println(InfinityStr);
    // 初始時脈.
    timeTick = 0;  
  }
  timeTick += millis()-timer;  
}


  • 編譯無誤後請上傳到Arduino NANO內。

【Unity】
  • 請將從GitHub下載的檔案解壓縮並使用Unity開啟位於[Tutorials_2\Unity\CurvedSensingGlovesTutorial_2]目錄。
  • 開啟後請先設定使用.NET 2.0,設定流程如下:

    • 點選Edit → Project Settings → Player,如下圖:


    • 將Api Compatibility Level*改成.Net 2.0,如下圖:

  • 主程式說明如下(Assets/Scripts/Scripts/MainCamera.cs):

using UnityEngine;
using UnityEngine.UI;
using System;
using System.IO.Ports;
using LinkedList;
using System.Threading;

public class MainCamera : MonoBehaviour
{
    // 連線COM(請查看您電腦藍牙接收器使用的COM編號作修改).
    const string PORT_NAME = "COM7";

    public Image ImageHandR = null;
    public Sprite ImageHandOpenR = null;
    public Sprite ImageHandCloseR = null;

    public Text ThumbText = null;                   // 拇指數值.
    public Text PointerText = null;                 // 食指數值.
    public Text MiddleText = null;                  // 中指數值
    public Text RingText = null;                    // 無名指數值.
    public Text LittleText = null;                  // 小指數值.

    public Slider SliderThumb = null;               // 拇指拉霸.
    public Slider SliderPointer = null;             // 食指拉霸.
    public Slider SliderMiddle = null;              // 中指拉霸.
    public Slider SliderRing = null;                // 無名指拉霸.
    public Slider SliderLittle = null;              // 小指拉霸.

    public InputField ThumbInputMax = null;         // 拇指張手最大值.
    public InputField PointerInputMax = null;       // 食指張手最大值.
    public InputField MiddleInputMax = null;        // 中指張手最大值.
    public InputField RingInputMax = null;          // 無名指張手最大值.
    public InputField LittleInputMax = null;        // 小指張手最大值.

    public InputField ThumbInputNow = null;         // 拇指數值.
    public InputField PointerInputNow = null;       // 食指數值.
    public InputField MiddleInputNow = null;        // 中指數值.
    public InputField RingInputNow = null;          // 無名指數值.
    public InputField LittleInputNow = null;        // 小指張數值.

    public InputField ThumbInputMin = null;         // 拇指張手最小值.
    public InputField PointerInputMin = null;       // 食指張手最小值.
    public InputField MiddleInputMin = null;        // 中指張手最小值.
    public InputField RingInputMin = null;          // 無名指張手最小值.
    public InputField LittleInputMin = null;        // 小指張手最小值.

    //------------------------------------------------------------------------
    // 手套相關,
    //------------------------------------------------------------------------
    private float thumbMax = 0;                     // 拇指最大數值.   
    private float pointerMax = 0;                   // 食指最大數值.   
    private float middleMax = 0;                    // 中指最大數值   
    private float ringMax = 0;                      // 無名指最大數值.   
    private float littleMax = 0;                    // 小指最大數值.

    private float thumbMin = 0;                     // 拇指最小數值.   
    private float pointerMin = 0;                   // 食指最小數值.   
    private float middleMin = 0;                    // 中指最小數值   
    private float ringMin = 0;                      // 無名指最小數值.   
    private float littleMin = 0;                    // 小指最小數值.

    private float thumbTransform = 0;               // 拇指轉換數值.   
    private float pointerTransform = 0;             // 食指轉換數值.   
    private float middleTransform = 0;              // 中指轉換數值   
    private float ringTransform = 0;                // 無名指轉換數值.   
    private float littleTransform = 0;              // 小指轉換數值.

    private float thumbShock = 0;                   // 拇指防震動.
    private float pointerShock = 0;                 // 食指防震動.
    private float middleShock = 0;                  // 中指防震動.
    private float ringShock = 0;                    // 無名指防震動.
    private float littleShock = 0;                  // 小指防震動.

    // 手指數據平均值物件.
    private BalancedValue thumbBalancedValue;
    private BalancedValue pointerBalancedValue;
    private BalancedValue middleBalancedValue;
    private BalancedValue ringBalancedValue;
    private BalancedValue littleBalancedValue;

    // 不全指令備份.
    private string receivedDataTemp = "";
    // 指令串列.
    private SinglyLinkedList<string> commandLinkedList = null;

    // 多執行緒.
    private Thread thread;
    private bool isRun = true;

    // 藍牙.
    private SerialPort sp;

    //------------------------------------------------------------------------
    // 初始.
    //------------------------------------------------------------------------
    void Start()
    {
        // 建立指令串列.
        commandLinkedList = new SinglyLinkedList<string>();

        // 藍牙.
        try
        {
            sp = new SerialPort("\\\\.\\" + PORT_NAME, 9600);
            sp.Open();
            sp.ReadTimeout = 50;
        }
        catch (System.IO.IOException e) { }
        catch (System.InvalidOperationException e) { }

        // 建立多執行緒接收指令.
        thread = new Thread(getReceivedData);
        thread.IsBackground = true;
        thread.Start();

        // 建立手指數據平均值物件.
        thumbBalancedValue = new BalancedValue(8);
        pointerBalancedValue = new BalancedValue(8);
        middleBalancedValue = new BalancedValue(8);
        ringBalancedValue = new BalancedValue(8);
        littleBalancedValue = new BalancedValue(8);
    }

    //------------------------------------------------------------------------
    // 更新.
    //------------------------------------------------------------------------
    void Update()
    {
        // 未與藍芽連線.
        if (sp == null || !sp.IsOpen) { return; }

        try
        {
            // 最大值.
            thumbMax = float.Parse(ThumbInputMax.text);            // 拇指最大數值.
            pointerMax = float.Parse(PointerInputMax.text);        // 食指最大數值.
            middleMax = float.Parse(MiddleInputMax.text);          // 中指最大數值.
            ringMax = float.Parse(RingInputMax.text);              // 無名指最大數值.
            littleMax = float.Parse(LittleInputMax.text);          // 小指最大數值.

            // 現值.
            ThumbInputNow.text = thumbBalancedValue.Value.ToString();
            PointerInputNow.text = pointerBalancedValue.Value.ToString();
            MiddleInputNow.text = middleBalancedValue.Value.ToString();
            RingInputNow.text = ringBalancedValue.Value.ToString();
            LittleInputNow.text = littleBalancedValue.Value.ToString();

            // 最小值.
            thumbMin = float.Parse(ThumbInputMin.text);             // 拇指最小數值.
            pointerMin = float.Parse(PointerInputMin.text);         // 食指最小數值.   
            middleMin = float.Parse(MiddleInputMin.text);           // 中指最小.   
            ringMin = float.Parse(RingInputMin.text);               // 無名指最小.   
            littleMin = float.Parse(LittleInputMin.text);           // 小指最小.

            // 接收數值.            
            string value = getCommand();
            if (value != "")
            {
                // 解指令.
                string[] decoding = null;
                decoding = value.Split('|');

                // 判斷指令.
                switch (decoding[0])
                {
                    case "inf":      // 判斷手套指令.
                        // 解字串.
                        string[] infDecoding = null;
                        infDecoding = decoding[1].Split(',');

                        //----------------------------------------
                        // 拇指數.
                        //----------------------------------------
                        // 存入手套取的數值以計算平均值.
                        thumbBalancedValue.Add(float.Parse(infDecoding[0]));
                        // 避免手指抖動(將上一次與最新的平均值相減後取絕對值).
                        if (Math.Abs(thumbBalancedValue.Value - thumbShock) > 4)
                        {
                            // 取得平均值.
                            thumbShock = thumbBalancedValue.Value;
                            // 最大.
                            if (thumbShock > thumbMax)
                            {
                                thumbTransform = 100;
                            }
                            // 最小.
                            else if (thumbShock < thumbMin)
                            {
                                thumbTransform = 0;
                            }
                            // 中間.
                            else
                            {
                                // 換算顯示數值(0~10).
                                thumbTransform = Mathf.Round((100 / (thumbMax - thumbMin)) * (thumbShock - thumbMin));
                            }
                            // 顯示拉條位置.
                            SliderThumb.value = thumbTransform * 0.01f;
                            // 顯示數值.
                            ThumbText.text = thumbTransform.ToString();
                        }

                        //----------------------------------------
                        // 食指數值.
                        //----------------------------------------
                        // 存入手套取的數值以計算平均值.
                        pointerBalancedValue.Add(float.Parse(infDecoding[1]));
                        // 避免手指抖動(將上一次與最新的平均值相減後取絕對值).
                        if (Math.Abs(pointerBalancedValue.Value - pointerShock) > 4)
                        {
                            // 取得平均值.
                            pointerShock = pointerBalancedValue.Value;
                            // 最大.
                            if (pointerShock > pointerMax)
                            {
                                pointerTransform = 100;
                            }
                            // 最小.
                            else if (pointerShock < pointerMin)

                            {
                                pointerTransform = 0;
                            }
                            // 中間.
                            else
                            {
                                // 換算顯示數值(0~10).
                                pointerTransform = Mathf.Round((100 / (pointerMax - pointerMin)) * (pointerShock - pointerMin));
                            }
                            // 顯示拉條位置.
                            SliderPointer.value = pointerTransform * 0.01f;
                            // 顯示數值.
                            PointerText.text = pointerTransform.ToString();
                        }

                        //----------------------------------------
                        // 中指數值.
                        //----------------------------------------
                        // 存入手套取的數值以計算平均值.
                        middleBalancedValue.Add(float.Parse(infDecoding[2]));
                        // 避免手指抖動(將上一次與最新的平均值相減後取絕對值).
                        if (Math.Abs(middleBalancedValue.Value - middleShock) > 4)
                        {
                            // 取得平均值.
                            middleShock = middleBalancedValue.Value;
                            // 最大.
                            if (middleShock > middleMax)
                            {
                                middleTransform = 100;
                            }
                            // 最小.
                            else if (middleShock < middleMin)

                            {
                                middleTransform = 0;
                            }
                            // 中間.
                            else
                            {
                                // 換算顯示數值(0~10).
                                middleTransform = Mathf.Round((100 / (middleMax - middleMin)) * (middleShock - middleMin));
                            }
                            // 顯示拉條位置.
                            SliderMiddle.value = middleTransform * 0.01f;
                            // 顯示數值.
                            MiddleText.text = middleTransform.ToString();
                        }

                        //----------------------------------------
                        // 無名指數值.
                        //----------------------------------------
                        // 存入手套取的數值以計算平均值.
                        ringBalancedValue.Add(float.Parse(infDecoding[3]));
                        // 避免手指抖動(將上一次與最新的平均值相減後取絕對值).
                        if (Math.Abs(ringBalancedValue.Value - ringShock) > 4)
                        {
                            // 取得平均值.
                            ringShock = ringBalancedValue.Value;
                            // 最大.
                            if (ringShock > ringMax)
                            {
                                ringTransform = 100;
                            }
                            // 最小.
                            else if (ringShock < ringMin)

                            {
                                ringTransform = 0;
                            }
                            // 中間.
                            else
                            {
                                // 換算顯示數值(0~10).
                                ringTransform = Mathf.Round((100 / (ringMax - ringMin)) * (ringShock - ringMin));
                            }
                            // 顯示拉條位置.
                            SliderRing.value = ringTransform * 0.01f;
                            // 顯示數值.
                            RingText.text = ringTransform.ToString();
                        }

                        //----------------------------------------
                        // 小指數值.
                        //----------------------------------------
                        // 存入手套取的數值以計算平均值.
                        littleBalancedValue.Add(float.Parse(infDecoding[4]));
                        // 避免手指抖動(將上一次與最新的平均值相減後取絕對值).
                        if (Math.Abs(littleBalancedValue.Value - littleShock) > 4)
                        {
                            // 取得平均值.
                            littleShock = littleBalancedValue.Value;
                            // 最大.
                            if (littleShock > littleMax)
                            {
                                littleTransform = 100;
                            }
                            // 最小.
                            else if (littleShock < littleMin)

                            {
                                littleTransform = 0;
                            }
                            // 中間.
                            else
                            {
                                // 換算顯示數值(0~10).
                                littleTransform = Mathf.Round((100 / (littleMax - littleMin)) * (littleShock - littleMin));
                            }
                            // 顯示拉條位置.
                            SliderLittle.value = littleTransform * 0.01f;
                            LittleText.text = littleTransform.ToString();
                        }
                        break;
                }
            }
        }
        catch (TimeoutException)
        {}
    }

    //------------------------------------------------------------------------
    // 離開程式.
    //------------------------------------------------------------------------
    void OnApplicationQuit()
    {
        isRun = false;
        thread = null;
        if (sp != null && sp.IsOpen)
            sp.Close();
    }

    //------------------------------------------------------------------------
    // 取得HC-05傳送的指令.
    //------------------------------------------------------------------------
    public String getCommand()
    {
        string s = "";

        s = "";
        if (commandLinkedList.Count > 0)
        {
            s = commandLinkedList.First.Value;
            commandLinkedList.RemoveFirst();
            return s;
        }
        return "";
    }

    //------------------------------------------------------------------------
    // 接收指令.
    //------------------------------------------------------------------------
    private string receivedData = "";
    public void getReceivedData()
    {
        while (isRun)
        {
            try
            {
                string s = "";
                int len = 0;

                // 組合字串.
                receivedData += sp.ReadLine();
                // 分解字串.
                len = receivedData.Length;
                for (int i = 0; i < len; i++)
                {
                    // 組合指令.
                    if (receivedData != '~')
                    {
                        s += receivedData;
                    }
                    else
                    {
                        // 加入指令.
                        commandLinkedList.AddLast(s);
                        s = "";
                    }
                }
                receivedData = s;
            }
            catch (System.InvalidOperationException e) { }
            catch (System.TimeoutException e) { }
        }
    }
}

  • 請先將"彎曲傳感手套"插上9V電池,在執行Unity程式,然後注意看手套上的HC-05藍牙模組LED燈,會由快速閃動狀態變成大約1秒只閃動一次,這就表示手套上的HC-05藍牙模組已經與電腦連上線了,這時請看Unity視窗的手掌圖,標示(B.)處會發現這一格顯示框會內的數值會開始跳動,這表示Unity程式已經陸續收到HC-05藍牙模組傳送過來的數據了。

  • 接下來要開始設定"彎曲傳感手套",首先請戴上"彎曲傳感手套"然後看上圖(A.)處這邊請參考(B.)處內的數值輸入每根手指在張開狀態下的最大數據,然後看到(C.)處這邊一樣請參考(B.)處內數值輸入每根手指在握拳的狀態下的最小數據。
  • 五根手指都設定完畢後請反覆張開與握緊手指,會發現每根手指旁拉條上的小圓球會隨著手指的張開與握緊而上下移動,這邊因該可以很容易的理解到小圓球的上下移動就表示手指的彎曲與伸直位置,各位可以看著小圓球的位置在微調最大(A.)處與最小(C.)處數值,等圓球的移動位置可以趨近於手指的彎曲與伸直位置時也就代表"彎曲傳感手套"的設定完成了。
  • (D.)處是手指彎曲以(0~100)顯示的轉換數值。

【關於外接電源】
  • 為了測試方便建議大家可以購買5V/1A的行動電源,使用USB線供電給Arduino NANO,等測試完畢後再換插9V電池。

【後記】
下一篇教學我們將繼續在Unity內整合3D手掌模型與新增硬體六軸傳感器並對這次很亂的程式碼作優化(其實善用陣列可以讓程式碼少一半),敬請期待。


檢舉 回應

新手會員2018-10-7 20:39

哇塞~神人啊~這東西也能搞出來,太強~~~期待作品!

檢舉 回應

一般用戶

等級1

路過旅人

查無原作者2018-10-23 10:39

我一直以為動作訊號要從~~定位器的usb傳進去~~想不到是另外一個藍芽輸出,哈~我真的想太多了

如果要另外藍芽傳輸動作~那軟體有支援才可以用~而不是任何遊戲都可以用~~嗯~~~~

這就是為何omni 需要 針對他開發的遊戲才可玩的原因~~

思考思考~~~~~樓主真強~~~

檢舉 回應

分享