forked from microsoft/Windows-universal-samples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Scenario2_Client.xaml.cs
573 lines (521 loc) · 24.7 KB
/
Scenario2_Client.xaml.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the MIT License (MIT).
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Security.Cryptography;
using Windows.Storage.Streams;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
namespace SDKTemplate
{
// This scenario connects to the device selected in the "Discover
// GATT Servers" scenario and communicates with it.
// Note that this scenario is rather artificial because it communicates
// with an unknown service with unknown characteristics.
// In practice, your app will be interested in a specific service with
// a specific characteristic.
public sealed partial class Scenario2_Client : Page
{
private MainPage rootPage = MainPage.Current;
private ObservableCollection<BluetoothLEAttributeDisplay> ServiceCollection = new ObservableCollection<BluetoothLEAttributeDisplay>();
private ObservableCollection<BluetoothLEAttributeDisplay> CharacteristicCollection = new ObservableCollection<BluetoothLEAttributeDisplay>();
private BluetoothLEDevice bluetoothLeDevice = null;
private GattCharacteristic selectedCharacteristic;
// Only one registered characteristic at a time.
private GattCharacteristic registeredCharacteristic;
private GattPresentationFormat presentationFormat;
#region Error Codes
readonly int E_BLUETOOTH_ATT_WRITE_NOT_PERMITTED = unchecked((int)0x80650003);
readonly int E_BLUETOOTH_ATT_INVALID_PDU = unchecked((int)0x80650004);
readonly int E_ACCESSDENIED = unchecked((int)0x80070005);
readonly int E_DEVICE_NOT_AVAILABLE = unchecked((int)0x800710df); // HRESULT_FROM_WIN32(ERROR_DEVICE_NOT_AVAILABLE)
#endregion
#region UI Code
public Scenario2_Client()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (string.IsNullOrEmpty(rootPage.SelectedBleDeviceId))
{
ConnectButton.IsEnabled = false;
}
}
protected override async void OnNavigatedFrom(NavigationEventArgs e)
{
var success = await ClearBluetoothLEDeviceAsync();
if (!success)
{
rootPage.NotifyUser("Error: Unable to reset app state", NotifyType.ErrorMessage);
}
}
#endregion
#region Enumerating Services
private async Task<bool> ClearBluetoothLEDeviceAsync()
{
if (subscribedForNotifications)
{
// Need to clear the CCCD from the remote device so we stop receiving notifications
var result = await registeredCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.None);
if (result != GattCommunicationStatus.Success)
{
return false;
}
else
{
selectedCharacteristic.ValueChanged -= Characteristic_ValueChanged;
subscribedForNotifications = false;
}
}
bluetoothLeDevice?.Dispose();
bluetoothLeDevice = null;
return true;
}
private async void ConnectButton_Click()
{
ConnectButton.IsEnabled = false;
if (!await ClearBluetoothLEDeviceAsync())
{
rootPage.NotifyUser("Error: Unable to reset state, try again.", NotifyType.ErrorMessage);
ConnectButton.IsEnabled = false;
return;
}
try
{
// BT_Code: BluetoothLEDevice.FromIdAsync must be called from a UI thread because it may prompt for consent.
bluetoothLeDevice = await BluetoothLEDevice.FromIdAsync(rootPage.SelectedBleDeviceId);
if (bluetoothLeDevice == null)
{
rootPage.NotifyUser("Failed to connect to device.", NotifyType.ErrorMessage);
}
}
catch (Exception ex) when (ex.HResult == E_DEVICE_NOT_AVAILABLE)
{
rootPage.NotifyUser("Bluetooth radio is not on.", NotifyType.ErrorMessage);
}
if (bluetoothLeDevice != null)
{
// Note: BluetoothLEDevice.GattServices property will return an empty list for unpaired devices. For all uses we recommend using the GetGattServicesAsync method.
// BT_Code: GetGattServicesAsync returns a list of all the supported services of the device (even if it's not paired to the system).
// If the services supported by the device are expected to change during BT usage, subscribe to the GattServicesChanged event.
GattDeviceServicesResult result = await bluetoothLeDevice.GetGattServicesAsync(BluetoothCacheMode.Uncached);
if (result.Status == GattCommunicationStatus.Success)
{
var services = result.Services;
rootPage.NotifyUser(String.Format("Found {0} services", services.Count), NotifyType.StatusMessage);
foreach (var service in services)
{
ServiceCollection.Add(new BluetoothLEAttributeDisplay(service));
}
ConnectButton.Visibility = Visibility.Collapsed;
ServiceList.Visibility = Visibility.Visible;
}
else
{
rootPage.NotifyUser("Device unreachable", NotifyType.ErrorMessage);
}
}
ConnectButton.IsEnabled = true;
}
#endregion
#region Enumerating Characteristics
private async void ServiceList_SelectionChanged()
{
var attributeInfoDisp = (BluetoothLEAttributeDisplay)ServiceList.SelectedItem;
CharacteristicCollection.Clear();
RemoveValueChangedHandler();
IReadOnlyList<GattCharacteristic> characteristics = null;
try
{
// Ensure we have access to the device.
var accessStatus = await attributeInfoDisp.service.RequestAccessAsync();
if (accessStatus == DeviceAccessStatus.Allowed)
{
// BT_Code: Get all the child characteristics of a service. Use the cache mode to specify uncached characterstics only
// and the new Async functions to get the characteristics of unpaired devices as well.
var result = await attributeInfoDisp.service.GetCharacteristicsAsync(BluetoothCacheMode.Uncached);
if (result.Status == GattCommunicationStatus.Success)
{
characteristics = result.Characteristics;
}
}
else
{
// Not granted access
rootPage.NotifyUser("Error accessing service.", NotifyType.ErrorMessage);
// On error, act as if there are no characteristics.
characteristics = new List<GattCharacteristic>();
}
}
catch (Exception ex)
{
rootPage.NotifyUser("Restricted service. Can't read characteristics: " + ex.Message,
NotifyType.ErrorMessage);
// On error, act as if there are no characteristics.
characteristics = new List<GattCharacteristic>();
}
foreach (GattCharacteristic c in characteristics)
{
CharacteristicCollection.Add(new BluetoothLEAttributeDisplay(c));
}
CharacteristicList.Visibility = Visibility.Visible;
}
#endregion
private void AddValueChangedHandler()
{
ValueChangedSubscribeToggle.Content = "Unsubscribe from value changes";
if (!subscribedForNotifications)
{
registeredCharacteristic = selectedCharacteristic;
registeredCharacteristic.ValueChanged += Characteristic_ValueChanged;
subscribedForNotifications = true;
}
}
private void RemoveValueChangedHandler()
{
ValueChangedSubscribeToggle.Content = "Subscribe to value changes";
if (subscribedForNotifications)
{
registeredCharacteristic.ValueChanged -= Characteristic_ValueChanged;
registeredCharacteristic = null;
subscribedForNotifications = false;
}
}
private async void CharacteristicList_SelectionChanged()
{
selectedCharacteristic = null;
var attributeInfoDisp = (BluetoothLEAttributeDisplay)CharacteristicList.SelectedItem;
if (attributeInfoDisp == null)
{
EnableCharacteristicPanels(GattCharacteristicProperties.None);
return;
}
selectedCharacteristic = attributeInfoDisp.characteristic;
if (selectedCharacteristic == null)
{
rootPage.NotifyUser("No characteristic selected", NotifyType.ErrorMessage);
return;
}
// Get all the child descriptors of a characteristics. Use the cache mode to specify uncached descriptors only
// and the new Async functions to get the descriptors of unpaired devices as well.
var result = await selectedCharacteristic.GetDescriptorsAsync(BluetoothCacheMode.Uncached);
if (result.Status != GattCommunicationStatus.Success)
{
rootPage.NotifyUser("Descriptor read failure: " + result.Status.ToString(), NotifyType.ErrorMessage);
}
// BT_Code: There's no need to access presentation format unless there's at least one.
presentationFormat = null;
if (selectedCharacteristic.PresentationFormats.Count > 0)
{
if (selectedCharacteristic.PresentationFormats.Count.Equals(1))
{
// Get the presentation format since there's only one way of presenting it
presentationFormat = selectedCharacteristic.PresentationFormats[0];
}
else
{
// It's difficult to figure out how to split up a characteristic and encode its different parts properly.
// In this case, we'll just encode the whole thing to a string to make it easy to print out.
}
}
// Enable/disable operations based on the GattCharacteristicProperties.
EnableCharacteristicPanels(selectedCharacteristic.CharacteristicProperties);
}
private void SetVisibility(UIElement element, bool visible)
{
element.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
}
private void EnableCharacteristicPanels(GattCharacteristicProperties properties)
{
// BT_Code: Hide the controls which do not apply to this characteristic.
SetVisibility(CharacteristicReadButton, properties.HasFlag(GattCharacteristicProperties.Read));
SetVisibility(CharacteristicWritePanel,
properties.HasFlag(GattCharacteristicProperties.Write) ||
properties.HasFlag(GattCharacteristicProperties.WriteWithoutResponse));
CharacteristicWriteValue.Text = "";
SetVisibility(ValueChangedSubscribeToggle, properties.HasFlag(GattCharacteristicProperties.Indicate) ||
properties.HasFlag(GattCharacteristicProperties.Notify));
}
private async void CharacteristicReadButton_Click()
{
// BT_Code: Read the actual value from the device by using Uncached.
GattReadResult result = await selectedCharacteristic.ReadValueAsync(BluetoothCacheMode.Uncached);
if (result.Status == GattCommunicationStatus.Success)
{
string formattedResult = FormatValueByPresentation(result.Value, presentationFormat);
rootPage.NotifyUser($"Read result: {formattedResult}", NotifyType.StatusMessage);
}
else
{
rootPage.NotifyUser($"Read failed: {result.Status}", NotifyType.ErrorMessage);
}
}
private async void CharacteristicWriteButton_Click()
{
if (!String.IsNullOrEmpty(CharacteristicWriteValue.Text))
{
var writeBuffer = CryptographicBuffer.ConvertStringToBinary(CharacteristicWriteValue.Text,
BinaryStringEncoding.Utf8);
var writeSuccessful = await WriteBufferToSelectedCharacteristicAsync(writeBuffer);
}
else
{
rootPage.NotifyUser("No data to write to device", NotifyType.ErrorMessage);
}
}
private async void CharacteristicWriteButtonInt_Click()
{
if (!String.IsNullOrEmpty(CharacteristicWriteValue.Text))
{
var isValidValue = Int32.TryParse(CharacteristicWriteValue.Text, out int readValue);
if (isValidValue)
{
var writer = new DataWriter();
writer.ByteOrder = ByteOrder.LittleEndian;
writer.WriteInt32(readValue);
var writeSuccessful = await WriteBufferToSelectedCharacteristicAsync(writer.DetachBuffer());
}
else
{
rootPage.NotifyUser("Data to write has to be an int32", NotifyType.ErrorMessage);
}
}
else
{
rootPage.NotifyUser("No data to write to device", NotifyType.ErrorMessage);
}
}
private async Task<bool> WriteBufferToSelectedCharacteristicAsync(IBuffer buffer)
{
try
{
// BT_Code: Writes the value from the buffer to the characteristic.
var result = await selectedCharacteristic.WriteValueWithResultAsync(buffer);
if (result.Status == GattCommunicationStatus.Success)
{
rootPage.NotifyUser("Successfully wrote value to device", NotifyType.StatusMessage);
return true;
}
else
{
rootPage.NotifyUser($"Write failed: {result.Status}", NotifyType.ErrorMessage);
return false;
}
}
catch (Exception ex) when (ex.HResult == E_BLUETOOTH_ATT_INVALID_PDU)
{
rootPage.NotifyUser(ex.Message, NotifyType.ErrorMessage);
return false;
}
catch (Exception ex) when (ex.HResult == E_BLUETOOTH_ATT_WRITE_NOT_PERMITTED || ex.HResult == E_ACCESSDENIED)
{
// This usually happens when a device reports that it support writing, but it actually doesn't.
rootPage.NotifyUser(ex.Message, NotifyType.ErrorMessage);
return false;
}
}
private bool subscribedForNotifications = false;
private async void ValueChangedSubscribeToggle_Click()
{
if (!subscribedForNotifications)
{
// initialize status
GattCommunicationStatus status = GattCommunicationStatus.Unreachable;
var cccdValue = GattClientCharacteristicConfigurationDescriptorValue.None;
if (selectedCharacteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Indicate))
{
cccdValue = GattClientCharacteristicConfigurationDescriptorValue.Indicate;
}
else if (selectedCharacteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify))
{
cccdValue = GattClientCharacteristicConfigurationDescriptorValue.Notify;
}
try
{
// BT_Code: Must write the CCCD in order for server to send indications.
// We receive them in the ValueChanged event handler.
status = await selectedCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(cccdValue);
if (status == GattCommunicationStatus.Success)
{
AddValueChangedHandler();
rootPage.NotifyUser("Successfully subscribed for value changes", NotifyType.StatusMessage);
}
else
{
rootPage.NotifyUser($"Error registering for value changes: {status}", NotifyType.ErrorMessage);
}
}
catch (UnauthorizedAccessException ex)
{
// This usually happens when a device reports that it support indicate, but it actually doesn't.
rootPage.NotifyUser(ex.Message, NotifyType.ErrorMessage);
}
}
else
{
try
{
// BT_Code: Must write the CCCD in order for server to send notifications.
// We receive them in the ValueChanged event handler.
// Note that this sample configures either Indicate or Notify, but not both.
var result = await
selectedCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
GattClientCharacteristicConfigurationDescriptorValue.None);
if (result == GattCommunicationStatus.Success)
{
subscribedForNotifications = false;
RemoveValueChangedHandler();
rootPage.NotifyUser("Successfully un-registered for notifications", NotifyType.StatusMessage);
}
else
{
rootPage.NotifyUser($"Error un-registering for notifications: {result}", NotifyType.ErrorMessage);
}
}
catch (UnauthorizedAccessException ex)
{
// This usually happens when a device reports that it support notify, but it actually doesn't.
rootPage.NotifyUser(ex.Message, NotifyType.ErrorMessage);
}
}
}
private async void Characteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
{
// BT_Code: An Indicate or Notify reported that the value has changed.
// Display the new value with a timestamp.
var newValue = FormatValueByPresentation(args.CharacteristicValue, presentationFormat);
var message = $"Value at {DateTime.Now:hh:mm:ss.FFF}: {newValue}";
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
() => CharacteristicLatestValue.Text = message);
}
private string FormatValueByPresentation(IBuffer buffer, GattPresentationFormat format)
{
// BT_Code: For the purpose of this sample, this function converts only UInt32 and
// UTF-8 buffers to readable text. It can be extended to support other formats if your app needs them.
byte[] data;
CryptographicBuffer.CopyToByteArray(buffer, out data);
if (format != null)
{
if (format.FormatType == GattPresentationFormatTypes.UInt32 && data.Length >= 4)
{
return BitConverter.ToInt32(data, 0).ToString();
}
else if (format.FormatType == GattPresentationFormatTypes.Utf8)
{
try
{
return Encoding.UTF8.GetString(data);
}
catch (ArgumentException)
{
return "(error: Invalid UTF-8 string)";
}
}
else
{
// Add support for other format types as needed.
return "Unsupported format: " + CryptographicBuffer.EncodeToHexString(buffer);
}
}
else if (data != null)
{
// We don't know what format to use. Let's try some well-known profiles, or default back to UTF-8.
if (selectedCharacteristic.Uuid.Equals(GattCharacteristicUuids.HeartRateMeasurement))
{
try
{
return "Heart Rate: " + ParseHeartRateValue(data).ToString();
}
catch (ArgumentException)
{
return "Heart Rate: (unable to parse)";
}
}
else if (selectedCharacteristic.Uuid.Equals(GattCharacteristicUuids.BatteryLevel))
{
try
{
// battery level is encoded as a percentage value in the first byte according to
// https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.battery_level.xml
return "Battery Level: " + data[0].ToString() + "%";
}
catch (ArgumentException)
{
return "Battery Level: (unable to parse)";
}
}
// This is our custom calc service Result UUID. Format it like an Int
else if (selectedCharacteristic.Uuid.Equals(Constants.ResultCharacteristicUuid))
{
return BitConverter.ToInt32(data, 0).ToString();
}
// No guarantees on if a characteristic is registered for notifications.
else if (registeredCharacteristic != null)
{
// This is our custom calc service Result UUID. Format it like an Int
if (registeredCharacteristic.Uuid.Equals(Constants.ResultCharacteristicUuid))
{
return BitConverter.ToInt32(data, 0).ToString();
}
}
else
{
try
{
return "Unknown format: " + Encoding.UTF8.GetString(data);
}
catch (ArgumentException)
{
return "Unknown format";
}
}
}
else
{
return "Empty data received";
}
return "Unknown format";
}
/// <summary>
/// Process the raw data received from the device into application usable data,
/// according the the Bluetooth Heart Rate Profile.
/// https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.heart_rate_measurement.xml&u=org.bluetooth.characteristic.heart_rate_measurement.xml
/// This function throws an exception if the data cannot be parsed.
/// </summary>
/// <param name="data">Raw data received from the heart rate monitor.</param>
/// <returns>The heart rate measurement value.</returns>
private static ushort ParseHeartRateValue(byte[] data)
{
// Heart Rate profile defined flag values
const byte heartRateValueFormat = 0x01;
byte flags = data[0];
bool isHeartRateValueSizeLong = ((flags & heartRateValueFormat) != 0);
if (isHeartRateValueSizeLong)
{
return BitConverter.ToUInt16(data, 1);
}
else
{
return data[1];
}
}
}
}