c# - UWP 蓝牙低功耗应用提前断开连接

标签 c# uwp bluetooth-lowenergy mbed nrf51

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

这是我创建 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 类的地方:
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 类:
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));
            }
        }
    }
}

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

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

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

更新

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

更新 2
所以在对导致问题的原因进行了更多研究后,我发现连接间隔和第 2 代垃圾收集同时发生时会发生断开连接。在 UWP 中,垃圾收集器可以暂停 UI 线程以进行第 2 代收集。 (见 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 应用程序中,我认为我将有更好的机会让它工作。

最佳答案

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

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

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

    关于c# - UWP 蓝牙低功耗应用提前断开连接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46860756/

    相关文章:

    c# - DelegatingHandler 适用于本地主机,但不适用于 Azure

    c# - 如何在 C# 类库中使用 Ninject

    c# - 如何以编程方式重新启动 Windows 10 IoT 应用程序

    ios - 在 iOS 设备上以编程方式测试蓝牙版本

    c# - 如何从运行在 Windows 10 Creators Update 上的 WPF 应用注册 BLE 通知?

    c# - 如何从下拉列表中获取选定的值 C# ASP.NET

    c# - 如何在旧版本的 Windows 10 上测试应用程序?

    c# - Windows.ApplicationModel.LockScreen 命名空间是否可供非信息亭使用?

    iOS CBPeripheral 连接问题

    c# - 代码隐藏中的绑定(bind)属性