UWP 低功耗蓝牙应用程序提前断开连接

UWP Bluetooth Low Energy Application Disconnects Early

所以我正在为 windows 笔记本电脑设计一个应用程序以连接到定制设计的压力传感器。应用程序与设备配对,然后每 10 毫秒从设备接收一次通知。然后由于某种原因,通信停止了。我知道这是我的应用程序的问题,而不是设备的问题,因为当我连接到我的 phone 时,我没有遇到这个问题。

这是我创建 devicewatcher 和发现设备的主页:

using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.Devices.Bluetooth;
using Windows.Devices.Enumeration;

// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409

namespace BLEInterfaceTest
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class MainPage : Page
    {
        private DeviceWatcher deviceWatcher;
        private ObservableCollection<DeviceInformation> deviceList = new ObservableCollection<DeviceInformation>();

    public MainPage()
    {
        this.InitializeComponent();
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        this.DataContext = deviceList;
        deviceListView.ItemsSource = deviceList;
        deviceWatcher = DeviceInformation.CreateWatcher(
            "System.ItemNameDisplay:~~\"Button\"",
            new string[] {
                "System.Devices.Aep.DeviceAddress",
                "System.Devices.Aep.IsConnected" },
            DeviceInformationKind.AssociationEndpoint);
        deviceWatcher.Added += DeviceWatcher_Added;
        deviceWatcher.Removed += DeviceWatcher_Removed;
        deviceWatcher.Start();
        base.OnNavigatedTo(e);
        SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
            AppViewBackButtonVisibility.Collapsed;
    }

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        deviceWatcher.Stop();
        base.OnNavigatedFrom(e);
    }

    private async void DeviceWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
    {
        var toRemove = (from a in deviceList where a.Id == args.Id select a).FirstOrDefault();

        if (toRemove != null)
        {
            await this.Dispatcher.RunAsync(
                Windows.UI.Core.CoreDispatcherPriority.Normal,
                () => { deviceList.Remove(toRemove); });
        }
    }

    private async void DeviceWatcher_Added(DeviceWatcher sender, DeviceInformation args)
    {
        await this.Dispatcher.RunAsync(
            Windows.UI.Core.CoreDispatcherPriority.Normal,
            () => { deviceList.Add(args); });
    }

    private void deviceListView_ItemClick(object sender, ItemClickEventArgs e)
    {
        this.Frame.Navigate(typeof(DevicePage), e.ClickedItem);
    }
  }
}'

下一个代码是连接压力传感器和从设备读取数据的页面。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.UI.Popups;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Bluetooth;
using Windows.Devices.Enumeration;
using Windows.Storage.Pickers;
using Windows.Storage;
using Windows.Storage.Streams;
using System.Threading.Tasks;
using Windows.ApplicationModel.Background;


// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238

namespace BLEInterfaceTest
{
    /// <summary>
    /// An empty page that can be used on its own or navigated to within a Frame.
    /// </summary>
    public sealed partial class DevicePage : Page
    {
        private DeviceInformation device { get; set; }
        private PressureSensor pSensor { get; set; }
        public static DateTime startTime { get; set; }
        public ObservableCollection<DataPoint> PressureData = new ObservableCollection<DataPoint>();
        public static ObservableCollection<DataPoint> inbetween;
        private static TextBox txtP;
        private BluetoothLEDevice leDevice;
        private DispatcherTimer timer = new DispatcherTimer();
        private int packetNum = 0;

        public DevicePage()
        {
            this.InitializeComponent();
            SystemNavigationManager.GetForCurrentView().BackRequested += DevicePage_BackRequested;
            txtP = txtValue1;
            inbetween = PressureData;
        }

        public static void ChangeText(string text)
        {
            txtP.Text = text;
        }

        private async void InitializePressureSensor(GattDeviceService service)
        {
            pSensor = new PressureSensor(service, SensorUUIDs.PressureSensorUuid);
            await pSensor.EnableNotifications();
            btnStart.IsEnabled = true;
        }

        private async void StartRecievingData()
        {
            try
            {
                leDevice = await BluetoothLEDevice.FromIdAsync(device.Id);
                string selector = "(System.DeviceInterface.Bluetooth.DeviceAddress:=\"" +
                    leDevice.BluetoothAddress.ToString("X") + "\")";
                var services = await leDevice.GetGattServicesAsync(BluetoothCacheMode.Uncached);

                foreach (var service in services.Services)
                {
                    if (service.Uuid.ToString() == SensorUUIDs.ButtonSensorServiceUuid)
                    {
                        InitializePressureSensor(service);
                    }
                }

                timer.Interval = new TimeSpan(0, 0, 0, 0, 1);
                timer.Tick += Timer_Tick1;
                startTime = DateTime.Now;
                timer.Start();
            }

            catch (Exception ex)
            {
                var messageDialog = new MessageDialog("An error has occured Please try again. \n" + ex.Message, "Error!");
            }
        }

        public async void UpdateAllData()
        {
            while (pSensor != null && pSensor.MorePacketsAvailable)
            {
                int[] values = await pSensor.GetPressure();

                int packetNumber = values[0];

                if (packetNumber > packetNum)
                {
                    packetNum = packetNumber;

                    txtValue1.Text = Convert.ToString(values[1]);
                    txtValue2.Text = Convert.ToString(values[5]);

                    for (int i = 1; i < 5; i++)
                    {
                        PressureData.Add(new DataPoint(DateTime.Now - startTime, packetNumber, ((i-1)*2.5 + 10*packetNumber), values[i], values[i + 4]));
                    }
                }
            }
        }

        private void Timer_Tick1(object sender, object e)
        {

            UpdateAllData();
        }

        private async void PairToDevice()
        {
            if (device.Pairing.CanPair)
            {
                var customPairing = device.Pairing.Custom;

                customPairing.PairingRequested += CustomPairing_PairingRequested;

                var result = await customPairing.PairAsync(DevicePairingKinds.ConfirmOnly);

                customPairing.PairingRequested -= CustomPairing_PairingRequested;

                if ((result.Status == DevicePairingResultStatus.Paired) || (result.Status == DevicePairingResultStatus.AlreadyPaired))
                {
                    /*while (device.Pairing.IsPaired == false)
                    {
                        device = await DeviceInformation.CreateFromIdAsync(device.Id);
                    }*/

                    StartRecievingData();
                }


            }

            else if (device.Pairing.IsPaired)
            {
                StartRecievingData();
            }
        }

        private void CustomPairing_PairingRequested(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs args)
        {
            args.Accept();
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            btnSave.Content = "Save";
            btnStop.IsEnabled = false;
            btnStart.IsEnabled = false;
            this.DataContext = PressureData;
            device = (DeviceInformation)e.Parameter;
            PairToDevice();
            //StartRecievingData();

            base.OnNavigatedTo(e);

            Frame rootFrame = Window.Current.Content as Frame;

            if (rootFrame.CanGoBack)
            {
                SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
                    AppViewBackButtonVisibility.Visible;
            }
        }

        private void DevicePage_BackRequested(object sender, BackRequestedEventArgs eventArgs)
        {
            Frame rootFrame = Window.Current.Content as Frame;

            if (rootFrame == null)
            {
                return;
            }

            // Navigate back if possible, and if the event has already been handled
            if (rootFrame.CanGoBack && eventArgs.Handled ==false)
            {
                eventArgs.Handled = true;
                rootFrame.GoBack();
            }
        }

        private async void btnSave_Click(object sender, RoutedEventArgs e)
        {
            timer.Stop();
            var picker = new FileSavePicker();
            picker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
            picker.FileTypeChoices.Add("CSV", new List<string>() { ".csv" });

            StorageFile file = await picker.PickSaveFileAsync();

            if (file != null)
            {
                var stream = await file.OpenAsync(FileAccessMode.ReadWrite);

                using (IOutputStream outputStream = stream.GetOutputStreamAt(0))
                {
                    using (var writer = new DataWriter(outputStream))
                    {
                        foreach (DataPoint p in PressureData)
                        {
                            string text = p.TimeStamp.ToString() + "," + p.PacketNumber.ToString() + "," + p.InternalTimestamp.ToString() + "," + p.PressureValue1.ToString() + "," + p.PressureValue2.ToString() +  "\n";
                            writer.WriteString(text);
                        }

                        await writer.StoreAsync();
                        await writer.FlushAsync();
                    }
                }

                stream.Dispose();
            }
        }

        private async void btnStart_Click(object sender, RoutedEventArgs e)
        {
            if (pSensor != null)
            {
                btnStop.IsEnabled = true;
                btnStart.IsEnabled = false;

                startTime = DateTime.Now;

                if (pSensor != null)
                {
                    await pSensor.BeginCollecting();
                }
            }
        }

        private async void btnStop_Click(object sender, RoutedEventArgs e)
        {
            btnStart.IsEnabled = true;
            btnStop.IsEnabled = false;

            if (pSensor != null)
            {
                await pSensor.StopCollecting();
            }
        }
    }
}

这里是我定义处理设备连接的 SensorBase 和 PressureSensor class 的地方:

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

namespace BLEInterfaceTest
{
    public static class SensorUUIDs
    {
        private static readonly string _packetUuid =           "0000a043-0000-1000-8000-00805f9b34fb";
        private static readonly string _buttonSensorServiceUuid = "0000a042-0000-1000-8000-00805f9b34fb";
        private static readonly string _sensorStateUuid =         "0000a044-0000-1000-8000-00805f9b34fb";

        public static string PressureSensorUuid
        {
            get { return _packetUuid; }
        }

        public static string ButtonSensorServiceUuid
        {
            get { return _buttonSensorServiceUuid; }
        }

        public static string SensorStateUuid
        {
            get { return _sensorStateUuid; }
        }
    }

    public class SensorBase : IDisposable
    {
        protected GattDeviceService deviceService;
        protected string sensorDataUuid;
        protected Queue<byte[]> fifoBuffer;
        protected bool isNotificationSupported = false;
        public bool newData = false;
        private GattCharacteristic dataCharacteristic;

        public SensorBase(GattDeviceService dataService, string sensorDataUuid)
        {
            this.deviceService = dataService;
            this.sensorDataUuid = sensorDataUuid;
            fifoBuffer = new Queue<byte[]>(20);
        }

        public bool MorePacketsAvailable
        {
            get
            {
                if (fifoBuffer.Count > 0)
                {
                    return true;
                }

                else
                {
                    return false;
                }
            }
        }

        public virtual async Task EnableNotifications()
        {
            GattCharacteristicsResult result = await deviceService.GetCharacteristicsAsync();

            foreach (var test in result.Characteristics)
            {
                string t = test.Uuid.ToString();
            }


            isNotificationSupported = true;
            dataCharacteristic = (await deviceService.GetCharacteristicsForUuidAsync(
                new Guid(sensorDataUuid))).Characteristics[0];
            dataCharacteristic.ValueChanged += dataCharacteristic_ValueChanged;
            GattCommunicationStatus status = await dataCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
                GattClientCharacteristicConfigurationDescriptorValue.Notify);

            var currentDescriptorValue = await dataCharacteristic.ReadClientCharacteristicConfigurationDescriptorAsync();

            if (currentDescriptorValue.Status != GattCommunicationStatus.Success
                || currentDescriptorValue.ClientCharacteristicConfigurationDescriptor != GattClientCharacteristicConfigurationDescriptorValue.Notify)
            {
                GattCommunicationStatus status2 = await dataCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
                GattClientCharacteristicConfigurationDescriptorValue.Notify);
            }
        }

        public virtual async Task DisableNotifications()
        {
            newData = false;
            isNotificationSupported = false;
            dataCharacteristic = (await deviceService.GetCharacteristicsForUuidAsync(
                new Guid(sensorDataUuid))).Characteristics[0];
            dataCharacteristic.ValueChanged -= dataCharacteristic_ValueChanged;
            GattCommunicationStatus status = await dataCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.None);
        }

        protected async Task<byte[]> ReadValue()
        {
            if (!isNotificationSupported)
            {
                if (dataCharacteristic == null)
                {
                    dataCharacteristic = (await deviceService.GetCharacteristicsForUuidAsync(
                        new Guid(sensorDataUuid))).Characteristics[0];
                }

                GattReadResult readResult = await dataCharacteristic.ReadValueAsync();
                byte[] data = new byte[readResult.Value.Length];
                DataReader.FromBuffer(readResult.Value).ReadBytes(data);

                fifoBuffer.Enqueue(data);
            }

            return fifoBuffer.Dequeue();
        }

        protected async Task WriteByteArray(string characteristicUuid, byte[] value)
        {
            GattCharacteristic writeCharacteristic = (await deviceService.GetCharacteristicsForUuidAsync(
                        new Guid(characteristicUuid))).Characteristics[0];

            var writer = new DataWriter();
            writer.WriteBytes(value);
            var res = await writeCharacteristic.WriteValueAsync(writer.DetachBuffer(), GattWriteOption.WriteWithoutResponse);
        }

        private void dataCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
        {
            byte[] data = new byte[args.CharacteristicValue.Length];
            DataReader.FromBuffer(args.CharacteristicValue).ReadBytes(data);
            fifoBuffer.Enqueue(data);
            newData = true;
        }

        public async void Dispose()
        {
            await DisableNotifications();
        }
    }

    public class PressureSensor: SensorBase
    {
        public PressureSensor(GattDeviceService dataService, string sensorDataUuid)
            : base(dataService, sensorDataUuid)
        {

        }

        public async Task BeginCollecting()
        {
            await WriteByteArray(SensorUUIDs.SensorStateUuid, new byte[] { 0x01 });
        }

        public async Task<int[]> GetPressure()
        {
            byte[] data = await ReadValue();

            if (data != null)
            {
                int[] values = new int[9];

                values[0] = (int)BitConverter.ToInt32(data, 0);

                for (int i = 1; i < values.Length; i++)
                {
                    values[i] = (int)BitConverter.ToInt16(data, 2 * i + 2);
                }

                return values;
            }

            else
            {
                return new int[] { 0 };
            }
        }

        public async Task StopCollecting()
        {
            await WriteByteArray(SensorUUIDs.SensorStateUuid, new byte[] { 0x00 });
        }
    }
}

这是我用来整理从压力传感器接收到的数据的 DataPoint Class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace BLEInterfaceTest
{
    public class DataPoint : INotifyPropertyChanged
    {
        private TimeSpan _timestamp;
        private int _packetNumber;
        private double _internalTimestamp;
        private int _pressure1;
        private int _pressure2;

        public event PropertyChangedEventHandler PropertyChanged;

        public TimeSpan TimeStamp
        {
            get { return _timestamp; }
            set
            {
                _timestamp = value;
                this.NotifyPropertyChanged();
            }
        }

        public int PacketNumber
        {
            get { return _packetNumber; }
            set
            {
                _packetNumber = value;
                this.NotifyPropertyChanged();
            }
        }
        public double InternalTimestamp
        {
            get { return _internalTimestamp; }
            set
            {
                _internalTimestamp = value;
                this.NotifyPropertyChanged();
            }
        }

        public int PressureValue1
        {
            get { return _pressure1; }
            set
            {
                _pressure1 = value;
                this.NotifyPropertyChanged();
            }
        }

        public int PressureValue2
        {
            get { return _pressure2; }
            set
            {
                _pressure2 = value;
                this.NotifyPropertyChanged();
            }
        }

        public DataPoint(TimeSpan time,int packetNumber, double internalTimestamp, int pressure1, int pressure2)
        {
            _timestamp = time;
            _packetNumber = packetNumber;
            _internalTimestamp = internalTimestamp;
            _pressure1 = pressure1;
            _pressure2 = pressure2;
        }

        private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            if (!string.IsNullOrEmpty(propertyName))
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

我对此进行了广泛的研究,我所能找到的只是有关如何启动断开连接的帮助。我有相反的问题。我找到的一页说问题可能是设备没有正确存储绑定状态引起的,但我已经检查过了,我确实初始化了设备以保存绑定状态。

有趣的是,如果我在尝试从中读取信息之前没有将设备与计算机配对,那么我就没有问题。连接永远不会随机停止。但是当我这样做时,计算机并没有收到从传感器设备发送的每一个数据包。它将接收一两个数据包,然后跳过五六个数据包。如果我将设备配对,那么我将收到每个数据包,但连接会随机中断。

所以我想我的问题有两个方面。设备配对时如何阻止连接断开?或者,有没有办法让应用程序在未配对时接收每个数据包?

更新

我意识到我应该在我的传感器外围设备上包含更多信息,以防错误出现在该代码中。在我继续设计嵌入式版本之前,我目前正在设计该传感器的快速原型。为此,我使用 RedBearLabs 的 BLE Nano 1 作为用户友好型原型。我正在使用在线 MBED 编译器对该设备进行编程。我包含了 nRF51822 和 BLE_API 库来处理蓝牙低功耗通信。

更新 2 因此,在进一步研究导致问题的原因后,我发现连接间隔和第 2 代垃圾收集同时发生时会发生断开连接。在 UWP 中,垃圾收集器可以暂停第 2 代收集的 UI 线程。 (参见 here

我的想法是,如果线程在连接间隔开始时暂停,则中央无法启动与外围设备的连接,因此外围设备认为客户端不再监听(参见更多关于how BLE connections work).

我发现这一点是通过找出在连接随机停止后恢复连接所需的确切条件。我从整个连接过程开始,并将其简化为:

public async Task ReconnectDevice()
{
   GattCommunicationStatus status = await dataCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
      GattClientCharacteristicConfigurationDescriptorValue.Notify);

   await WriteByteArray(SensorUUIDs.SensorStateUuid, new byte[] { 0x01 });
}

因为我的 BluetoothLEDevice、GattService 和 GattCharacteristic 对象没有被释放,我需要做的就是重新订阅通知并向设备写入 1,以便它再次开始收集数据。

自从发现这一点后,我已经显着减少了应用程序中的内存分配,而且 gen2 收集的时间已减少到平均 5 毫秒。此外,连接断开之前的时间量已增加到大约 4-5 秒。

UWP 有一个 GattCharacteristicNotificationTrigger 用于在 BackgroundTask 中接收通知,但我在将后台任务合并到 UWP 中从来没有取得多大成功。

我想接下来我会尝试将 windows.devices 合并到 WPF 应用程序中,我认为我将有更好的机会让它工作。

在我看来,这些问题与 BluetoothCacheMode 枚举有关。 这表明某些蓝牙 API 方法是否应该对系统中缓存的值进行操作,或者 从蓝牙设备中检索这些值。 使用 BluetoothCacheMode.Uncached 属性允许服务在需要时更新属性。 如果设备已配对,则不需要 BluetoothCacheMode(我认为 BluetoothCacheMode.Cached 是默认值)。 在您的代码中,行:

var services = await leDevice.GetGattServicesAsync(BluetoothCacheMode.Uncached);

如果配对,可能是连接丢失的原因。

GetGattServicesAsync()、GetCharacteristicsAsync() 和 ReadValueAsync() 未配对时必须具有属性 BluetoothCacheMode.Uncached,配对时默认或 BluetoothCacheMode.Cached。 参见 https://msdn.microsoft.com/en-us/library/windows/apps/dn263758.aspx

所以,在尝试不同的想法一段时间后,我终于偶然发现了解决我的问题的方法。我必须进行 2 处更改:

  1. 使用了非配对连接而不是配对连接。这解决了连接突然断开的问题。

  2. 将连接间隔增加到 40 毫秒。出于某种原因,当我这样做时,我收到了所有数据并且不再有任何问题。在与 Windows 设备通信时,低于 40 毫秒的任何时间都会导致信息丢失(我必须在我的传感器上对 C 代码 运行 进行此更改。)

进行此更改后,我已经使用这些设备大约 2 个月了,完全没有问题。