繁体   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