Приложение UWP Bluetooth с низким энергопотреблением отключается на ранней стадии

Поэтому я разрабатываю приложение для ноутбуков с ОС 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));
            }
        }
    }
}

Я тщательно исследовал это, и все, что я мог найти, это помочь в том, как инициировать отключение. У меня противоположная проблема. На одной из найденных мной страниц было указано, что проблема может быть вызвана тем, что устройство неправильно хранит состояние соединения, но я проверил это и инициализировал устройство, чтобы сохранить состояние соединения.

Интересно, что если я не подключу устройство к компьютеру до того, как попытаюсь прочитать с него информацию, у меня не возникнет проблемы. Соединение никогда не останавливается случайным образом. Но когда я делаю это, компьютер не получает каждый пакет данных, отправленных с сенсорного устройства. Он получит один или два пакета, а затем пропустит пять или шесть пакетов. Если я подключу устройство, я получу каждый пакет, но соединение будет случайно разорвано.

Так что мой вопрос в два раза, я думаю. Как предотвратить прерывание соединения при сопряжении устройства? Или, в качестве альтернативы, есть ли способ, позволяющий приложению получать каждый пакет данных, когда он не связан?

ОБНОВИТЬ

Я понял, что должен включить больше информации о моем периферийном устройстве датчика на случай, если ошибка в этом коде. В настоящее время я разрабатываю быстрое прототипирование этого датчика, прежде чем перейти к разработке встроенной версии. Для этого я использую BLE Nano 1 от RedBearLabs в качестве удобного прототипа. Я программирую это устройство с помощью онлайн-компилятора MBED. Я включил библиотеки nRF51822 и BLE_API для связи Bluetooth с низким энергопотреблением.

ОБНОВЛЕНИЕ 2 Итак, после дополнительных исследований того, что является причиной проблемы, я обнаружил, что разъединение происходит, когда интервал соединения и сборка мусора поколения 2 происходят одновременно. В UWP сборщик мусора может приостановить поток пользовательского интерфейса для сборок второго поколения. (см. здесь)

Я думаю, что если поток приостанавливается в начале интервала соединения, то центральное устройство не может инициировать соединение с периферийным устройством, и поэтому периферийное устройство думает, что клиент больше не слушает (подробнее о том, как работают соединения BLE),

Я обнаружил это, выяснив, что именно необходимо для восстановления соединения после случайного прерывания. Я начал со всего процесса подключения и сократил его до этого:

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 ответа

Решение

Итак, после того, как я попробовал разные идеи, я, наконец, наткнулся на решение своей проблемы. Мне пришлось сделать 2 изменения:

  1. Использовали непарное соединение вместо спаренного соединения. Это решило проблему разрыва соединения внезапно.

  2. Увеличен интервал соединения до 40 мс. По какой-то причине, когда я сделал это, я получил все данные, и у меня больше не было проблем. Если значение меньше 40 мс, это приводит к потере информации при обмене данными с устройством Windows (мне пришлось внести это изменение в код C, работающий на моих датчиках).

Я использовал устройства в течение 2 месяцев после внесения этих изменений, и у меня не было никаких проблем.

Мне кажется, что эти проблемы связаны с перечислением BluetoothCacheMode. Это указывает, должны ли определенные методы Bluetooth API работать со значениями, кэшированными в системе, или извлекать эти значения из устройства Bluetooth. Использование атрибута 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.

Другие вопросы по тегам