簡體   English   中英

使用 Unity 的 BLE 設備

[英]BLE device with Unity

我正在嘗試在 Unity3D 中創建一個游戲,該游戲連接到支持心率服務並收集 HR 數據的藍牙低功耗設備。 我創建了一個 WPF 庫,該庫已在控制台應用程序上進行了測試,以連接到 BLE 設備並讀取完美運行的數據。 但是,我不知道如何在 Unity 中使用它。

我將庫轉移到 Unity 並且不得不為編輯器禁用它,因為代碼使用了編輯器不支持的 Windows 命名空間。 我現在的問題是如何在 Unity 中調試代碼以檢查運行游戲時庫代碼是否正常工作。 我嘗試在#if NETFX_CORE、#if ENABLE_WINMD_SUPPORT、#if WINDOWS_UWP 等中包裝用於調用庫命名空間和庫中函數的代碼,但從未獲得我編寫的任何調試日志。

有沒有可能的解決方案? 任何幫助將不勝感激,謝謝!

這是藍牙LE連接庫的代碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Storage.Streams;

namespace BLEHR
{
/// <summary>
/// Wraps and makes use if the <see cref="BluetoothLeAdvertisementWatcher"/>
/// for easier consumption
/// 
/// </summary>
public class BLEAdvertisementWatcher
{
    #region Private Members
    /// <summary>
    /// The underlying bluetooth watcher class
    /// </summary>
    private readonly BluetoothLEAdvertisementWatcher mWatcher;

    /// <summary>
    /// a list of discovered devices
    /// </summary>
    private readonly Dictionary<string, BLEDevice> mDiscoveredDevices = new Dictionary<string, BLEDevice>();

    /// <summary>
    /// The details about Gatt services
    /// </summary>
    private readonly GattServiceIDs mGattServiceIds;

    /// <summary>
    /// a thread lock object for this class
    /// </summary>
    private readonly object mThreadLock = new object();
    #endregion

    #region Public Properties

    /// <summary>
    /// indicates is this watcher is listening for advertisements
    /// </summary>
    public bool Listening => mWatcher.Status == BluetoothLEAdvertisementWatcherStatus.Started;

    /// <summary>
    /// a list of discovered devices
    /// </summary>
    public IReadOnlyCollection<BLEDevice> DiscoveredDevices
    {
        get
        {
            //Clena up any Timeouts
            CleanupTimeouts();
            //Practice Thread safety
            lock (mThreadLock)
            {
                //Convert to readonly list
                return mDiscoveredDevices.Values.ToList().AsReadOnly();
            }
        }
    }

    /// <summary>
    /// The timeout in seconds that a device is removed from the <see cref="DiscoveredDevices"/>
    /// list if it is not re-advertised within this time
    /// </summary>
    public int TimeoutRemoval { get; set; } = 20;

    public int HRValue { get; set; }

    #endregion

    #region Constructor
    /// <summary>
    /// The default constructor
    /// </summary>
    public BLEAdvertisementWatcher(GattServiceIDs gattIds)
    {
        //Null guard
        mGattServiceIds = gattIds ?? throw new ArgumentNullException(nameof(gattIds));
        //Create bluetooth listener
        mWatcher = new BluetoothLEAdvertisementWatcher
        {
            ScanningMode = BluetoothLEScanningMode.Active
        };

        //Listen out for new advertisements
        mWatcher.Received += WatcherAdvertisementReceivedAsync;

        //Listen out for when the watcher stops listening
        mWatcher.Stopped += (watcher, e) =>
        {
            //Inform listeners
            StoppedListening();
        };
    }
    #endregion

    #region Private Methods
    /// <summary>
    /// Listens out for watcher advertisements
    /// </summary>
    /// <param name="sender"> The Watcher </param>
    /// <param name="args">The Arguments </param>
    private async void WatcherAdvertisementReceivedAsync(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
    {
        //cleanup timeouts
        CleanupTimeouts();

        //Get BLE device info
        var device = await GetBLEDeviceAsync(args.BluetoothAddress, args.Timestamp, args.RawSignalStrengthInDBm);

        //Null guard
        if(device == null)
        {
            return;
        }

        //is new discovery?
        var newDiscovery = false;
        var existingName = default(string);

        //Lock your doors
        lock (mThreadLock)
        {
            //Check if this is a new discovery
           newDiscovery= !mDiscoveredDevices.ContainsKey(device.DeviceID);

            //If this is not new...
            if (!newDiscovery)
            {
                //Store the old name
                existingName = mDiscoveredDevices[device.DeviceID].Name;
            }
        }

        //Name changed?
        var nameChanged = 
            //if it already exists
            !newDiscovery &&
            //And is not a blank name
            !string.IsNullOrEmpty(device.Name) &&
            //And the name is different
            existingName != device.Name;

        lock (mThreadLock)
        {
            //add/update the device in the dictionary
            mDiscoveredDevices[device.DeviceID] = device;
        }

        //Inform listeners
        DeviceDiscovered(device);

        //if new discovery...
        if (newDiscovery)
        {
            //Inform listeners
            NewDeviceDiscovered(device);
        }
    }

    /// <summary>
    /// Connects to the BLE device and extracts more information from the
    /// <seealso cref="https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice"/>
    /// </summary>
    /// <param name="address">The BT address of the device to connect to</param>
    /// <param name="broadcastTime">The time the broadcast message was received</param>
    /// <param name="rssi">The signal strength in dB</param>
    /// <returns></returns>
    private async Task<BLEDevice> GetBLEDeviceAsync(ulong address, DateTimeOffset broadcastTime, short rssi)
    {
        //Get bluetooth device info
        var device = await BluetoothLEDevice.FromBluetoothAddressAsync(address).AsTask();

        //Null guard
        if(device == null)
        {
            return null;
        }

        //Get GATT services that are available
        var gatt = await device.GetGattServicesAsync().AsTask();

        //if we have any services..
        if(gatt.Status == GattCommunicationStatus.Success)
        {
            //loop each gatt service
            foreach(var service in gatt.Services)
            {
                //This ID contains the GATT profile assigned number we want!
                //TODO: Get more info and connect
                var gattProfileId = service.Uuid;
            }
        }

        //Return the new device information
        return new BLEDevice
            (
            //Device ID
        deviceID: device.DeviceId,
        //Bluetooth address
        address: device.BluetoothAddress,
        //Device name
        name: device.Name,
        //Broadcast time
        broadcastTime: broadcastTime,
        //Signal strength
        rssi: rssi,
        //Is connected
        connected: device.ConnectionStatus== BluetoothConnectionStatus.Connected,
        //Can Pair?
        canPair: device.DeviceInformation.Pairing.CanPair,
        //Is Paired?
        isPaired: device.DeviceInformation.Pairing.IsPaired
            );


    }

    /// <summary>
    /// Prune any timed out devices that we have not heard off
    /// </summary>
    private void CleanupTimeouts()
    {
        lock (mThreadLock)
        {
            //The date in time that if less than means a device has timed out
            var threshold = DateTime.UtcNow - TimeSpan.FromSeconds(TimeoutRemoval);

            //any devices that have not sent a new broadcast within the time
            mDiscoveredDevices.Where(f => f.Value.BroadcastTime < threshold).ToList().ForEach(device =>
             {
                //remove device
                mDiscoveredDevices.Remove(device.Key);

                //Inform listeners
                DeviceTimeout(device.Value);
             });
        }
    }
    #endregion

    #region Public events
    /// <summary>
    /// Fired when the bluetooth watcher stops listening
    /// </summary>
    public event Action StoppedListening = () => { };

    /// <summary>
    /// Fired when the bluetooth watcher start listening
    /// </summary>
    public event Action StartedListening = () => { };

    /// <summary>
    /// fired when a new device is discovered
    /// </summary>
    public event Action<BLEDevice> NewDeviceDiscovered = (device) => {};

    /// <summary>
    /// fired when a device is discovered
    /// </summary>
    public event Action<BLEDevice> DeviceDiscovered = (device) => { };

    /// <summary>
    /// Fired when a device is removed for timing out
    /// </summary>
    public event Action<BLEDevice> DeviceTimeout = (device) => { };
    #endregion

    #region Public Methods
    /// <summary>
    /// Starts listening for advertisements
    /// </summary>
    public void StartListening()
    {
        lock (mThreadLock)
        {
            //if already listening...
            if (Listening)
            {
                //DO nothing more
                return;
            }
            //Start the underlying watcher
            mWatcher.Start();
        }
        //inform listeners
        StartedListening();
    }

    /// <summary>
    /// Stops listening for advertisements
    /// </summary>
    public void StopListening()
    {
        lock (mThreadLock)
        {
            //if we are not currently listening
            if (!Listening)
            {
                //Do nothing more
                return;
            }

            //Stop listening
            mWatcher.Stop();

            //clear any devices
            mDiscoveredDevices.Clear();
        }
    }

    /// <summary>
    /// Attempts to pair to a BLE device, by ID
    /// </summary>
    /// <param name="deviceID"> The BLE device ID</param>
    /// <returns></returns>
    public async Task PairToDeviceAsync(string deviceID)
    {
        //Get bluetooth device info
        var device = await BluetoothLEDevice.FromIdAsync(deviceID).AsTask();

        //Null guard
        if (device == null)
        {
            //TODO: localize
            throw new ArgumentNullException("");
        }

        //if we are already paired...
        if (device.DeviceInformation.Pairing.IsPaired)
        {
            //un-pair the device
            await device.DeviceInformation.Pairing.UnpairAsync().AsTask();
            return;
        }
        //Try and pair to the device
        device.DeviceInformation.Pairing.Custom.PairingRequested += (sender, args) =>
            {
                //Accept all attempts
                args.Accept(); // <-- could enter a pin in here to accept
            };
        var result = await device.DeviceInformation.Pairing.Custom.PairAsync(DevicePairingKinds.ConfirmOnly).AsTask();

        //Get GATT services that are available
        var gatt = await device.GetGattServicesAsync().AsTask();

        GattDeviceService serviceReq = null;

        GattCharacteristic characteristicReq = null;

        //if we have any services..
        if (gatt.Status == GattCommunicationStatus.Success)
        {
            //loop each gatt service
            foreach (var service in gatt.Services)
            {
                if (service.Uuid == GattServiceUuids.HeartRate)
                {
                    serviceReq = service;
                }
                //This ID contains the GATT profile assigned number we want!
                //TODO: Get more info and connect
                var gattProfileId = service.Uuid;
            }

            var charResults = await serviceReq.GetCharacteristicsAsync().AsTask();

            if(charResults.Status == GattCommunicationStatus.Success)
            {
                foreach (var chars in charResults.Characteristics)
                {
                    if(chars.Uuid == GattCharacteristicUuids.HeartRateMeasurement)
                    {
                        characteristicReq = chars;
                    }
                }

                GattCharacteristicProperties properties = characteristicReq.CharacteristicProperties;

                if (properties.HasFlag(GattCharacteristicProperties.Read))
                {
                    GattReadResult readVal = await characteristicReq.ReadValueAsync().AsTask();

                    if(readVal.Status == GattCommunicationStatus.Success)
                    {
                        var reader = DataReader.FromBuffer(readVal.Value);
                        byte[] input = new byte[reader.UnconsumedBufferLength];
                        reader.ReadBytes(input);
                        HRValue = BitConverter.ToInt32(input, 0);
                    }
                }
            }
        }

        ////Log the result
        //if(result.Status == DevicePairingResultStatus.Paired)
        //{
        //    Console.WriteLine("Pairing successful");
        //}
        //else
        //{
        //    Console.WriteLine($"Pairing failed: {result.Status}");
        //}

    }
    #endregion
}
}

這是我在 Unity 中嘗試的代碼:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

#if NETFX_CORE
using BLEHR
#endif

public class HRTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
#if NETFX_CORE
        var watcher = new BLEAdvertisementWatcher(new GattServiceIDs());
        Debug.Log("Working?");
#endif

    }

    // Update is called once per frame
    void Update()
    {
#if WINDOWS_UWP
        Debug.Log("Connecting");
#endif
    }
}

由於編輯-編譯-運行周期很長,在 Unity 中使用 UWP/.Net-Core-only 功能進行調試對我來說似乎是一個不可行的工作流程。 Unity 編輯器與 .Net-Framework 一起運行,因此無法直接訪問 .Net-Core 功能。 對於非 Unity 項目,可以通過 Windows 提供的包裝器從 .Net-Framework 訪問 UWP-API。 但這對 Unity 不起作用,因為某些 Unity 特定的1

但是有可能將 UWP 調用包裝在 C++ winrt dll 中並將該 dll 復制到 Unity。 這樣,UWP-API 也可以在 Unity 編輯器中使用(例如,按下播放按鈕時)。 我的用例也需要 BLE,我在這個 repo 中上傳了我的包裝器。 如果您願意,您可以自由地將其用作起點。

最后一件事:隨着 .Net 第 5 版也為 Unity 發布,我想這應該會過時,因為據說第 5 版合並了 .Net-Framework 和 .Net-Core。


[1] 這個答案的很多信息來自Mike Taulty 的這篇博文

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM