본문 바로가기

유니티

[BLE] Bluetooth LE (2) - 유니티에서 에셋을 활용하여 BLE 구현

[에셋]

[ Bluetooth LE for iOS, tvOS and Android ]

=>유니티에서 BLE의 구현을 위해 Shatalmic.llc 의 에셋을 활용하고자 한다 .

해당 에셋은 GATT Bluetooth API에 대한 접근을 제공한다 . (Android는 Peripheral은 제공 안됨)

에셋

[개요]

[ Android Java Class Library ]

=>Plugins/Android 폴더에는 안드로이드 자바 코드 라이브러리가 AndroidManefest.xml을 따라 플러그인을 위한 적절한

허용과 세팅이 설정되어 있다. AndroidManefest.xml에는 Android Pemission이 포함되어 있다.

BLE를 지원하지 않는 장치에 대해 앱이  표시되는지 여부를 제어하는 세팅 역시 존재한다.

<uses- feature android:name="android.hardware.bluetooth_le" android:required="false"/>

false라면 해당 앱은 호환되지 않더라도 모든 기기에 설치가 가능하며 플레이스토어에 보일 것이다.

해당 플러그인의 설정중 기기는 BLE 적합성을 체크하는데, 기기가 지원하지 않는다면 error와 error 콜백을 받을 것이다 .

 

true라면 타겟 기기가 BLE를 지원하지 않는 한 구글 플레이스토어에 보이지 않을것이다.

 

[ Note ]

해당 에셋은 안드로이드에서 제공하는 GATT Bluetooth API 에 접근하게 해준다.포함된 C# Script의 BluetoothHardwareInterface class는 static Method를 가지고 있다.

 

해당 에셋의 작업들은 asynchronous(비동기적이다) .

이는 그 전 오퍼레이션의 콜백을 받기 전까지는새로운 오퍼레이션을 실행 할 수 없다는 말이다.

 

또한 , 에셋에는 UUID를 안드로이드는 대소문자 구분이 있기에  소문자로 바꾸어주는 로직 / FullUUID로 바꾸어주는 로직/

2개의 UUID가 같은지 비교하는 메소드가 포함 되어 있다.

 

[ Sample Guide]

[Sample Guide]

해당 에셋 내부에 있는 샘플 가이드를 통해 에셋 사용법을 이해하고자 한다 .

Setup Guide : Scanner Test

Start

BluetoothLEHardwareInterface.Log ("Start");

=>안드로이드 콘솔에 시작함을 로그로 띄운다 

 

BluetoothLEHardwareInterface.Initialize

=>블루투스 시스템을 이니셜라이즈 진행. 받는 인자들은 다음을 뜻한다.

BluetoothLEHardwareInterface.Initialize(true//1번 :As Central, false//2번 :As Peripal, () =>
        {

            SetState(States.Scan, 0.1f);//3번 :성공 콜백

        }, (error) =>
        {

            StatusMessage = "Error during initialize: " + error;//4번 :실패 콜백
        });

 

해당 블루투스 시스템은 ConnectionMode(1:1통신)이다. 인자를 살펴 보자면

 

1번 인자

as Central (Master) : Central로 설정한다 .연결 주기를 스캔, 적절한 디바이스 연결 요청. 연결후 timing(channel hopping 규칙) 설정 , 주기적인 데이터 교환 주도한다.

 

2번 인자

asPeripheral (Slave) : Peripheral 로 설정한다 .주기적으로 연결 advertising 신호를 보내고 , 연결요청이 오면 수락

=>처음,두번째 인자는 내가 어떤 역할을 수행하는지 정한다.둘다 true 가 될 수 있다.

 

3번 인자

성공시 : _Timeout에 _StartScanDelay를 넣어준다(0.5f)

 

4번 인자

실패시:로그로 에러메시지를 띄운다.만약 에러가 bluetooth LE Not Enabled로 나온다면

안드로이드의 블루투스를 켜준다.

 

BLE 연결역할과 과정에 관하여

 

Update

=>해당 메서드들은 모두 asynchronous(비동기적이다) .그 전 오퍼레이션의 콜백을 받기 전까지는새로운 오퍼레이션을 실행 할 수 없기에 timeout을 통해 기다린 후 실행하게 된다 .

_timeout이 0보다 크다면 timeout에서 현재시간을 빼준다.(초기에는 0.5초를 기다릴것)

_startScan(기본적으로  true) 이 true상태라면 false로 변경후 _timeout에 _startScanTimeout (10f)를 넣어준다.

그후BluetoothLEHardwareInterface.Initialize()를 실행

 

 

public static void ScanForPeripheralsWithServices

=>디바이스를 스캔모드로 하여 serviceUUIDs배열의 서비스가 있는 peripherals를 찾는다. 

만약,null이라면 모든 기기를 서칭한다. 찾았다면 액션 콜백이 실행된다.(해당 id와 이름으로)

 

=>actionAdvertisingInfo는 하위호환성을 위해 비워져있다.해당 메서드는 advertising data를 받을때마다 호출될것이다

Ble Advertice Packet

 

=>rssiOnly 인자는 manufactrie specific data가 없는 스캔된 기기가 RSSI 값을 보냄을  허용한다.하위 호환성을 위해 false.

 manufacturer specific data

RSSI

 

=>clearPeripheralList는 ios에서만 쓰인다.크로스 플렛폼을 위해 api에 존재.

 

해당 메서드에서 UUID => null,성공 액션=>null,

actionAdvertisingInfo는 스캔주소를 로그에 띄우고.scanedItem에 키가 있다면 RSSI (수신신호강도)를 입력,이미 리스트에 있음을 시사

 

없다면 해당 프리팹을 생성후 textAddressValue/name/RSSI값을 입력후 리스트에 넣어준다

 

=>기기의 ID와 주소

 

=>_timeout이 0보다 크다면 timeout에서 현재시간을 빼준다.(초기에는 0.5초를 기다릴것) 

 

인자목록

=>serviceUUID () : 서비스의 구분자 / 

BLE(Bluetooth Low Energy)의 이해와 UUID목록

 

using UnityEngine;
using UnityEngine.UI;

public class StartingExample : MonoBehaviour
{
	//기기의 이름 . Scan 단계에서 해당 이름을 포함하는 기기를 찾아 연결한다
    public string DeviceName = "ledbtn";
    //서비스 (Characteristic)의 집합의 UUID
    public string ServiceUUID = "A9E90000-194C-4523-A473-5FDF36AA4D20";
    //(Characteristic)LED 신호의 UUID
    public string LedUUID = "A9E90001-194C-4523-A473-5FDF36AA4D20";
     //(Characteristic)버튼 신호의 UUID
    public string ButtonUUID = "A9E90002-194C-4523-A473-5FDF36AA4D20";
	
    //현재 연결의 진행 상태
    enum States
    {
        None,
        Scan,
        ScanRSSI,
        ReadRSSI,
        Connect,
        RequestMTU,
        Subscribe,
        Unsubscribe,
        Disconnect,
    }

    private bool _connected = false;
    private float _timeout = 0f;
    private States _state = States.None;
    private string _deviceAddress;
    private bool _foundButtonUUID = false;
    private bool _foundLedUUID = false;
    private bool _rssiOnly = false;
    private int _rssi = 0;

    public Text StatusText;
    public Text ButtonPositionText;

    private string StatusMessage
    {
        set
        {
            BluetoothLEHardwareInterface.Log(value);
            StatusText.text = value;
        }
    }

    void Reset()
    {
        _connected = false;
        _timeout = 0f;
        _state = States.None;
        _deviceAddress = null;
        _foundButtonUUID = false;
        _foundLedUUID = false;
        _rssi = 0;
    }

    void SetState(States newState, float timeout)
    {
        _state = newState;
        _timeout = timeout;
    }

    void StartProcess()
    {
        Reset();
        //BLE에서의 역할을 초기설정한다 . 성공 실패 여부에 따른 콜백을 호출한다 .
        BluetoothLEHardwareInterface.Initialize(true, false, () =>
        {

            SetState(States.Scan, 0.1f);

        }, (error) =>
        {

            StatusMessage = "Error during initialize: " + error;
        });
    }

    // Use this for initialization
    void Start()
    {
        StartProcess();
    }

    private void ProcessButton(byte[] bytes)
    {
        if (bytes[0] == 0x00)
            ButtonPositionText.text = "Not Pushed";
        else
            ButtonPositionText.text = "Pushed";
    }

    // Update is called once per frame
    void Update()
    {
    	//_timeout을 통해 비동기적인 작업을 처리한다 .
        if (_timeout > 0f)
        {
            _timeout -= Time.deltaTime;
            if (_timeout <= 0f)
            {
                _timeout = 0f;

                switch (_state)
                {
                	//1. 초기설정
                    case States.None:
                        break;
					
                    //2. 이름을 찾아 스캔과 연결 시도
                    case States.Scan:
                        StatusMessage = "Scanning for " + DeviceName;

                        BluetoothLEHardwareInterface.ScanForPeripheralsWithServices(null, (address, name) =>
                        {
                            // if your device does not advertise the rssi and manufacturer specific data
                            // then you must use this callback because the next callback only gets called
                            // if you have manufacturer specific data

                            if (!_rssiOnly)
                            {
                                if (name.Contains(DeviceName))
                                {
                                    StatusMessage = "Found " + name;

                                    // found a device with the name we want
                                    // this example does not deal with finding more than one
                                    _deviceAddress = address;
                                    SetState(States.Connect, 0.5f);
                                }
                            }

                        }, (address, name, rssi, bytes) =>
                        {

                            // use this one if the device responses with manufacturer specific data and the rssi

                            if (name.Contains(DeviceName))
                            {
                                StatusMessage = "Found " + name;

                                if (_rssiOnly)
                                {
                                    _rssi = rssi;
                                }
                                else
                                {
                                    // found a device with the name we want
                                    // this example does not deal with finding more than one
                                    _deviceAddress = address;
                                    SetState(States.Connect, 0.5f);
                                }
                            }

                        }, _rssiOnly); // this last setting allows RFduino to send RSSI without having manufacturer data
						
                        //_rssi만 스캔
                        if (_rssiOnly)
                            SetState(States.ScanRSSI, 0.5f);
                        break;

                    case States.ScanRSSI:
                        break;

                    case States.ReadRSSI:
                        StatusMessage = $"Call Read RSSI";
                        BluetoothLEHardwareInterface.ReadRSSI(_deviceAddress, (address, rssi) =>
                        {
                            StatusMessage = $"Read RSSI: {rssi}";
                        });

                        SetState(States.ReadRSSI, 2f);
                        break;
					
                    //RSSI Only가 아닐때 연결을 진행한다 .
                    case States.Connect:
                        StatusMessage = "Connecting...";

                        // set these flags
                        _foundButtonUUID = false;
                        _foundLedUUID = false;

                        // note that the first parameter is the address, not the name. I have not fixed this because
                        // of backwards compatiblity.
                        // also note that I am note using the first 2 callbacks. If you are not looking for specific characteristics you can use one of
                        // the first 2, but keep in mind that the device will enumerate everything and so you will want to have a timeout
                        // large enough that it will be finished enumerating before you try to subscribe or do any other operations.
                        
                        //스캔을 멈추고 Peripheral 기기에 연겷한다 .미리 정의한 서비스 UUID와 기기의 서비스 UUID가 맞다면 
                        BluetoothLEHardwareInterface.ConnectToPeripheral(_deviceAddress, null, null, (address, serviceUUID, characteristicUUID) =>
                        {
                            StatusMessage = "Connected...";

                            BluetoothLEHardwareInterface.StopScan();

                            if (IsEqual(serviceUUID, ServiceUUID))
                            {
                                StatusMessage = "Found Service UUID";

                                _foundButtonUUID = _foundButtonUUID || IsEqual(characteristicUUID, ButtonUUID);
                                _foundLedUUID = _foundLedUUID || IsEqual(characteristicUUID, LedUUID);

                                // if we have found both characteristics that we are waiting for
                                // set the state. make sure there is enough timeout that if the
                                // device is still enumerating other characteristics it finishes
                                // before we try to subscribe
                                if (_foundButtonUUID && _foundLedUUID)
                                {
                                    _connected = true;
                                    SetState(States.RequestMTU, 2f);
                                }
                            }
                        });
                        break;
					
                    //MTU 설정을 요청한다 .
                    case States.RequestMTU:
                        StatusMessage = "Requesting MTU";

                        BluetoothLEHardwareInterface.RequestMtu(_deviceAddress, 185, (address, newMTU) =>
                        {
                            StatusMessage = "MTU set to " + newMTU.ToString();

                            SetState(States.Subscribe, 0.1f);
                        });
                        break;
					
                    //원하는 Characteristic을 찾아 연결
                    case States.Subscribe:
                        StatusMessage = "Subscribing to characteristics...";

                        BluetoothLEHardwareInterface.SubscribeCharacteristicWithDeviceAddress(_deviceAddress, ServiceUUID, ButtonUUID, (notifyAddress, notifyCharacteristic) =>
                        {
                            StatusMessage = "Waiting for user action (1)...";
                            _state = States.None;

                            // read the initial state of the button
                            BluetoothLEHardwareInterface.ReadCharacteristic(_deviceAddress, ServiceUUID, ButtonUUID, (characteristic, bytes) =>
                            {
                                ProcessButton(bytes);
                            });

                            SetState(States.ReadRSSI, 1f);

                        }, (address, characteristicUUID, bytes) =>
                        {
                            if (_state != States.None)
                            {
                                // some devices do not properly send the notification state change which calls
                                // the lambda just above this one so in those cases we don't have a great way to
                                // set the state other than waiting until we actually got some data back.
                                // The esp32 sends the notification above, but if yuor device doesn't you would have
                                // to send data like pressing the button on the esp32 as the sketch for this demo
                                // would then send data to trigger this.
                                StatusMessage = "Waiting for user action (2)...";

                                SetState(States.ReadRSSI, 1f);
                            }

                            // we received some data from the device
                            ProcessButton(bytes);
                        });
                        break;

                    case States.Unsubscribe:
                        BluetoothLEHardwareInterface.UnSubscribeCharacteristic(_deviceAddress, ServiceUUID, ButtonUUID, null);
                        SetState(States.Disconnect, 4f);
                        break;

                    case States.Disconnect:
                        StatusMessage = "Commanded disconnect.";

                        if (_connected)
                        {
                            BluetoothLEHardwareInterface.DisconnectPeripheral(_deviceAddress, (address) =>
                            {
                                StatusMessage = "Device disconnected";
                                BluetoothLEHardwareInterface.DeInitialize(() =>
                                {
                                    _connected = false;
                                    _state = States.None;
                                });
                            });
                        }
                        else
                        {
                            BluetoothLEHardwareInterface.DeInitialize(() =>
                            {
                                _state = States.None;
                            });
                        }
                        break;
                }
            }
        }
    }

    private bool ledON = false;
    public void OnLED()
    {
        ledON = !ledON;
        if (ledON)
        {
            SendByte((byte)0x01);
        }
        else
        {
            SendByte((byte)0x00);
        }
    }

    string FullUUID(string uuid)
    {
        string fullUUID = uuid;
        if (fullUUID.Length == 4)
            fullUUID = "0000" + uuid + "-0000-1000-8000-00805f9b34fb";

        return fullUUID;
    }

    bool IsEqual(string uuid1, string uuid2)
    {
        if (uuid1.Length == 4)
            uuid1 = FullUUID(uuid1);
        if (uuid2.Length == 4)
            uuid2 = FullUUID(uuid2);

        return (uuid1.ToUpper().Equals(uuid2.ToUpper()));
    }

    void SendByte(byte value)
    {
        byte[] data = { value };
        BluetoothLEHardwareInterface.WriteCharacteristic(_deviceAddress, ServiceUUID, LedUUID, data, data.Length, true, (characteristicUUID) =>
        {

            BluetoothLEHardwareInterface.Log("Write Succeeded");
        });
    }
}