diff --git a/Samples/BackgroundTransfer/cpp/Tasks/CompletionGroupTask.cpp b/Samples/BackgroundTransfer/cpp/Tasks/CompletionGroupTask.cpp index c58f14a48e..5af2fd8db4 100644 --- a/Samples/BackgroundTransfer/cpp/Tasks/CompletionGroupTask.cpp +++ b/Samples/BackgroundTransfer/cpp/Tasks/CompletionGroupTask.cpp @@ -104,6 +104,8 @@ BackgroundDownloader^ CompletionGroupTask::CreateBackgroundDownloader() builder->SetTrigger(completionGroup->Trigger); + // The system automatically unregisters the BackgroundTransferCompletionGroup task when it triggers. + // You do not need to unregister it explicitly. BackgroundTaskRegistration^ taskRegistration = builder->Register(); BackgroundDownloader^ downloader = ref new BackgroundDownloader(completionGroup); diff --git a/Samples/BackgroundTransfer/cs/Tasks/CompletionGroupTask.cs b/Samples/BackgroundTransfer/cs/Tasks/CompletionGroupTask.cs index 48375f1ded..8f0e8e9334 100644 --- a/Samples/BackgroundTransfer/cs/Tasks/CompletionGroupTask.cs +++ b/Samples/BackgroundTransfer/cs/Tasks/CompletionGroupTask.cs @@ -133,6 +133,8 @@ public static BackgroundDownloader CreateBackgroundDownloader() builder.TaskEntryPoint = "Tasks.CompletionGroupTask"; builder.SetTrigger(completionGroup.Trigger); + // The system automatically unregisters the BackgroundTransferCompletionGroup task when it triggers. + // You do not need to unregister it explicitly. BackgroundTaskRegistration taskRegistration = builder.Register(); BackgroundDownloader downloader = new BackgroundDownloader(completionGroup); diff --git a/Samples/BackgroundTransfer/js/js/completionGroupBackgroundTask.js b/Samples/BackgroundTransfer/js/js/completionGroupBackgroundTask.js index 1085830b79..fc0b1bb35a 100644 --- a/Samples/BackgroundTransfer/js/js/completionGroupBackgroundTask.js +++ b/Samples/BackgroundTransfer/js/js/completionGroupBackgroundTask.js @@ -73,6 +73,9 @@ var builder = new Windows.ApplicationModel.Background.BackgroundTaskBuilder(); builder.taskEntryPoint = taskEntryPoint; builder.setTrigger(completionGroup.trigger); + + // The system automatically unregisters the BackgroundTransferCompletionGroup task when it triggers. + // You do not need to unregister it explicitly. var taskRegistration = builder.register(); var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader(completionGroup); diff --git a/Samples/BackgroundTransfer/vb/Tasks/CompletionGroupTask.vb b/Samples/BackgroundTransfer/vb/Tasks/CompletionGroupTask.vb index fa9150b12a..5c6687e804 100644 --- a/Samples/BackgroundTransfer/vb/Tasks/CompletionGroupTask.vb +++ b/Samples/BackgroundTransfer/vb/Tasks/CompletionGroupTask.vb @@ -90,7 +90,11 @@ Namespace Global.Tasks Dim builder As BackgroundTaskBuilder = New BackgroundTaskBuilder() builder.TaskEntryPoint = "Tasks.CompletionGroupTask" builder.SetTrigger(completionGroup.Trigger) + + ' The system automatically unregisters the BackgroundTransferCompletionGroup task when it triggers. + ' You do not need to unregister it explicitly. Dim taskRegistration As BackgroundTaskRegistration = builder.Register() + Dim downloader As BackgroundDownloader = New BackgroundDownloader(completionGroup) Return downloader End Function diff --git a/Samples/BluetoothLE/cppwinrt/BluetoothLE.sln b/Samples/BluetoothLE/cppwinrt/BluetoothLE.sln new file mode 100644 index 0000000000..437df5dec5 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/BluetoothLE.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BluetoothLE", "BluetoothLE.vcxproj", "{6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM = Debug|ARM + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM = Release|ARM + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|ARM.ActiveCfg = Debug|ARM + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|ARM.Build.0 = Debug|ARM + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|ARM.Deploy.0 = Debug|ARM + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|x64.ActiveCfg = Debug|x64 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|x64.Build.0 = Debug|x64 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|x64.Deploy.0 = Debug|x64 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|x86.ActiveCfg = Debug|Win32 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|x86.Build.0 = Debug|Win32 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Debug|x86.Deploy.0 = Debug|Win32 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|ARM.ActiveCfg = Release|ARM + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|ARM.Build.0 = Release|ARM + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|ARM.Deploy.0 = Release|ARM + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|x64.ActiveCfg = Release|x64 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|x64.Build.0 = Release|x64 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|x64.Deploy.0 = Release|x64 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|x86.ActiveCfg = Release|Win32 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|x86.Build.0 = Release|Win32 + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849}.Release|x86.Deploy.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B7CCE8FC-0342-4D8C-AA55-C89A6673C44A} + EndGlobalSection +EndGlobal diff --git a/Samples/BluetoothLE/cppwinrt/BluetoothLE.vcxproj b/Samples/BluetoothLE/cppwinrt/BluetoothLE.vcxproj new file mode 100644 index 0000000000..8899abba75 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/BluetoothLE.vcxproj @@ -0,0 +1,211 @@ + + + + + true + {6BA13EC4-5ACC-4B1A-99B4-6FC4D67C7849} + BluetoothLE + SDKTemplate + en-US + 15.0 + true + Windows Store + 10.0 + 10.0.17763.0 + $(WindowsTargetPlatformVersion) + + + + + Debug + ARM + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + Win32 + + + Release + x64 + + + + Application + v141 + Unicode + + + true + true + + + false + true + false + + + + + + + + $(VC_IncludePath);$(UniversalCRT_IncludePath);$(WindowsSDK_IncludePath);..\..\..\SharedContent\cppwinrt + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + %(AdditionalOptions) /bigobj + 4453;28204 + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + + + + + ..\..\..\SharedContent\xaml\App.xaml + + + ..\..\..\SharedContent\xaml\MainPage.xaml + + + + + + + + ..\shared\Scenario1_Discovery.xaml + + + ..\shared\Scenario2_Client.xaml + + + ..\shared\Scenario3_ServerForeground.xaml + + + + + Designer + + + Designer + + + + + + Styles\Styles.xaml + + + + + ..\..\..\SharedContent\xaml\App.xaml + + + ..\..\..\SharedContent\xaml\MainPage.xaml + + + BluetoothLEAttributeDisplay.h + + + BluetoothLEDeviceDisplay.h + + + Create + pch.h + + + SampleConfiguration.h + + + ..\shared\Scenario1_Discovery.xaml + + + ..\shared\Scenario2_Client.xaml + + + ..\shared\Scenario3_ServerForeground.xaml + + + + + + ..\..\..\SharedContent\xaml\App.xaml + + + ..\..\..\SharedContent\xaml\MainPage.xaml + + + + ..\shared\Scenario1_Discovery.xaml + + + ..\shared\Scenario2_Client.xaml + + + ..\shared\Scenario3_ServerForeground.xaml + + + + + Designer + + + + + + Assets\microsoft-sdk.png + + + Assets\smallTile-sdk.png + + + Assets\splash-sdk.png + + + Assets\squareTile-sdk.png + + + Assets\storeLogo-sdk.png + + + Assets\tile-sdk.png + + + Assets\windows-sdk.png + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/Samples/BluetoothLE/cppwinrt/BluetoothLE.vcxproj.filters b/Samples/BluetoothLE/cppwinrt/BluetoothLE.vcxproj.filters new file mode 100644 index 0000000000..8333300f8d --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/BluetoothLE.vcxproj.filters @@ -0,0 +1,80 @@ + + + + + 4416d50a-7676-4d0a-9b2c-91ff70c6047f + bmp;fbx;gif;jpg;jpeg;tga;tiff;tif;png + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + + + + + + + \ No newline at end of file diff --git a/Samples/BluetoothLE/cppwinrt/BluetoothLEAttributeDisplay.cpp b/Samples/BluetoothLE/cppwinrt/BluetoothLEAttributeDisplay.cpp new file mode 100644 index 0000000000..82e6714737 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/BluetoothLEAttributeDisplay.cpp @@ -0,0 +1,223 @@ +#include "pch.h" +#include "BluetoothLEAttributeDisplay.h" + +using namespace winrt; +using namespace Windows::Devices::Bluetooth::GenericAttributeProfile; + +namespace +{ + /// + /// Determines whether the UUID was assigned by the Bluetooth SIG. + /// If so, extracts the assigned number. + /// + + bool TryParseSigDefinedUuid(guid const& uuid, uint16_t& shortId) + { + // UUIDs defined by the Bluetooth SIG are of the form + // 0000xxxx-0000-1000-8000-00805F9B34FB. + constexpr guid BluetoothGuid = { 0x00000000, 0x0000, 0x1000, { 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB } }; + + shortId = static_cast(uuid.Data1); + guid possibleBluetoothGuid = uuid; + possibleBluetoothGuid.Data1 &= 0xFFFF0000; + return possibleBluetoothGuid == BluetoothGuid; + } + + hstring GetGattServiceFriendlyName(guid const& uuid) + { + uint16_t shortId; + + if (TryParseSigDefinedUuid(uuid, shortId)) + { + // Reference: https://developer.bluetooth.org/gatt/services/Pages/ServicesHome.aspx + const static std::map knownServiceIds = + { + { 0x0000, L"None" }, + { 0x1811, L"AlertNotification" }, + { 0x180F, L"Battery" }, + { 0x1810, L"BloodPressure" }, + { 0x1805, L"CurrentTimeService" }, + { 0x1816, L"CyclingSpeedandCadence" }, + { 0x180A, L"DeviceInformation" }, + { 0x1800, L"GenericAccess" }, + { 0x1801, L"GenericAttribute" }, + { 0x1808, L"Glucose" }, + { 0x1809, L"HealthThermometer" }, + { 0x180D, L"HeartRate" }, + { 0x1812, L"HumanInterfaceDevice" }, + { 0x1802, L"ImmediateAlert" }, + { 0x1803, L"LinkLoss" }, + { 0x1807, L"NextDSTChange" }, + { 0x180E, L"PhoneAlertStatus" }, + { 0x1806, L"ReferenceTimeUpdateService" }, + { 0x1814, L"RunningSpeedandCadence" }, + { 0x1813, L"ScanParameters" }, + { 0x1804, L"TxPower" }, + { 0xFFE0, L"SimpleKeyService" }, + }; + auto it = knownServiceIds.find(shortId); + if (it != knownServiceIds.end()) + { + return it->second; + } + } + return L"Custom service: " + to_hstring(uuid); + } + + hstring GetGattCharacteristicFriendlyName(guid const& uuid, hstring const& userDescription) + { + uint16_t shortId; + + if (TryParseSigDefinedUuid(uuid, shortId)) + { + // Reference: https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicsHome.aspx + const static std::map knownCharacteristicIds = + { + { 0x0000, L"None" }, + { 0x2A43, L"AlertCategoryID" }, + { 0x2A42, L"AlertCategoryIDBitMask" }, + { 0x2A06, L"AlertLevel" }, + { 0x2A44, L"AlertNotificationControlPoint" }, + { 0x2A3F, L"AlertStatus" }, + { 0x2A01, L"Appearance" }, + { 0x2A19, L"BatteryLevel" }, + { 0x2A49, L"BloodPressureFeature" }, + { 0x2A35, L"BloodPressureMeasurement" }, + { 0x2A38, L"BodySensorLocation" }, + { 0x2A22, L"BootKeyboardInputReport" }, + { 0x2A32, L"BootKeyboardOutputReport" }, + { 0x2A33, L"BootMouseInputReport" }, + { 0x2A5C, L"CSCFeature" }, + { 0x2A5B, L"CSCMeasurement" }, + { 0x2A2B, L"CurrentTime" }, + { 0x2A08, L"DateTime" }, + { 0x2A0A, L"DayDateTime" }, + { 0x2A09, L"DayofWeek" }, + { 0x2A00, L"DeviceName" }, + { 0x2A0D, L"DSTOffset" }, + { 0x2A0C, L"ExactTime256" }, + { 0x2A26, L"FirmwareRevisionString" }, + { 0x2A51, L"GlucoseFeature" }, + { 0x2A18, L"GlucoseMeasurement" }, + { 0x2A34, L"GlucoseMeasurementContext" }, + { 0x2A27, L"HardwareRevisionString" }, + { 0x2A39, L"HeartRateControlPoint" }, + { 0x2A37, L"HeartRateMeasurement" }, + { 0x2A4C, L"HIDControlPoint" }, + { 0x2A4A, L"HIDInformation" }, + { 0x2A2A, L"IEEE11073_20601RegulatoryCertificationDataList" }, + { 0x2A36, L"IntermediateCuffPressure" }, + { 0x2A1E, L"IntermediateTemperature" }, + { 0x2A0F, L"LocalTimeInformation" }, + { 0x2A29, L"ManufacturerNameString" }, + { 0x2A21, L"MeasurementInterval" }, + { 0x2A24, L"ModelNumberString" }, + { 0x2A46, L"NewAlert" }, + { 0x2A04, L"PeripheralPreferredConnectionParameters" }, + { 0x2A02, L"PeripheralPrivacyFlag" }, + { 0x2A50, L"PnPID" }, + { 0x2A4E, L"ProtocolMode" }, + { 0x2A03, L"ReconnectionAddress" }, + { 0x2A52, L"RecordAccessControlPoint" }, + { 0x2A14, L"ReferenceTimeInformation" }, + { 0x2A4D, L"Report" }, + { 0x2A4B, L"ReportMap" }, + { 0x2A40, L"RingerControlPoint" }, + { 0x2A41, L"RingerSetting" }, + { 0x2A54, L"RSCFeature" }, + { 0x2A53, L"RSCMeasurement" }, + { 0x2A55, L"SCControlPoint" }, + { 0x2A4F, L"ScanIntervalWindow" }, + { 0x2A31, L"ScanRefresh" }, + { 0x2A5D, L"SensorLocation" }, + { 0x2A25, L"SerialNumberString" }, + { 0x2A05, L"ServiceChanged" }, + { 0x2A28, L"SoftwareRevisionString" }, + { 0x2A47, L"SupportedNewAlertCategory" }, + { 0x2A48, L"SupportedUnreadAlertCategory" }, + { 0x2A23, L"SystemID" }, + { 0x2A1C, L"TemperatureMeasurement" }, + { 0x2A1D, L"TemperatureType" }, + { 0x2A12, L"TimeAccuracy" }, + { 0x2A13, L"TimeSource" }, + { 0x2A16, L"TimeUpdateControlPoint" }, + { 0x2A17, L"TimeUpdateState" }, + { 0x2A11, L"TimewithDST" }, + { 0x2A0E, L"TimeZone" }, + { 0x2A07, L"TxPowerLevel" }, + { 0x2A45, L"UnreadAlertStatus" }, + { 0x2A5A, L"AggregateInput" }, + { 0x2A58, L"AnalogInput" }, + { 0x2A59, L"AnalogOutput" }, + { 0x2A66, L"CyclingPowerControlPoint" }, + { 0x2A65, L"CyclingPowerFeature" }, + { 0x2A63, L"CyclingPowerMeasurement" }, + { 0x2A64, L"CyclingPowerVector" }, + { 0x2A56, L"DigitalInput" }, + { 0x2A57, L"DigitalOutput" }, + { 0x2A0B, L"ExactTime100" }, + { 0x2A6B, L"LNControlPoint" }, + { 0x2A6A, L"LNFeature" }, + { 0x2A67, L"LocationandSpeed" }, + { 0x2A68, L"Navigation" }, + { 0x2A3E, L"NetworkAvailability" }, + { 0x2A69, L"PositionQuality" }, + { 0x2A3C, L"ScientificTemperatureinCelsius" }, + { 0x2A10, L"SecondaryTimeZone" }, + { 0x2A3D, L"String" }, + { 0x2A1F, L"TemperatureinCelsius" }, + { 0x2A20, L"TemperatureinFahrenheit" }, + { 0x2A15, L"TimeBroadcast" }, + { 0x2A1B, L"BatteryLevelState" }, + { 0x2A1A, L"BatteryPowerState" }, + { 0x2A5F, L"PulseOximetryContinuousMeasurement" }, + { 0x2A62, L"PulseOximetryControlPoint" }, + { 0x2A61, L"PulseOximetryFeatures" }, + { 0x2A60, L"PulseOximetryPulsatileEvent" }, + { 0xFFE1, L"SimpleKeyState" }, + }; + + auto it = knownCharacteristicIds.find(shortId); + if (it != knownCharacteristicIds.end()) + { + return it->second; + } + } + if (!userDescription.empty()) + { + return userDescription; + } + + return L"Custom Characteristic: " + to_hstring(uuid); + } +} + +namespace winrt::SDKTemplate::implementation +{ + hstring BluetoothLEAttributeDisplay::Name() + { + switch (m_attributeType) + { + case AttributeType::Service: + return GetGattServiceFriendlyName(m_service.Uuid()); + + case AttributeType::Characteristic: + return GetGattCharacteristicFriendlyName(m_characteristic.Uuid(), m_characteristic.UserDescription()); + } + return L"Invalid"; + } + + hstring BluetoothLEAttributeDisplay::AttributeDisplayType() + { + switch (m_attributeType) + { + case AttributeType::Service: + return L"Service"; + case AttributeType::Characteristic: + return L"Characteristic"; + case AttributeType::Descriptor: + return L"Descriptor"; + } + return L"Invalid"; + } +} diff --git a/Samples/BluetoothLE/cppwinrt/BluetoothLEAttributeDisplay.h b/Samples/BluetoothLE/cppwinrt/BluetoothLEAttributeDisplay.h new file mode 100644 index 0000000000..5b18b8a6b5 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/BluetoothLEAttributeDisplay.h @@ -0,0 +1,75 @@ +#pragma once + +#include "BluetoothLEAttributeDisplay.g.h" + +namespace winrt::SDKTemplate::implementation +{ + struct BluetoothLEAttributeDisplay : BluetoothLEAttributeDisplayT + { + BluetoothLEAttributeDisplay(Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService const& service) + : m_service(service), m_attributeType(SDKTemplate::AttributeType::Service) + { + } + + BluetoothLEAttributeDisplay(Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic const& characteristic) + : m_characteristic(characteristic), m_attributeType(SDKTemplate::AttributeType::Characteristic) + { + } + + Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic characteristic() + { + return m_characteristic; + } + + void characteristic(Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic const& value) + { + m_characteristic = value; + } + + Windows::Devices::Bluetooth::GenericAttributeProfile::GattDescriptor descriptor() + { + return m_descriptor; + } + + void descriptor(Windows::Devices::Bluetooth::GenericAttributeProfile::GattDescriptor const& value) + { + m_descriptor = value; + } + + Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService service() + { + return m_service; + } + + void service(Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService const& value) + { + m_service = value; + } + + hstring Name(); + hstring AttributeDisplayType(); + + static SDKTemplate::BluetoothLEAttributeDisplay CreateFromService(Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService const& service) + { + return BluetoothLEAttributeDisplay(service); + } + + static SDKTemplate::BluetoothLEAttributeDisplay CreateFromCharacteristic(Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic const& characteristic) + { + return BluetoothLEAttributeDisplay(characteristic); + } + + private: + Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic m_characteristic{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattDescriptor m_descriptor{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService m_service{ nullptr }; + SDKTemplate::AttributeType m_attributeType; + }; +} + +namespace winrt::SDKTemplate::factory_implementation +{ + struct BluetoothLEAttributeDisplay : BluetoothLEAttributeDisplayT + { + }; +} diff --git a/Samples/BluetoothLE/cppwinrt/BluetoothLEDeviceDisplay.cpp b/Samples/BluetoothLE/cppwinrt/BluetoothLEDeviceDisplay.cpp new file mode 100644 index 0000000000..e85140c457 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/BluetoothLEDeviceDisplay.cpp @@ -0,0 +1,63 @@ +#include "pch.h" +#include "BluetoothLEDeviceDisplay.h" + +using namespace winrt; +using namespace Windows::Foundation; +using namespace Windows::Devices::Enumeration; +using namespace Windows::UI::Xaml::Data; +using namespace Windows::UI::Xaml::Media::Imaging; + +namespace winrt::SDKTemplate::implementation +{ + BluetoothLEDeviceDisplay::BluetoothLEDeviceDisplay(Windows::Devices::Enumeration::DeviceInformation const& deviceInfoIn) + : m_deviceInformation(deviceInfoIn) + { + UpdateGlyphBitmapImage(); + } + + bool BluetoothLEDeviceDisplay::LookupBooleanProperty(param::hstring const& property) + { + auto value = m_deviceInformation.Properties().TryLookup(property); + return value && unbox_value(value); + } + + void BluetoothLEDeviceDisplay::Update(DeviceInformationUpdate const& deviceInfoUpdate) + { + m_deviceInformation.Update(deviceInfoUpdate); + + OnPropertyChanged(L"Id"); + OnPropertyChanged(L"Name"); + OnPropertyChanged(L"DeviceInformation"); + OnPropertyChanged(L"IsPaired"); + OnPropertyChanged(L"IsConnected"); + OnPropertyChanged(L"Properties"); + OnPropertyChanged(L"IsConnectable"); + + UpdateGlyphBitmapImage(); + } + + event_token BluetoothLEDeviceDisplay::PropertyChanged(PropertyChangedEventHandler const& handler) + { + return m_propertyChanged.add(handler); + } + + void BluetoothLEDeviceDisplay::PropertyChanged(event_token const& token) noexcept + { + m_propertyChanged.remove(token); + } + + void BluetoothLEDeviceDisplay::OnPropertyChanged(param::hstring const& property) + { + m_propertyChanged(*this, PropertyChangedEventArgs(property)); + } + + fire_and_forget BluetoothLEDeviceDisplay::UpdateGlyphBitmapImage() + { + auto lifetime = get_strong(); + DeviceThumbnail deviceThumbnail = co_await m_deviceInformation.GetGlyphThumbnailAsync(); + BitmapImage glyphBitmapImage; + co_await glyphBitmapImage.SetSourceAsync(deviceThumbnail); + m_glyphBitmapImage = glyphBitmapImage; + OnPropertyChanged(L"GlyphBitmapImage"); + } +} diff --git a/Samples/BluetoothLE/cppwinrt/BluetoothLEDeviceDisplay.h b/Samples/BluetoothLE/cppwinrt/BluetoothLEDeviceDisplay.h new file mode 100644 index 0000000000..21ac2c6c43 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/BluetoothLEDeviceDisplay.h @@ -0,0 +1,72 @@ +#pragma once + +#include "BluetoothLEDeviceDisplay.g.h" + +namespace winrt::SDKTemplate::implementation +{ + struct BluetoothLEDeviceDisplay : BluetoothLEDeviceDisplayT + { + BluetoothLEDeviceDisplay(Windows::Devices::Enumeration::DeviceInformation const& deviceInfoIn); + + Windows::Devices::Enumeration::DeviceInformation DeviceInformation() + { + return m_deviceInformation; + } + + hstring Id() + { + return m_deviceInformation.Id(); + } + + hstring Name() + { + return m_deviceInformation.Name(); + } + + bool IsPaired() + { + return m_deviceInformation.Pairing().IsPaired(); + } + + bool IsConnected() + { + return LookupBooleanProperty(L"System.Devices.Aep.IsConnected"); + } + + bool IsConnectable() + { + return LookupBooleanProperty(L"System.Devices.Aep.Bluetooth.Le.IsConnectable"); + } + + Windows::Foundation::Collections::IMapView Properties() + { + return m_deviceInformation.Properties(); + } + + Windows::UI::Xaml::Media::Imaging::BitmapImage GlyphBitmapImage() + { + return m_glyphBitmapImage; + } + + void Update(Windows::Devices::Enumeration::DeviceInformationUpdate const& deviceInfoUpdate); + + event_token PropertyChanged(Windows::UI::Xaml::Data::PropertyChangedEventHandler const& handler); + void PropertyChanged(event_token const& token) noexcept; + + private: + Windows::Devices::Enumeration::DeviceInformation m_deviceInformation{ nullptr }; + Windows::UI::Xaml::Media::Imaging::BitmapImage m_glyphBitmapImage{ nullptr }; + event m_propertyChanged; + + fire_and_forget UpdateGlyphBitmapImage(); + void OnPropertyChanged(param::hstring const& property); + bool LookupBooleanProperty(param::hstring const& property); + }; +} + +namespace winrt::SDKTemplate::factory_implementation +{ + struct BluetoothLEDeviceDisplay : BluetoothLEDeviceDisplayT + { + }; +} diff --git a/Samples/BluetoothLE/cppwinrt/DisplayHelpers.idl b/Samples/BluetoothLE/cppwinrt/DisplayHelpers.idl new file mode 100644 index 0000000000..f33fb5c2b6 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/DisplayHelpers.idl @@ -0,0 +1,57 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +namespace SDKTemplate +{ + enum AttributeType + { + Service = 0, + Characteristic = 1, + Descriptor = 2 + }; + + /// + /// Represents the display of an attribute - both characteristics and services. + /// + runtimeclass BluetoothLEAttributeDisplay + { + static BluetoothLEAttributeDisplay CreateFromService(Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService service); + static BluetoothLEAttributeDisplay CreateFromCharacteristic(Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristic characteristic); + + Windows.Devices.Bluetooth.GenericAttributeProfile.GattCharacteristic characteristic; + Windows.Devices.Bluetooth.GenericAttributeProfile.GattDescriptor descriptor; + Windows.Devices.Bluetooth.GenericAttributeProfile.GattDeviceService service; + String Name{ get; }; + String AttributeDisplayType{ get; }; + }; + + /// + /// Display class used to represent a BluetoothLEDevice in the Device list + /// + runtimeclass BluetoothLEDeviceDisplay : Windows.UI.Xaml.Data.INotifyPropertyChanged + { + BluetoothLEDeviceDisplay(Windows.Devices.Enumeration.DeviceInformation deviceInfoIn); + + Windows.Devices.Enumeration.DeviceInformation DeviceInformation{ get; }; + + String Id{ get; }; + String Name{ get; }; + Boolean IsPaired{ get; }; + Boolean IsConnected{ get; }; + Boolean IsConnectable{ get; }; + + IMapView Properties{ get; }; + + Windows.UI.Xaml.Media.Imaging.BitmapImage GlyphBitmapImage{ get; }; + + void Update(Windows.Devices.Enumeration.DeviceInformationUpdate deviceInfoUpdate); + }; +} diff --git a/Samples/BluetoothLE/cppwinrt/Package.appxmanifest b/Samples/BluetoothLE/cppwinrt/Package.appxmanifest new file mode 100644 index 0000000000..d9191f2864 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Package.appxmanifest @@ -0,0 +1,42 @@ + + + + + + BluetoothLE C++/WinRT Sample + Microsoft Corporation + Assets\storelogo-sdk.png + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/BluetoothLE/cppwinrt/PresentationFormats.h b/Samples/BluetoothLE/cppwinrt/PresentationFormats.h new file mode 100644 index 0000000000..701e13da4f --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/PresentationFormats.h @@ -0,0 +1,165 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#pragma once +#include "pch.h" + +namespace winrt::SDKTemplate +{ + struct PresentationFormats + { + /// + /// Units are established international standards for the measurement of physical quantities. + /// + /// Please refer https://www.bluetooth.com/specifications/assigned-numbers/units + enum class Units : uint16_t + { + Unitless = 0x2700, + LengthMetre = 0x2701, + MassKilogram = 0x2702, + TimeSecond = 0x2703, + ElectricCurrentAmpere = 0x2704, + ThermodynamicTemperatureKelvin = 0x2705, + AmountOfSubstanceMole = 0x2706, + LuminousIntensityCandela = 0x2707, + AreaSquareMetres = 0x2710, + VolumeCubicMetres = 0x2711, + VelocityMetresPerSecond = 0x2712, + AccelerationMetresPerSecondSquared = 0x2713, + WaveNumberReciprocalMetre = 0x2714, + DensityKilogramperCubicMetre = 0x2715, + SurfaceDensityKilogramPerSquareMetre = 0x2716, + SpecificVolumeCubicMetrePerKilogram = 0x2717, + CurrentDensityAmperePerSquareMetre = 0x2718, + MagneticFieldStrengthAmperePerMetre = 0x2719, + AmountConcentrationMolePerCubicMetre = 0x271A, + MassConcentrationKilogramPerCubicMetre = 0x271B, + LuminanceCandelaPerSquareMetre = 0x271C, + RefractiveIndex = 0x271D, + RelativePermeability = 0x271E, + PlaneAngleRadian = 0x2720, + SolidAngleSteradian = 0x2721, + FrequencyHertz = 0x2722, + ForceNewton = 0x2723, + PressurePascal = 0x2724, + EnergyJoule = 0x2725, + PowerWatt = 0x2726, + ElectricChargeCoulomb = 0x2727, + ElectricPotentialDifferenceVolt = 0x2728, + CapacitanceFarad = 0x2729, + ElectricResistanceOhm = 0x272A, + ElectricConductanceSiemens = 0x272B, + MagneticFluxWeber = 0x272C, + MagneticFluxDensityTesla = 0x272D, + InductanceHenry = 0x272E, + CelsiusTemperatureDegreeCelsius = 0x272F, + LuminousFluxLumen = 0x2730, + IlluminanceLux = 0x2731, + ActivityReferredToARadioNuclideBecquerel = 0x2732, + AbsorbedDoseGray = 0x2733, + DoseEquivalentSievert = 0x2734, + CatalyticActivityKatal = 0x2735, + DynamicViscosityPascalSecond = 0x2740, + MomentOfForceNewtonMetre = 0x2741, + SurfaceTensionNewtonPerMetre = 0x2742, + AngularVelocityRadianPerSecond = 0x2743, + AngularAccelerationRadianPerSecondSquared = 0x2744, + HeatFluxDensityWattPerSquareMetre = 0x2745, + HeatCapacityJoulePerKelvin = 0x2746, + SpecificHeatCapacityJoulePerKilogramKelvin = 0x2747, + SpecificEnergyJoulePerKilogram = 0x2748, + ThermalConductivityWattPerMetreKelvin = 0x2749, + EnergyDensityJoulePerCubicMetre = 0x274A, + ElectricfieldstrengthVoltPerMetre = 0x274B, + ElectricchargeDensityCoulombPerCubicMetre = 0x274C, + SurfacechargeDensityCoulombPerSquareMetre = 0x274D, + ElectricFluxDensityCoulombPerSquareMetre = 0x274E, + PermittivityFaradPerMetre = 0x274F, + PermeabilityHenryPerMetre = 0x2750, + MolarEnergyJoulePermole = 0x2751, + MolarentropyJoulePermoleKelvin = 0x2752, + ExposureCoulombPerKilogram = 0x2753, + AbsorbeddoserateGrayPerSecond = 0x2754, + RadiantintensityWattPerSteradian = 0x2755, + RadianceWattPerSquareMetreSteradian = 0x2756, + CatalyticActivityConcentrationKatalPerCubicMetre = 0x2757, + TimeMinute = 0x2760, + TimeHour = 0x2761, + TimeDay = 0x2762, + PlaneAngleDegree = 0x2763, + PlaneAngleMinute = 0x2764, + PlaneAngleSecond = 0x2765, + AreaHectare = 0x2766, + VolumeLitre = 0x2767, + MassTonne = 0x2768, + PressureBar = 0x2780, + PressureMilliMetreofmercury = 0x2781, + LengthAngstrom = 0x2782, + LengthNauticalMile = 0x2783, + AreaBarn = 0x2784, + VelocityKnot = 0x2785, + LogarithmicRadioQuantityNeper = 0x2786, + LogarithmicRadioQuantityBel = 0x2787, + LengthYard = 0x27A0, + LengthParsec = 0x27A1, + LengthInch = 0x27A2, + LengthFoot = 0x27A3, + LengthMile = 0x27A4, + PressurePoundForcePerSquareinch = 0x27A5, + VelocityKiloMetrePerHour = 0x27A6, + VelocityMilePerHour = 0x27A7, + AngularVelocityRevolutionPerminute = 0x27A8, + EnergyGramcalorie = 0x27A9, + EnergyKilogramcalorie = 0x27AA, + EnergyKiloWattHour = 0x27AB, + ThermodynamicTemperatureDegreeFahrenheit = 0x27AC, + Percentage = 0x27AD, + PerMille = 0x27AE, + PeriodBeatsPerMinute = 0x27AF, + ElectricchargeAmpereHours = 0x27B0, + MassDensityMilligramPerdeciLitre = 0x27B1, + MassDensityMillimolePerLitre = 0x27B2, + TimeYear = 0x27B3, + TimeMonth = 0x27B4, + ConcentrationCountPerCubicMetre = 0x27B5, + IrradianceWattPerSquareMetre = 0x27B6, + MilliliterPerKilogramPerminute = 0x27B7, + MassPound = 0x27B8, + }; + + /// + /// The Name Space field is used to identify the organization that is responsible for defining the enumerations for the description field. + /// + /// + /// Please refer https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.characteristic_presentation_format.xml + /// + enum class NamespaceId : uint8_t + { + BluetoothSigAssignedNumber = 1, + ReservedForFutureUse, + }; + + /// + /// The Description is an enumerated value from the organization identified by the Name Space field. + /// + /// + /// Please refer https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.characteristic_presentation_format.xml + /// + static constexpr uint16_t Description = 0x0000; + + /// + /// Exponent value for the characteristics + /// + static constexpr int Exponent = 0; + + }; +} + diff --git a/Samples/BluetoothLE/cppwinrt/SampleConfiguration.cpp b/Samples/BluetoothLE/cppwinrt/SampleConfiguration.cpp new file mode 100644 index 0000000000..5ffb6c1290 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/SampleConfiguration.cpp @@ -0,0 +1,43 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#include "pch.h" +#include +#include "MainPage.h" +#include "SampleConfiguration.h" + +using namespace winrt; +using namespace SDKTemplate; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Xaml; + +hstring implementation::MainPage::FEATURE_NAME() +{ + return L"BluetoothLE C++/WinRT Sample"; +} + +IVector implementation::MainPage::scenariosInner = winrt::single_threaded_observable_vector( +{ + Scenario{ L"Client: Discover servers", xaml_typename() }, + Scenario{ L"Client: Connect to a server", xaml_typename() }, + Scenario{ L"Server: Publish foreground", xaml_typename() }, +}); + +hstring SampleState::SelectedBleDeviceId; +hstring SampleState::SelectedBleDeviceName{ L"No device selected" }; + +Rect winrt::SDKTemplate::GetElementRect(FrameworkElement const& element) +{ + auto transform = element.TransformToVisual(nullptr); + Point point = transform.TransformPoint({}); + return { point, { static_cast(element.ActualWidth()), static_cast(element.ActualHeight()) } }; +} diff --git a/Samples/BluetoothLE/cppwinrt/SampleConfiguration.h b/Samples/BluetoothLE/cppwinrt/SampleConfiguration.h new file mode 100644 index 0000000000..4930533aff --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/SampleConfiguration.h @@ -0,0 +1,33 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#pragma once +#include "pch.h" + +namespace winrt::SDKTemplate +{ + struct Constants + { + static constexpr guid CalcServiceUuid{ 0xcaecface, 0xe1d9, 0x11e6, { 0xbf, 0x01, 0xfe, 0x55, 0x13, 0x50, 0x34, 0xf0 } }; // {caecface-e1d9-11e6-bf01-fe55135034f0} + static constexpr guid Op1CharacteristicUuid{ 0xcaecface, 0xe1d9, 0x11e6, { 0xbf, 0x01, 0xfe, 0x55, 0x13, 0x50, 0x34, 0xf1 } }; // {caecface-e1d9-11e6-bf01-fe55135034f1} + static constexpr guid Op2CharacteristicUuid{ 0xcaecface, 0xe1d9, 0x11e6, { 0xbf, 0x01, 0xfe, 0x55, 0x13, 0x50, 0x34, 0xf2 } }; // {caecface-e1d9-11e6-bf01-fe55135034f2} + static constexpr guid OperatorCharacteristicUuid{ 0xcaecface, 0xe1d9, 0x11e6, { 0xbf, 0x01, 0xfe, 0x55, 0x13, 0x50, 0x34, 0xf3 } }; // {caecface-e1d9-11e6-bf01-fe55135034f3} + static constexpr guid ResultCharacteristicUuid{ 0xcaecface, 0xe1d9, 0x11e6, { 0xbf, 0x01, 0xfe, 0x55, 0x13, 0x50, 0x34, 0xf4 } }; // {caecface-e1d9-11e6-bf01-fe55135034f4} + }; + + struct SampleState + { + static hstring SelectedBleDeviceId; + static hstring SelectedBleDeviceName; + }; + + Windows::Foundation::Rect GetElementRect(Windows::UI::Xaml::FrameworkElement const& element); +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.cpp b/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.cpp new file mode 100644 index 0000000000..d3eff7c080 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.cpp @@ -0,0 +1,321 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#include "pch.h" +#include "Scenario1_Discovery.h" +#include "SampleConfiguration.h" +#include "BluetoothLEDeviceDisplay.h" + +using namespace winrt; +using namespace Windows::Devices::Enumeration; +using namespace Windows::Foundation; +using namespace Windows::UI::Core; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Navigation; + +namespace winrt +{ + hstring to_hstring(DevicePairingResultStatus status) + { + switch (status) + { + case DevicePairingResultStatus::Paired: return L"Paired"; + case DevicePairingResultStatus::NotReadyToPair: return L"NotReadyToPair"; + case DevicePairingResultStatus::NotPaired: return L"NotPaired"; + case DevicePairingResultStatus::AlreadyPaired: return L"AlreadyPaired"; + case DevicePairingResultStatus::ConnectionRejected: return L"ConnectionRejected"; + case DevicePairingResultStatus::TooManyConnections: return L"TooManyConnections"; + case DevicePairingResultStatus::HardwareFailure: return L"HardwareFailure"; + case DevicePairingResultStatus::AuthenticationTimeout: return L"AuthenticationTimeout"; + case DevicePairingResultStatus::AuthenticationNotAllowed: return L"AuthenticationNotAllowed"; + case DevicePairingResultStatus::AuthenticationFailure: return L"AuthenticationFailure"; + case DevicePairingResultStatus::NoSupportedProfiles: return L"NoSupportedProfiles"; + case DevicePairingResultStatus::ProtectionLevelCouldNotBeMet: return L"ProtectionLevelCouldNotBeMet"; + case DevicePairingResultStatus::AccessDenied: return L"AccessDenied"; + case DevicePairingResultStatus::InvalidCeremonyData: return L"InvalidCeremonyData"; + case DevicePairingResultStatus::PairingCanceled: return L"PairingCanceled"; + case DevicePairingResultStatus::OperationAlreadyInProgress: return L"OperationAlreadyInProgress"; + case DevicePairingResultStatus::RequiredHandlerNotRegistered: return L"RequiredHandlerNotRegistered"; + case DevicePairingResultStatus::RejectedByHandler: return L"RejectedByHandler"; + case DevicePairingResultStatus::RemoteDeviceHasAssociation: return L"RemoteDeviceHasAssociation"; + case DevicePairingResultStatus::Failed: return L"Failed"; + } + return L"Code " + to_hstring(static_cast(status)); + } +} + +namespace winrt::SDKTemplate::implementation +{ + // This scenario uses a DeviceWatcher to enumerate nearby Bluetooth Low Energy devices, + // displays them in a ListView, and lets the user select a device and pair it. + // This device will be used by future scenarios. + // For more information about device discovery and pairing, including examples of + // customizing the pairing process, see the DeviceEnumerationAndPairing sample. + +#pragma region UI Code + + Scenario1_Discovery::Scenario1_Discovery() + { + InitializeComponent(); + } + + void Scenario1_Discovery::OnNavigatedFrom(NavigationEventArgs const&) + { + StopBleDeviceWatcher(); + + // Save the selected device's ID for use in other scenarios. + auto bleDeviceDisplay = ResultsListView().SelectedItem().as(); + if (bleDeviceDisplay != nullptr) + { + SampleState::SelectedBleDeviceId = bleDeviceDisplay.Id(); + SampleState::SelectedBleDeviceName = bleDeviceDisplay.Name(); + } + } + + void Scenario1_Discovery::EnumerateButton_Click() + { + if (deviceWatcher == nullptr) + { + StartBleDeviceWatcher(); + EnumerateButton().Content(box_value(L"Stop enumerating")); + rootPage.NotifyUser(L"Device watcher started.", NotifyType::StatusMessage); + } + else + { + StopBleDeviceWatcher(); + EnumerateButton().Content(box_value(L"Start enumerating")); + rootPage.NotifyUser(L"Device watcher stopped.", NotifyType::StatusMessage); + } + } +#pragma endregion + +#pragma region Device discovery + /// + /// Starts a device watcher that looks for all nearby Bluetooth devices (paired or unpaired). + /// Attaches event handlers to populate the device collection. + /// + void Scenario1_Discovery::StartBleDeviceWatcher() + { + // Additional properties we would like about the device. + // Property strings are documented here https://msdn.microsoft.com/en-us/library/windows/desktop/ff521659(v=vs.85).aspx + auto requestedProperties = single_threaded_vector({ L"System.Devices.Aep.DeviceAddress", L"System.Devices.Aep.IsConnected", L"System.Devices.Aep.Bluetooth.Le.IsConnectable" }); + + // BT_Code: Example showing paired and non-paired in a single query. + hstring aqsAllBluetoothLEDevices = L"(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\")"; + + deviceWatcher = + Windows::Devices::Enumeration::DeviceInformation::CreateWatcher( + aqsAllBluetoothLEDevices, + requestedProperties, + DeviceInformationKind::AssociationEndpoint); + + // Register event handlers before starting the watcher. + deviceWatcherAddedToken = deviceWatcher.Added({ get_weak(), &Scenario1_Discovery::DeviceWatcher_Added }); + deviceWatcherUpdatedToken = deviceWatcher.Updated({ get_weak(), &Scenario1_Discovery::DeviceWatcher_Updated }); + deviceWatcherRemovedToken = deviceWatcher.Removed({ get_weak(), &Scenario1_Discovery::DeviceWatcher_Removed }); + deviceWatcherEnumerationCompletedToken = deviceWatcher.EnumerationCompleted({ get_weak(), &Scenario1_Discovery::DeviceWatcher_EnumerationCompleted }); + deviceWatcherStoppedToken = deviceWatcher.Stopped({ get_weak(), &Scenario1_Discovery::DeviceWatcher_Stopped }); + + // Start over with an empty collection. + m_knownDevices.Clear(); + + // Start the watcher. Active enumeration is limited to approximately 30 seconds. + // This limits power usage and reduces interference with other Bluetooth activities. + // To monitor for the presence of Bluetooth LE devices for an extended period, + // use the BluetoothLEAdvertisementWatcher runtime class. See the BluetoothAdvertisement + // sample for an example. + deviceWatcher.Start(); + } + + /// + /// Stops watching for all nearby Bluetooth devices. + /// + void Scenario1_Discovery::StopBleDeviceWatcher() + { + if (deviceWatcher != nullptr) + { + // Unregister the event handlers. + deviceWatcher.Added(deviceWatcherAddedToken); + deviceWatcher.Updated(deviceWatcherUpdatedToken); + deviceWatcher.Removed(deviceWatcherRemovedToken); + deviceWatcher.EnumerationCompleted(deviceWatcherEnumerationCompletedToken); + deviceWatcher.Stopped(deviceWatcherStoppedToken); + + // Stop the watcher. + deviceWatcher.Stop(); + deviceWatcher = nullptr; + } + } + + std::tuple Scenario1_Discovery::FindBluetoothLEDeviceDisplay(hstring const& id) + { + uint32_t size = m_knownDevices.Size(); + for (uint32_t index = 0; index < size; index++) + { + auto bleDeviceDisplay = m_knownDevices.GetAt(index).as(); + if (bleDeviceDisplay.Id() == id) + { + return { bleDeviceDisplay, index }; + } + } + return { nullptr, 0-1U }; + } + + std::vector::iterator Scenario1_Discovery::FindUnknownDevices(hstring const& id) + { + return std::find_if(UnknownDevices.begin(), UnknownDevices.end(), [&](auto&& bleDeviceInfo) + { + return bleDeviceInfo.Id() == id; + }); + } + + fire_and_forget Scenario1_Discovery::DeviceWatcher_Added(DeviceWatcher sender, DeviceInformation deviceInfo) + { + // We must update the collection on the UI thread because the collection is databound to a UI element. + auto lifetime = get_strong(); + co_await resume_foreground(Dispatcher()); + + OutputDebugStringW((L"Added " + deviceInfo.Id() + deviceInfo.Name()).c_str()); + + // Protect against race condition if the task runs after the app stopped the deviceWatcher. + if (sender == deviceWatcher) + { + // Make sure device isn't already present in the list. + if (std::get<0>(FindBluetoothLEDeviceDisplay(deviceInfo.Id())) == nullptr) + { + if (!deviceInfo.Name().empty()) + { + // If device has a friendly name display it immediately. + m_knownDevices.Append(make(deviceInfo)); + } + else + { + // Add it to a list in case the name gets updated later. + UnknownDevices.push_back(deviceInfo); + } + } + } + } + + fire_and_forget Scenario1_Discovery::DeviceWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate deviceInfoUpdate) + { + // We must update the collection on the UI thread because the collection is databound to a UI element. + auto lifetime = get_strong(); + co_await resume_foreground(Dispatcher()); + + OutputDebugStringW((L"Updated " + deviceInfoUpdate.Id()).c_str()); + + // Protect against race condition if the task runs after the app stopped the deviceWatcher. + if (sender == deviceWatcher) + { + SDKTemplate::BluetoothLEDeviceDisplay bleDeviceDisplay = std::get<0>(FindBluetoothLEDeviceDisplay(deviceInfoUpdate.Id())); + if (bleDeviceDisplay != nullptr) + { + // Device is already being displayed - update UX. + bleDeviceDisplay.Update(deviceInfoUpdate); + co_return; + } + + auto deviceInfo = FindUnknownDevices(deviceInfoUpdate.Id()); + if (deviceInfo != UnknownDevices.end()) + { + deviceInfo->Update(deviceInfoUpdate); + // If device has been updated with a friendly name it's no longer unknown. + if (!deviceInfo->Name().empty()) + { + m_knownDevices.Append(make(*deviceInfo)); + UnknownDevices.erase(deviceInfo); + } + } + } + } + + fire_and_forget Scenario1_Discovery::DeviceWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate deviceInfoUpdate) + { + // We must update the collection on the UI thread because the collection is databound to a UI element. + auto lifetime = get_strong(); + co_await resume_foreground(Dispatcher()); + + OutputDebugStringW((L"Removed " + deviceInfoUpdate.Id()).c_str()); + + // Protect against race condition if the task runs after the app stopped the deviceWatcher. + if (sender == deviceWatcher) + { + // Find the corresponding DeviceInformation in the collection and remove it. + auto[bleDeviceDisplay, index] = FindBluetoothLEDeviceDisplay(deviceInfoUpdate.Id()); + if (bleDeviceDisplay != nullptr) + { + m_knownDevices.RemoveAt(index); + } + + auto deviceInfo = FindUnknownDevices(deviceInfoUpdate.Id()); + if (deviceInfo != UnknownDevices.end()) + { + UnknownDevices.erase(deviceInfo); + } + } + } + + fire_and_forget Scenario1_Discovery::DeviceWatcher_EnumerationCompleted(DeviceWatcher sender, IInspectable const&) + { + // Access this->deviceWatcher on the UI thread to avoid race conditions. + auto lifetime = get_strong(); + co_await resume_foreground(Dispatcher()); + + // Protect against race condition if the task runs after the app stopped the deviceWatcher. + if (sender == deviceWatcher) + { + rootPage.NotifyUser(to_hstring(m_knownDevices.Size()) + L" devices found. Enumeration completed.", + NotifyType::StatusMessage); + } + } + + fire_and_forget Scenario1_Discovery::DeviceWatcher_Stopped(DeviceWatcher sender, IInspectable const&) + { + // Access this->deviceWatcher on the UI thread to avoid race conditions. + auto lifetime = get_strong(); + co_await resume_foreground(Dispatcher()); + + // Protect against race condition if the task runs after the app stopped the deviceWatcher. + if (sender == deviceWatcher) + { + rootPage.NotifyUser(L"No longer watching for devices.", + sender.Status() == DeviceWatcherStatus::Aborted ? NotifyType::ErrorMessage : NotifyType::StatusMessage); + } + } +#pragma endregion + +#pragma region Pairing + fire_and_forget Scenario1_Discovery::PairButton_Click() + { + auto lifetime = get_strong(); + EnumerateButton().IsEnabled(false); + + rootPage.NotifyUser(L"Pairing started. Please wait...", NotifyType::StatusMessage); + + // For more information about device pairing, including examples of + // customizing the pairing process, see the DeviceEnumerationAndPairing sample. + + // Capture the current selected item in case the user changes it while we are pairing. + auto bleDeviceDisplay = ResultsListView().SelectedItem().as(); + + // BT_Code: Pair the currently selected device. + DevicePairingResult result = co_await bleDeviceDisplay.DeviceInformation().Pairing().PairAsync(); + DevicePairingResultStatus status = result.Status(); + rootPage.NotifyUser(L"Pairing result = " + to_hstring(status), + status == DevicePairingResultStatus::Paired || status == DevicePairingResultStatus::AlreadyPaired + ? NotifyType::StatusMessage + : NotifyType::ErrorMessage); + + EnumerateButton().IsEnabled(true); + } +#pragma endregion +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.h b/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.h new file mode 100644 index 0000000000..aa1084260a --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.h @@ -0,0 +1,63 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#pragma once + +#include "Scenario1_Discovery.g.h" +#include "MainPage.h" + +namespace winrt::SDKTemplate::implementation +{ + struct Scenario1_Discovery : Scenario1_DiscoveryT + { + Scenario1_Discovery(); + + void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs const& e); + + Windows::Foundation::Collections::IObservableVector KnownDevices() + { + return m_knownDevices; + } + + void EnumerateButton_Click(); + fire_and_forget PairButton_Click(); + bool Not(bool value) { return !value; } + + private: + SDKTemplate::MainPage rootPage{ SDKTemplate::implementation::MainPage::Current() }; + Windows::Foundation::Collections::IObservableVector m_knownDevices = single_threaded_observable_vector(); + std::vector UnknownDevices; + Windows::Devices::Enumeration::DeviceWatcher deviceWatcher{ nullptr }; + event_token deviceWatcherAddedToken; + event_token deviceWatcherUpdatedToken; + event_token deviceWatcherRemovedToken; + event_token deviceWatcherEnumerationCompletedToken; + event_token deviceWatcherStoppedToken; + + void StartBleDeviceWatcher(); + void StopBleDeviceWatcher(); + std::tuple FindBluetoothLEDeviceDisplay(hstring const& id); + std::vector::iterator FindUnknownDevices(hstring const& id); + + fire_and_forget DeviceWatcher_Added(Windows::Devices::Enumeration::DeviceWatcher sender, Windows::Devices::Enumeration::DeviceInformation deviceInfo); + fire_and_forget DeviceWatcher_Updated(Windows::Devices::Enumeration::DeviceWatcher sender, Windows::Devices::Enumeration::DeviceInformationUpdate deviceInfoUpdate); + fire_and_forget DeviceWatcher_Removed(Windows::Devices::Enumeration::DeviceWatcher sender, Windows::Devices::Enumeration::DeviceInformationUpdate deviceInfoUpdate); + fire_and_forget DeviceWatcher_EnumerationCompleted(Windows::Devices::Enumeration::DeviceWatcher sender, Windows::Foundation::IInspectable const&); + fire_and_forget DeviceWatcher_Stopped(Windows::Devices::Enumeration::DeviceWatcher sender, Windows::Foundation::IInspectable const&); + }; +} + +namespace winrt::SDKTemplate::factory_implementation +{ + struct Scenario1_Discovery : Scenario1_DiscoveryT + { + }; +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.idl b/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.idl new file mode 100644 index 0000000000..c64c88eb56 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario1_Discovery.idl @@ -0,0 +1,26 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +namespace SDKTemplate +{ + [default_interface] + runtimeclass Scenario1_Discovery : Windows.UI.Xaml.Controls.Page + { + Scenario1_Discovery(); + + Windows.Foundation.Collections.IObservableVector KnownDevices{ get; }; + + Windows.UI.Xaml.Controls.ListView ResultsListView{ get; }; + void EnumerateButton_Click(); + void PairButton_Click(); + Boolean Not(Boolean value); + } +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario2_Client.cpp b/Samples/BluetoothLE/cppwinrt/Scenario2_Client.cpp new file mode 100644 index 0000000000..f7dee725aa --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario2_Client.cpp @@ -0,0 +1,657 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#include "pch.h" +#include "Scenario2_Client.h" +#include "SampleConfiguration.h" +#include "BluetoothLEAttributeDisplay.h" + +using namespace winrt; +using namespace Windows::Devices::Bluetooth; +using namespace Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace Windows::Devices::Enumeration; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Globalization; +using namespace Windows::Security::Cryptography; +using namespace Windows::Storage::Streams; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Navigation; + +namespace +{ + void SetVisibility(UIElement const& element, bool visible) + { + element.Visibility(visible ? Visibility::Visible : Visibility::Collapsed); + } + + // Utility function to convert a string to an int32_t and detect bad input + bool TryParseInt(const wchar_t* str, int32_t& result) + { + wchar_t* end; + errno = 0; + result = std::wcstol(str, &end, 0); + + if (str == end) + { + // Not parseable. + return false; + } + + if (errno == ERANGE || result < INT_MIN || INT_MAX < result) + { + // Out of range. + return false; + } + + if (*end != L'\0') + { + // Extra unparseable characters at the end. + return false; + } + + return true; + } + + template + T Read(DataReader const& reader); + + template<> + uint32_t Read(DataReader const& reader) + { + return reader.ReadUInt32(); + } + + template<> + int32_t Read(DataReader const& reader) + { + return reader.ReadInt32(); + } + + template<> + uint8_t Read(DataReader const& reader) + { + return reader.ReadByte(); + } + + template + bool TryExtract(IBuffer const& buffer, T& result) + { + if (buffer.Length() >= sizeof(T)) + { + DataReader reader = DataReader::FromBuffer(buffer); + result = Read(reader); + return true; + } + return false; + } +} + +namespace winrt +{ + hstring to_hstring(GattCommunicationStatus status) + { + switch (status) + { + case GattCommunicationStatus::Success: return L"Success"; + case GattCommunicationStatus::Unreachable: return L"Unreachable"; + case GattCommunicationStatus::ProtocolError: return L"ProtocolError"; + case GattCommunicationStatus::AccessDenied: return L"AccessDenied"; + } + return to_hstring(static_cast(status)); + } +} + +namespace winrt::SDKTemplate::implementation +{ +#pragma region UI Code + Scenario2_Client::Scenario2_Client() + { + InitializeComponent(); + } + + void Scenario2_Client::OnNavigatedTo(NavigationEventArgs const&) + { + SelectedDeviceRun().Text(SampleState::SelectedBleDeviceName); + if (SampleState::SelectedBleDeviceId.empty()) + { + ConnectButton().IsEnabled(false); + } + } + + fire_and_forget Scenario2_Client::OnNavigatedFrom(NavigationEventArgs const&) + { + auto lifetime = get_strong(); + if (!co_await ClearBluetoothLEDeviceAsync()) + { + rootPage.NotifyUser(L"Error: Unable to reset app state", NotifyType::ErrorMessage); + } + } +#pragma endregion + +#pragma region Enumerating Services + IAsyncOperation Scenario2_Client::ClearBluetoothLEDeviceAsync() + { + auto lifetime = get_strong(); + + if (notificationsToken) + { + // Need to clear the CCCD from the remote device so we stop receiving notifications + GattCommunicationStatus result = co_await registeredCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::None); + if (result != GattCommunicationStatus::Success) + { + co_return false; + } + else + { + selectedCharacteristic.ValueChanged(std::exchange(notificationsToken, {})); + } + } + + if (bluetoothLeDevice != nullptr) + { + bluetoothLeDevice.Close(); + bluetoothLeDevice = nullptr; + } + co_return true; + } + + fire_and_forget Scenario2_Client::ConnectButton_Click() + { + auto lifetime = get_strong(); + + ConnectButton().IsEnabled(false); + + if (!co_await ClearBluetoothLEDeviceAsync()) + { + rootPage.NotifyUser(L"Error: Unable to reset state, try again.", NotifyType::ErrorMessage); + ConnectButton().IsEnabled(true); + co_return; + } + + try + { + // BT_Code: BluetoothLEDevice.FromIdAsync must be called from a UI thread because it may prompt for consent. + bluetoothLeDevice = co_await BluetoothLEDevice::FromIdAsync(SampleState::SelectedBleDeviceId); + + if (bluetoothLeDevice == nullptr) + { + rootPage.NotifyUser(L"Failed to connect to device.", NotifyType::ErrorMessage); + } + } + catch (hresult_error& ex) + { + if (ex.to_abi() == HRESULT_FROM_WIN32(ERROR_DEVICE_NOT_AVAILABLE)) + { + rootPage.NotifyUser(L"Bluetooth radio is not on.", NotifyType::ErrorMessage); + } + else + { + throw; + } + } + + if (bluetoothLeDevice != nullptr) + { + // 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 = co_await bluetoothLeDevice.GetGattServicesAsync(BluetoothCacheMode::Uncached); + + if (result.Status() == GattCommunicationStatus::Success) + { + IVectorView services = result.Services(); + rootPage.NotifyUser(L"Found " + to_hstring(services.Size()) + L" services", NotifyType::StatusMessage); + for (auto&& service : services) + { + m_serviceCollection.Append(make(service)); + } + ConnectButton().Visibility(Visibility::Collapsed); + ServiceList().Visibility(Visibility::Visible); + } + else + { + rootPage.NotifyUser(L"Device unreachable", NotifyType::ErrorMessage); + } + } + ConnectButton().IsEnabled(true); + } + +#pragma region Enumerating Characteristics + fire_and_forget Scenario2_Client::ServiceList_SelectionChanged() + { + auto lifetime = get_strong(); + + auto attributeInfoDisp = ServiceList().SelectedItem().as(); + + m_characteristicCollection.Clear(); + RemoveValueChangedHandler(); + + IVectorView characteristics{ nullptr }; + try + { + // Ensure we have access to the device. + auto accessStatus = co_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. + GattCharacteristicsResult result = co_await attributeInfoDisp.service().GetCharacteristicsAsync(BluetoothCacheMode::Uncached); + if (result.Status() == GattCommunicationStatus::Success) + { + characteristics = result.Characteristics(); + } + else + { + rootPage.NotifyUser(L"Error accessing service.", NotifyType::ErrorMessage); + } + } + else + { + // Not granted access + rootPage.NotifyUser(L"Error accessing service.", NotifyType::ErrorMessage); + + } + } + catch (hresult_error& ex) + { + rootPage.NotifyUser(L"Restricted service. Can't read characteristics: " + ex.message(), NotifyType::ErrorMessage); + } + + if (characteristics) + { + for (GattCharacteristic&& c : characteristics) + { + m_characteristicCollection.Append(make(c)); + } + } + CharacteristicList().Visibility(Visibility::Visible); + } +#pragma endregion + + void Scenario2_Client::AddValueChangedHandler() + { + ValueChangedSubscribeToggle().Content(box_value(L"Unsubscribe from value changes")); + if (!notificationsToken) + { + registeredCharacteristic = selectedCharacteristic; + notificationsToken = registeredCharacteristic.ValueChanged({ get_weak(), &Scenario2_Client::Characteristic_ValueChanged }); + } + } + void Scenario2_Client::RemoveValueChangedHandler() + { + ValueChangedSubscribeToggle().Content(box_value(L"Subscribe to value changes")); + if (notificationsToken) + { + registeredCharacteristic.ValueChanged(std::exchange(notificationsToken, {})); + registeredCharacteristic = nullptr; + } + } + + fire_and_forget Scenario2_Client::CharacteristicList_SelectionChanged() + { + auto lifetime = get_strong(); + + selectedCharacteristic = nullptr; + + auto attributeInfoDisp = CharacteristicList().SelectedItem().as(); + if (attributeInfoDisp == nullptr) + { + EnableCharacteristicPanels(GattCharacteristicProperties::None); + co_return; + } + + selectedCharacteristic = attributeInfoDisp.characteristic(); + if (selectedCharacteristic == nullptr) + { + rootPage.NotifyUser(L"No characteristic selected", NotifyType::ErrorMessage); + co_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. + GattDescriptorsResult result = co_await selectedCharacteristic.GetDescriptorsAsync(BluetoothCacheMode::Uncached); + if (result.Status() != GattCommunicationStatus::Success) + { + rootPage.NotifyUser(L"Descriptor read failure: " + to_hstring(result.Status()), NotifyType::ErrorMessage); + } + + // BT_Code: There's no need to access presentation format unless there's at least one. + presentationFormat = nullptr; + if (selectedCharacteristic.PresentationFormats().Size() > 0) + { + + if (selectedCharacteristic.PresentationFormats().Size() == 1) + { + // Get the presentation format since there's only one way of presenting it + presentationFormat = selectedCharacteristic.PresentationFormats().GetAt(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()); + } + + void Scenario2_Client::EnableCharacteristicPanels(GattCharacteristicProperties properties) + { + // BT_Code: Hide the controls which do not apply to this characteristic. + SetVisibility(CharacteristicReadButton(), + (properties & GattCharacteristicProperties::Read) != GattCharacteristicProperties::None); + + SetVisibility(CharacteristicWritePanel(), + (properties & (GattCharacteristicProperties::Write | GattCharacteristicProperties::WriteWithoutResponse)) != GattCharacteristicProperties::None); + CharacteristicWriteValue().Text(L""); + + SetVisibility(ValueChangedSubscribeToggle(), + (properties & (GattCharacteristicProperties::Indicate | GattCharacteristicProperties::Notify)) != GattCharacteristicProperties::None); + } + + fire_and_forget Scenario2_Client::CharacteristicReadButton_Click() + { + auto lifetime = get_strong(); + + // BT_Code: Read the actual value from the device by using Uncached. + GattReadResult result = co_await selectedCharacteristic.ReadValueAsync(BluetoothCacheMode::Uncached); + if (result.Status() == GattCommunicationStatus::Success) + { + rootPage.NotifyUser(L"Read result: " + FormatValueByPresentation(result.Value(), presentationFormat), NotifyType::StatusMessage); + } + else + { + rootPage.NotifyUser(L"Read failed: Status = " + to_hstring(result.Status()), NotifyType::ErrorMessage); + } + } + + fire_and_forget Scenario2_Client::CharacteristicWriteButton_Click() + { + auto lifetime = get_strong(); + + hstring text = CharacteristicWriteValue().Text(); + if (!text.empty()) + { + IBuffer writeBuffer = CryptographicBuffer::ConvertStringToBinary(CharacteristicWriteValue().Text(), BinaryStringEncoding::Utf8); + + co_await WriteBufferToSelectedCharacteristicAsync(writeBuffer); + } + else + { + rootPage.NotifyUser(L"No data to write to device", NotifyType::ErrorMessage); + } + } + + fire_and_forget Scenario2_Client::CharacteristicWriteButtonInt_Click() + { + auto lifetime = get_strong(); + + int32_t readValue; + if (TryParseInt(CharacteristicWriteValue().Text().begin(), readValue)) + { + DataWriter writer; + writer.ByteOrder(ByteOrder::LittleEndian); + writer.WriteInt32(readValue); + + co_await WriteBufferToSelectedCharacteristicAsync(writer.DetachBuffer()); + } + else + { + rootPage.NotifyUser(L"Data to write has to be an int32", NotifyType::ErrorMessage); + } + } + + IAsyncOperation Scenario2_Client::WriteBufferToSelectedCharacteristicAsync(IBuffer buffer) + { + auto lifetime = get_strong(); + + try + { + // BT_Code: Writes the value from the buffer to the characteristic. + GattWriteResult result = co_await selectedCharacteristic.WriteValueWithResultAsync(buffer); + + if (result.Status() == GattCommunicationStatus::Success) + { + rootPage.NotifyUser(L"Successfully wrote value to device", NotifyType::StatusMessage); + co_return true; + } + else + { + rootPage.NotifyUser(L"Write failed: Status = " + to_hstring(result.Status()), NotifyType::ErrorMessage); + co_return false; + } + } + catch (hresult_error& ex) + { + if (ex.code() == E_BLUETOOTH_ATT_INVALID_PDU) + { + rootPage.NotifyUser(ex.message(), NotifyType::ErrorMessage); + co_return false; + } + if (ex.code() == E_BLUETOOTH_ATT_WRITE_NOT_PERMITTED || ex.code() == E_ACCESSDENIED) + { + // This usually happens when a device reports that it support writing, but it actually doesn't. + rootPage.NotifyUser(ex.message(), NotifyType::ErrorMessage); + co_return false; + } + throw; + } + } + + fire_and_forget Scenario2_Client::ValueChangedSubscribeToggle_Click() + { + auto lifetime = get_strong(); + + if (!notificationsToken) + { + GattClientCharacteristicConfigurationDescriptorValue cccdValue = GattClientCharacteristicConfigurationDescriptorValue::None; + if ((selectedCharacteristic.CharacteristicProperties() & GattCharacteristicProperties::Indicate) != GattCharacteristicProperties::None) + { + cccdValue = GattClientCharacteristicConfigurationDescriptorValue::Indicate; + } + + else if ((selectedCharacteristic.CharacteristicProperties() & GattCharacteristicProperties::Notify) != GattCharacteristicProperties::None) + { + 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. + GattCommunicationStatus status = co_await selectedCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(cccdValue); + + if (status == GattCommunicationStatus::Success) + { + AddValueChangedHandler(); + rootPage.NotifyUser(L"Successfully subscribed for value changes", NotifyType::StatusMessage); + } + else + { + rootPage.NotifyUser(L"Error registering for value changes: Status = " + to_hstring(status), NotifyType::ErrorMessage); + } + } + catch (hresult_access_denied& 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. + GattCommunicationStatus result = co_await + selectedCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync( + GattClientCharacteristicConfigurationDescriptorValue::None); + if (result == GattCommunicationStatus::Success) + { + RemoveValueChangedHandler(); + rootPage.NotifyUser(L"Successfully un-registered for notifications", NotifyType::StatusMessage); + } + else + { + rootPage.NotifyUser(L"Error un-registering for notifications: Status = " + to_hstring(result), NotifyType::ErrorMessage); + } + } + catch (hresult_access_denied& ex) + { + // This usually happens when a device reports that it support notify, but it actually doesn't. + rootPage.NotifyUser(ex.message(), NotifyType::ErrorMessage); + } + } + } + + fire_and_forget Scenario2_Client::Characteristic_ValueChanged(GattCharacteristic const&, GattValueChangedEventArgs args) + { + auto lifetime = get_strong(); + + // BT_Code: An Indicate or Notify reported that the value has changed. + // Display the new value with a timestamp. + hstring newValue = FormatValueByPresentation(args.CharacteristicValue(), presentationFormat); + std::time_t now = clock::to_time_t(clock::now()); + char buffer[26]; + ctime_s(buffer, ARRAYSIZE(buffer), &now); + hstring message = L"Value at " + to_hstring(buffer) + L": " + newValue; + co_await resume_foreground(Dispatcher()); + CharacteristicLatestValue().Text(message); + } + + /// + /// 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 std::out_of_range if the data cannot be parsed. + /// + /// Raw data received from the heart rate monitor. + /// The heart rate measurement value. + int32_t ParseHeartRateValue(IBuffer const& buffer) + { + // Heart Rate profile defined flag values + const uint8_t heartRateValueFormat = 0x1; + + com_array bytes; + CryptographicBuffer::CopyToByteArray(buffer, bytes); + + if (bytes.at(0) & heartRateValueFormat) + { + return bytes.at(1) | (bytes.at(2) << 8); + } + return bytes.at(1); + } + + hstring Scenario2_Client::FormatValueByPresentation(IBuffer const& buffer, GenericAttributeProfile::GattPresentationFormat const& 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. + if (format != nullptr) + { + if (format.FormatType() == GattPresentationFormatTypes::UInt32()) + { + uint32_t value; + if (TryExtract(buffer, value)) + { + return to_hstring(value); + } + return L"(error: Invalid UInt32)"; + + } + else if (format.FormatType() == GattPresentationFormatTypes::Utf8()) + { + try + { + return CryptographicBuffer::ConvertBinaryToString(BinaryStringEncoding::Utf8, buffer); + } + catch (hresult_invalid_argument&) + { + return L"(error: Invalid UTF-8 string)"; + } + } + else + { + // Add support for other format types as needed. + return L"Unsupported format: " + CryptographicBuffer::EncodeToHexString(buffer); + } + } + else if (buffer != nullptr) + { + // We don't know what format to use. Let's try some well-known profiles, or default back to UTF-8. + if (selectedCharacteristic.Uuid() == GattCharacteristicUuids::HeartRateMeasurement()) + { + try + { + return L"Heart Rate: " + to_hstring(ParseHeartRateValue(buffer)); + } + catch (std::out_of_range&) + { + return L"Heart Rate: (unable to parse)"; + } + } + else if (selectedCharacteristic.Uuid() == GattCharacteristicUuids::BatteryLevel()) + { + // 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 + uint8_t percent; + if (TryExtract(buffer, percent)) + { + return L"Battery Level: " + to_hstring(percent) + L"%"; + } + return L"Battery Level: (unable to parse)"; + } + // This is our custom calc service Result UUID. Format it like an Int + else if (selectedCharacteristic.Uuid() == Constants::ResultCharacteristicUuid) + { + int32_t value; + if (TryExtract(buffer, value)) + { + return to_hstring(value); + } + return L"Invalid response from calc server"; + } + // No guarantees on if a characteristic is registered for notifications. + else if (registeredCharacteristic != nullptr) + { + // This is our custom calc service Result UUID. Format it like an Int + if (registeredCharacteristic.Uuid() == Constants::ResultCharacteristicUuid) + { + int32_t value; + if (TryExtract(buffer, value)) + { + return to_hstring(value); + } + return L"Invalid response from calc server"; + } + } + else + { + try + { + return L"Unknown format: " + CryptographicBuffer::ConvertBinaryToString(BinaryStringEncoding::Utf8, buffer); + + } + catch (hresult_invalid_argument&) + { + return L"Unknown format"; + } + } + } + else + { + return L"Empty data received"; + } + return L"Unknown format"; + } +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario2_Client.h b/Samples/BluetoothLE/cppwinrt/Scenario2_Client.h new file mode 100644 index 0000000000..3bab41620d --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario2_Client.h @@ -0,0 +1,72 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#pragma once + +#include "Scenario2_Client.g.h" +#include "MainPage.h" + +namespace winrt::SDKTemplate::implementation +{ + struct Scenario2_Client : Scenario2_ClientT + { + Scenario2_Client(); + + void OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs const& e); + fire_and_forget OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs const&); + + Windows::Foundation::Collections::IObservableVector ServiceCollection() + { + return m_serviceCollection; + } + + Windows::Foundation::Collections::IObservableVector CharacteristicCollection() + { + return m_characteristicCollection; + } + + fire_and_forget ConnectButton_Click(); + fire_and_forget ServiceList_SelectionChanged(); + fire_and_forget CharacteristicList_SelectionChanged(); + fire_and_forget CharacteristicReadButton_Click(); + fire_and_forget CharacteristicWriteButton_Click(); + fire_and_forget CharacteristicWriteButtonInt_Click(); + fire_and_forget ValueChangedSubscribeToggle_Click(); + + private: + SDKTemplate::MainPage rootPage{ SDKTemplate::implementation::MainPage::Current() }; + Windows::Foundation::Collections::IObservableVector m_serviceCollection = single_threaded_observable_vector(); + Windows::Foundation::Collections::IObservableVector m_characteristicCollection = single_threaded_observable_vector(); + Windows::Devices::Bluetooth::BluetoothLEDevice bluetoothLeDevice{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic selectedCharacteristic{ nullptr }; + + // Only one registered characteristic at a time. + Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic registeredCharacteristic{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattPresentationFormat presentationFormat{ nullptr }; + + event_token notificationsToken; + + Windows::Foundation::IAsyncOperation ClearBluetoothLEDeviceAsync(); + void AddValueChangedHandler(); + void RemoveValueChangedHandler(); + void EnableCharacteristicPanels(Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristicProperties properties); + Windows::Foundation::IAsyncOperation WriteBufferToSelectedCharacteristicAsync(Windows::Storage::Streams::IBuffer buffer); + hstring FormatValueByPresentation(Windows::Storage::Streams::IBuffer const& buffer, Windows::Devices::Bluetooth::GenericAttributeProfile::GattPresentationFormat const& format); + fire_and_forget Characteristic_ValueChanged(Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic const&, Windows::Devices::Bluetooth::GenericAttributeProfile::GattValueChangedEventArgs args); + }; +} + +namespace winrt::SDKTemplate::factory_implementation +{ + struct Scenario2_Client : Scenario2_ClientT + { + }; +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario2_Client.idl b/Samples/BluetoothLE/cppwinrt/Scenario2_Client.idl new file mode 100644 index 0000000000..bd8382074f --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario2_Client.idl @@ -0,0 +1,29 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +namespace SDKTemplate +{ + [default_interface] + runtimeclass Scenario2_Client : Windows.UI.Xaml.Controls.Page + { + Scenario2_Client(); + + Windows.Foundation.Collections.IObservableVector ServiceCollection{ get; }; + Windows.Foundation.Collections.IObservableVector CharacteristicCollection{ get; }; + void ConnectButton_Click(); + void ServiceList_SelectionChanged(); + void CharacteristicList_SelectionChanged(); + void CharacteristicReadButton_Click(); + void CharacteristicWriteButton_Click(); + void CharacteristicWriteButtonInt_Click(); + void ValueChangedSubscribeToggle_Click(); + } +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.cpp b/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.cpp new file mode 100644 index 0000000000..74b627a687 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.cpp @@ -0,0 +1,512 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#include "pch.h" +#include "Scenario3_ServerForeground.h" +#include "SampleConfiguration.h" +#include "PresentationFormats.h" + +using namespace winrt; +using namespace Windows::Devices::Bluetooth; +using namespace Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; +using namespace Windows::Storage::Streams; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Navigation; + +namespace winrt +{ + hstring to_hstring(BluetoothError error) + { + switch (error) + { + case BluetoothError::Success: return L"Success"; + case BluetoothError::RadioNotAvailable: return L"RadioNotAvailable"; + case BluetoothError::ResourceInUse: return L"ResourceInUse"; + case BluetoothError::DeviceNotConnected: return L"DeviceNotConnected"; + case BluetoothError::OtherError: return L"OtherError"; + case BluetoothError::DisabledByPolicy: return L"DisabledByPolicy"; + case BluetoothError::NotSupported: return L"NotSupported"; + case BluetoothError::DisabledByUser: return L"DisabledByUser"; + case BluetoothError::ConsentRequired: return L"ConsentRequired"; + case BluetoothError::TransportNotSupported: return L"TransportNotSupported"; + } + return L"Code " + to_hstring(static_cast(error)); + } + + hstring to_hstring(GattServiceProviderAdvertisementStatus status) + { + switch (status) + { + case GattServiceProviderAdvertisementStatus::Created: return L"Created"; + case GattServiceProviderAdvertisementStatus::Stopped: return L"Stopped"; + case GattServiceProviderAdvertisementStatus::Started: return L"Started"; + case GattServiceProviderAdvertisementStatus::Aborted: return L"Aborted"; + } + return L"Code " + to_hstring(static_cast(status)); + } +} + +namespace winrt::SDKTemplate::implementation +{ + // This scenario declares support for a calculator service. + // Remote clients (including this sample on another machine) can supply: + // - Operands 1 and 2 + // - an operator (+,-,*,/) + // and get a result + +#pragma region UI Code + Scenario3_ServerForeground::Scenario3_ServerForeground() + { + InitializeComponent(); + } + + fire_and_forget Scenario3_ServerForeground::OnNavigatedTo(NavigationEventArgs const&) + { + auto lifetime = get_strong(); + peripheralSupported = co_await CheckPeripheralRoleSupportAsync(); + if (peripheralSupported) + { + ServerPanel().Visibility(Visibility::Visible); + } + else + { + PeripheralWarning().Visibility(Visibility::Visible); + } + } + + void Scenario3_ServerForeground::OnNavigatedFrom(NavigationEventArgs const&) + { + if (op1CharacteristicWriteToken) + { + op1Characteristic.WriteRequested(std::exchange(op1CharacteristicWriteToken, {})); + } + if (op2CharacteristicWriteToken) + { + op2Characteristic.WriteRequested(std::exchange(op2CharacteristicWriteToken, {})); + } + if (operatorCharacteristicWriteToken) + { + operatorCharacteristic.WriteRequested(std::exchange(operatorCharacteristicWriteToken, {})); + } + if (resultCharacteristicReadToken) + { + resultCharacteristic.ReadRequested(std::exchange(resultCharacteristicReadToken, {})); + } + if (resultCharacteristicClientsChangedToken) + { + resultCharacteristic.SubscribedClientsChanged(std::exchange(resultCharacteristicClientsChangedToken, {})); + } + if (serviceProviderAdvertisementChangedToken) + { + serviceProvider.AdvertisementStatusChanged(std::exchange(serviceProviderAdvertisementChangedToken, {})); + } + + if (serviceProvider != nullptr) + { + if (serviceProvider.AdvertisementStatus() != GattServiceProviderAdvertisementStatus::Stopped) + { + serviceProvider.StopAdvertising(); + } + serviceProvider = nullptr; + } + } + + fire_and_forget Scenario3_ServerForeground::PublishButton_ClickAsync() + { + auto lifetime = get_strong(); + + // Server not initialized yet - initialize it and start publishing + if (serviceProvider == nullptr) + { + bool serviceStarted = co_await ServiceProviderInitAsync(); + if (serviceStarted) + { + rootPage.NotifyUser(L"Service successfully started", NotifyType::StatusMessage); + PublishButton().Content(box_value(L"Stop Service")); + } + else + { + rootPage.NotifyUser(L"Service not started", NotifyType::ErrorMessage); + } + } + else + { + // BT_Code: Stops advertising support for custom GATT Service + serviceProvider.StopAdvertising(); + serviceProvider = nullptr; + PublishButton().Content(box_value(L"Start Service")); + } + } + + fire_and_forget Scenario3_ServerForeground::UpdateUX() + { + auto lifetime = get_strong(); + co_await resume_foreground(Dispatcher()); + + switch (operatorReceived) + { + case CalculatorOperators::Add: + OperationText().Text(L"+"); + break; + case CalculatorOperators::Subtract: + OperationText().Text(L"-"); + break; + case CalculatorOperators::Multiply: + OperationText().Text(L"*"); + break; + case CalculatorOperators::Divide: + OperationText().Text(L"/"); + break; + default: + OperationText().Text(L"INV"); + break; + } + Operand1Text().Text(to_hstring(operand1Received)); + Operand2Text().Text(to_hstring(operand2Received)); + resultVal = ComputeResult(); + ResultText().Text(to_hstring(resultVal)); + } +#pragma endregion + + IAsyncOperation Scenario3_ServerForeground::CheckPeripheralRoleSupportAsync() + { + // BT_Code: New for Creator's Update - Bluetooth adapter has properties of the local BT radio. + auto localAdapter = co_await BluetoothAdapter::GetDefaultAsync(); + + if (localAdapter != nullptr) + { + co_return localAdapter.IsPeripheralRoleSupported(); + } + else + { + // Bluetooth is not turned on + co_return false; + } + } + + /// + /// Uses the relevant Service/Characteristic UUIDs to initialize, hook up event handlers and start a service on the local system. + /// + /// + IAsyncOperation Scenario3_ServerForeground::ServiceProviderInitAsync() + { + // BT_Code: Initialize and starting a custom GATT Service using GattServiceProvider. + auto lifetime = get_strong(); + + GattServiceProviderResult serviceResult = co_await GattServiceProvider::CreateAsync(Constants::CalcServiceUuid); + if (serviceResult.Error() == BluetoothError::Success) + { + serviceProvider = serviceResult.ServiceProvider(); + } + else + { + rootPage.NotifyUser(L"Could not create service provider: " + to_hstring(serviceResult.Error()), NotifyType::ErrorMessage); + co_return false; + } + + // BT_Code: Initializes custom local parameters w/ properties, protection levels as well as common descriptors like User Description. + GattLocalCharacteristicParameters gattOperandParameters; + gattOperandParameters.CharacteristicProperties(GattCharacteristicProperties::Write | GattCharacteristicProperties::WriteWithoutResponse); + gattOperandParameters.WriteProtectionLevel(GattProtectionLevel::Plain); + gattOperandParameters.UserDescription(L"Operand Characteristic"); + + GattLocalCharacteristicResult result = co_await serviceProvider.Service().CreateCharacteristicAsync(Constants::Op1CharacteristicUuid, gattOperandParameters); + if (result.Error() == BluetoothError::Success) + { + op1Characteristic = result.Characteristic(); + } + else + { + rootPage.NotifyUser(L"Could not create operand1 characteristic: " + to_hstring(result.Error()), NotifyType::ErrorMessage); + co_return false; + } + op1CharacteristicWriteToken = op1Characteristic.WriteRequested({ get_weak(), &Scenario3_ServerForeground::Op1Characteristic_WriteRequestedAsync }); + + // Create the second operand characteristic. + result = co_await serviceProvider.Service().CreateCharacteristicAsync(Constants::Op2CharacteristicUuid, gattOperandParameters); + if (result.Error() == BluetoothError::Success) + { + op2Characteristic = result.Characteristic(); + } + else + { + rootPage.NotifyUser(L"Could not create operand2 characteristic: " + to_hstring(result.Error()), NotifyType::ErrorMessage); + co_return false; + } + + op2CharacteristicWriteToken = op2Characteristic.WriteRequested({ get_weak(), &Scenario3_ServerForeground::Op2Characteristic_WriteRequestedAsync }); + + // Create the operator characteristic. + GattLocalCharacteristicParameters gattOperatorParameters; + gattOperandParameters.CharacteristicProperties(GattCharacteristicProperties::Write | GattCharacteristicProperties::WriteWithoutResponse); + gattOperandParameters.WriteProtectionLevel(GattProtectionLevel::Plain); + gattOperandParameters.UserDescription(L"Operator Characteristic"); + + result = co_await serviceProvider.Service().CreateCharacteristicAsync(Constants::OperatorCharacteristicUuid, gattOperatorParameters); + if (result.Error() == BluetoothError::Success) + { + operatorCharacteristic = result.Characteristic(); + } + else + { + rootPage.NotifyUser(L"Could not create operator characteristic: " + to_hstring(result.Error()), NotifyType::ErrorMessage); + co_return false; + } + + operatorCharacteristicWriteToken = operatorCharacteristic.WriteRequested({ get_weak(), &Scenario3_ServerForeground::OperatorCharacteristic_WriteRequestedAsync }); + + // Create the result characteristic. + GattLocalCharacteristicParameters gattResultParameters; + gattResultParameters.CharacteristicProperties(GattCharacteristicProperties::Read | GattCharacteristicProperties::Notify); + gattResultParameters.WriteProtectionLevel(GattProtectionLevel::Plain); + gattResultParameters.UserDescription(L"Result Characteristic"); + + // Add presentation format - 32-bit unsigned integer, with exponent 0, the unit is unitless, with no company description + GattPresentationFormat intFormat = GattPresentationFormat::FromParts( + GattPresentationFormatTypes::UInt32(), + PresentationFormats::Exponent, + static_cast(PresentationFormats::Units::Unitless), + static_cast(PresentationFormats::NamespaceId::BluetoothSigAssignedNumber), + PresentationFormats::Description); + + gattResultParameters.PresentationFormats().Append(intFormat); + + result = co_await serviceProvider.Service().CreateCharacteristicAsync(Constants::ResultCharacteristicUuid, gattResultParameters); + if (result.Error() == BluetoothError::Success) + { + resultCharacteristic = result.Characteristic(); + } + else + { + rootPage.NotifyUser(L"Could not create result characteristic: " + to_hstring(result.Error()), NotifyType::ErrorMessage); + co_return false; + } + resultCharacteristicReadToken = resultCharacteristic.ReadRequested({ get_weak(), &Scenario3_ServerForeground::ResultCharacteristic_ReadRequestedAsync }); + resultCharacteristicClientsChangedToken = resultCharacteristic.SubscribedClientsChanged({ get_weak(), &Scenario3_ServerForeground::ResultCharacteristic_SubscribedClientsChanged }); + + // BT_Code: Indicate if your sever advertises as connectable and discoverable. + GattServiceProviderAdvertisingParameters advParameters; + + // IsConnectable determines whether a call to publish will attempt to start advertising and + // put the service UUID in the ADV packet (best effort) + advParameters.IsConnectable(peripheralSupported); + + // IsDiscoverable determines whether a remote device can query the local device for support + // of this service + advParameters.IsDiscoverable(true); + + serviceProviderAdvertisementChangedToken = serviceProvider.AdvertisementStatusChanged({ get_weak(), &Scenario3_ServerForeground::ServiceProvider_AdvertisementStatusChanged }); + serviceProvider.StartAdvertising(advParameters); + co_return true; + } + + void Scenario3_ServerForeground::ResultCharacteristic_SubscribedClientsChanged(GattLocalCharacteristic const& sender, IInspectable const&) + { + rootPage.NotifyUser(L"New device subscribed. New subscribed count: " + to_hstring(sender.SubscribedClients().Size()), NotifyType::StatusMessage); + } + + void Scenario3_ServerForeground::ServiceProvider_AdvertisementStatusChanged(GattServiceProvider const& sender, GattServiceProviderAdvertisementStatusChangedEventArgs const&) + { + // Created - The default state of the advertisement, before the service is published for the first time. + // Stopped - Indicates that the application has canceled the service publication and its advertisement. + // Started - Indicates that the system was successfully able to issue the advertisement request. + // Aborted - Indicates that the system was unable to submit the advertisement request, or it was canceled due to resource contention. + + rootPage.NotifyUser(L"New Advertisement Status: AdvertisementStatus = " + to_hstring(sender.AdvertisementStatus()), NotifyType::StatusMessage); + } + + fire_and_forget Scenario3_ServerForeground::ResultCharacteristic_ReadRequestedAsync(GattLocalCharacteristic const&, GattReadRequestedEventArgs args) + { + // BT_Code: Process a read request. + auto lifetime = get_strong(); + auto deferral = args.GetDeferral(); + + // Get the request information. This requires device access before an app can access the device's request. + GattReadRequest request = co_await args.GetRequestAsync(); + if (request == nullptr) + { + // No access allowed to the device. Application should indicate this to the user. + rootPage.NotifyUser(L"Access to device not allowed", NotifyType::ErrorMessage); + } + else + { + DataWriter writer; + writer.ByteOrder(ByteOrder::LittleEndian); + writer.WriteInt32(resultVal); + + // Can get details about the request such as the size and offset, as well as monitor the state to see if it has been completed/cancelled externally. + // request.Offset + // request.Length + // request.State + // request.StateChanged += + + // Gatt code to handle the response + request.RespondWithValue(writer.DetachBuffer()); + } + + deferral.Complete(); + } + + int Scenario3_ServerForeground::ComputeResult() + { + int computedValue = 0; + switch (operatorReceived) + { + case CalculatorOperators::Add: + computedValue = operand1Received + operand2Received; + break; + case CalculatorOperators::Subtract: + computedValue = operand1Received - operand2Received; + break; + case CalculatorOperators::Multiply: + computedValue = operand1Received * operand2Received; + break; + case CalculatorOperators::Divide: + if (operand2Received == 0 || (operand1Received == std::numeric_limits::min() && operand2Received == -1)) + { + rootPage.NotifyUser(L"Division overflow", NotifyType::ErrorMessage); + } + else + { + computedValue = operand1Received / operand2Received; + } + break; + default: + rootPage.NotifyUser(L"Invalid Operator", NotifyType::ErrorMessage); + break; + } + NotifyClientDevices(computedValue); + return computedValue; + } + + fire_and_forget Scenario3_ServerForeground::NotifyClientDevices(int computedValue) + { + auto lifetime = get_strong(); + DataWriter writer; + writer.ByteOrder(ByteOrder::LittleEndian); + writer.WriteInt32(computedValue); + + // BT_Code: Returns a collection of all clients that the notification was attempted and the result. + IVectorView results = co_await resultCharacteristic.NotifyValueAsync(writer.DetachBuffer()); + + rootPage.NotifyUser(L"Sent value " + to_hstring(computedValue) + L" to clients.", NotifyType::StatusMessage); + for (GattClientNotificationResult&& result : results) + { + // An application can iterate through each registered client that was notified and retrieve the results: + // + // result.SubscribedClient(): The details on the remote client. + // result.Status(): The GattCommunicationStatus + // result.ProtocolError(): iff Status() == GattCommunicationStatus::ProtocolError + } + } + + fire_and_forget Scenario3_ServerForeground::Op1Characteristic_WriteRequestedAsync(GattLocalCharacteristic const&, GattWriteRequestedEventArgs args) + { + // BT_Code: Processing a write request. + auto lifetime = get_strong(); + auto deferral = args.GetDeferral(); + + // Get the request information. This requires device access before an app can access the device's request. + GattWriteRequest request = co_await args.GetRequestAsync(); + if (request == nullptr) + { + // No access allowed to the device. Application should indicate this to the user. + } + else + { + ProcessWriteCharacteristic(request, CalculatorCharacteristics::Operand1); + } + deferral.Complete(); + } + + fire_and_forget Scenario3_ServerForeground::Op2Characteristic_WriteRequestedAsync(GattLocalCharacteristic const&, GattWriteRequestedEventArgs args) + { + auto lifetime = get_strong(); + auto deferral = args.GetDeferral(); + + // Get the request information. This requires device access before an app can access the device's request. + GattWriteRequest request = co_await args.GetRequestAsync(); + if (request == nullptr) + { + // No access allowed to the device. Application should indicate this to the user. + } + else + { + ProcessWriteCharacteristic(request, CalculatorCharacteristics::Operand2); + } + deferral.Complete(); + } + + fire_and_forget Scenario3_ServerForeground::OperatorCharacteristic_WriteRequestedAsync(GattLocalCharacteristic const&, GattWriteRequestedEventArgs args) + { + auto lifetime = get_strong(); + auto deferral = args.GetDeferral(); + + // Get the request information. This requires device access before an app can access the device's request. + GattWriteRequest request = co_await args.GetRequestAsync(); + if (request == nullptr) + { + // No access allowed to the device. Application should indicate this to the user. + } + else + { + ProcessWriteCharacteristic(request, CalculatorCharacteristics::Operator); + } + deferral.Complete(); + } + + void Scenario3_ServerForeground::ProcessWriteCharacteristic(GattWriteRequest const& request, CalculatorCharacteristics opCode) + { + if (request.Value().Length() != 4) + { + // Input is the wrong length. Respond with a protocol error if requested. + if (request.Option() == GattWriteOption::WriteWithResponse) + { + request.RespondWithProtocolError(GattProtocolError::InvalidAttributeValueLength()); + } + return; + } + + DataReader reader = DataReader::FromBuffer(request.Value()); + reader.ByteOrder(ByteOrder::LittleEndian); + int val = reader.ReadInt32(); + + switch (opCode) + { + case CalculatorCharacteristics::Operand1: + operand1Received = val; + break; + case CalculatorCharacteristics::Operand2: + operand2Received = val; + break; + case CalculatorCharacteristics::Operator: + if (!IsValidOperator(static_cast(val))) + { + if (request.Option() == GattWriteOption::WriteWithResponse) + { + request.RespondWithProtocolError(GattProtocolError::InvalidPdu()); + } + return; + } + operatorReceived = static_cast(val); + break; + } + // Complete the request if needed + if (request.Option() == GattWriteOption::WriteWithResponse) + { + request.Respond(); + } + + UpdateUX(); + } +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.h b/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.h new file mode 100644 index 0000000000..11b3f3e50b --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.h @@ -0,0 +1,87 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +#pragma once + +#include "Scenario3_ServerForeground.g.h" +#include "MainPage.h" + +namespace winrt::SDKTemplate::implementation +{ + struct Scenario3_ServerForeground : Scenario3_ServerForegroundT + { + Scenario3_ServerForeground(); + + fire_and_forget OnNavigatedTo(Windows::UI::Xaml::Navigation::NavigationEventArgs const&); + void OnNavigatedFrom(Windows::UI::Xaml::Navigation::NavigationEventArgs const& e); + + fire_and_forget PublishButton_ClickAsync(); + private: + enum class CalculatorCharacteristics + { + Operand1 = 1, + Operand2 = 2, + Operator = 3, + }; + + enum class CalculatorOperators + { + Add = 1, + Subtract = 2, + Multiply = 3, + Divide = 4, + }; + + bool IsValidOperator(CalculatorOperators op) + { + return op >= CalculatorOperators::Add && op < CalculatorOperators::Divide; + } + + SDKTemplate::MainPage rootPage{ SDKTemplate::implementation::MainPage::Current() }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattServiceProvider serviceProvider{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic op1Characteristic{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic op2Characteristic{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic operatorCharacteristic{ nullptr }; + Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic resultCharacteristic{ nullptr }; + int operand1Received = 0; + int operand2Received = 0; + CalculatorOperators operatorReceived; + int resultVal = 0; + bool peripheralSupported = false; + + event_token op1CharacteristicWriteToken; + event_token op2CharacteristicWriteToken; + event_token operatorCharacteristicWriteToken; + event_token resultCharacteristicReadToken; + event_token resultCharacteristicClientsChangedToken; + event_token serviceProviderAdvertisementChangedToken; + + fire_and_forget UpdateUX(); + static Windows::Foundation::IAsyncOperation CheckPeripheralRoleSupportAsync(); + Windows::Foundation::IAsyncOperation ServiceProviderInitAsync(); + void ResultCharacteristic_SubscribedClientsChanged(Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic const& sender, IInspectable const& args); + void ServiceProvider_AdvertisementStatusChanged(Windows::Devices::Bluetooth::GenericAttributeProfile::GattServiceProvider const& sender, Windows::Devices::Bluetooth::GenericAttributeProfile::GattServiceProviderAdvertisementStatusChangedEventArgs const& args); + fire_and_forget ResultCharacteristic_ReadRequestedAsync(Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic const& sender, Windows::Devices::Bluetooth::GenericAttributeProfile::GattReadRequestedEventArgs args); + int ComputeResult(); + fire_and_forget NotifyClientDevices(int computedValue); + fire_and_forget Op1Characteristic_WriteRequestedAsync(Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic const& sender, Windows::Devices::Bluetooth::GenericAttributeProfile::GattWriteRequestedEventArgs args); + fire_and_forget Op2Characteristic_WriteRequestedAsync(Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic const& sender, Windows::Devices::Bluetooth::GenericAttributeProfile::GattWriteRequestedEventArgs args); + fire_and_forget OperatorCharacteristic_WriteRequestedAsync(Windows::Devices::Bluetooth::GenericAttributeProfile::GattLocalCharacteristic const& sender, Windows::Devices::Bluetooth::GenericAttributeProfile::GattWriteRequestedEventArgs args); + void ProcessWriteCharacteristic(Windows::Devices::Bluetooth::GenericAttributeProfile::GattWriteRequest const& request, CalculatorCharacteristics opCode); + }; +} + +namespace winrt::SDKTemplate::factory_implementation +{ + struct Scenario3_ServerForeground : Scenario3_ServerForegroundT + { + }; +} diff --git a/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.idl b/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.idl new file mode 100644 index 0000000000..1f7deb9c8e --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/Scenario3_ServerForeground.idl @@ -0,0 +1,21 @@ +//********************************************************* +// +// 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. +// +//********************************************************* + +namespace SDKTemplate +{ + [default_interface] + runtimeclass Scenario3_ServerForeground : Windows.UI.Xaml.Controls.Page + { + Scenario3_ServerForeground(); + + void PublishButton_ClickAsync(); + } +} diff --git a/Samples/BluetoothLE/cppwinrt/packages.config b/Samples/BluetoothLE/cppwinrt/packages.config new file mode 100644 index 0000000000..f3dbd89d49 --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Samples/BluetoothLE/cppwinrt/pch.cpp b/Samples/BluetoothLE/cppwinrt/pch.cpp new file mode 100644 index 0000000000..01484ff5aa --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/pch.cpp @@ -0,0 +1,6 @@ +// +// pch.cpp +// Include the standard header and generate the precompiled header. +// + +#include "pch.h" diff --git a/Samples/BluetoothLE/cppwinrt/pch.h b/Samples/BluetoothLE/cppwinrt/pch.h new file mode 100644 index 0000000000..04ac6ce07b --- /dev/null +++ b/Samples/BluetoothLE/cppwinrt/pch.h @@ -0,0 +1,23 @@ +#pragma once + +#define NOMINMAX +#define __STDC_WANT_SECURE_LIB__ 1 +#include +#include "winrt/Windows.Foundation.h" +#include "winrt/Windows.Foundation.Collections.h" +#include "winrt/Windows.ApplicationModel.Activation.h" +#include "winrt/Windows.Devices.Enumeration.h" +#include "winrt/Windows.Security.Cryptography.h" +#include "winrt/Windows.Storage.Streams.h" +#include "winrt/Windows.System.h" +#include "winrt/Windows.UI.Core.h" +#include "winrt/Windows.UI.Xaml.h" +#include "winrt/Windows.UI.Xaml.Automation.Peers.h" +#include "winrt/Windows.UI.Xaml.Controls.h" +#include "winrt/Windows.UI.Xaml.Controls.Primitives.h" +#include "winrt/Windows.UI.Xaml.Documents.h" +#include "winrt/Windows.UI.Xaml.Interop.h" +#include "winrt/Windows.UI.Xaml.Markup.h" +#include "winrt/Windows.UI.Xaml.Media.h" +#include "winrt/Windows.UI.Xaml.Navigation.h" +#include \ No newline at end of file diff --git a/Samples/BluetoothLE/cs/BluetoothLE.csproj b/Samples/BluetoothLE/cs/BluetoothLE.csproj index 32a5d3dc7d..7d75525681 100644 --- a/Samples/BluetoothLE/cs/BluetoothLE.csproj +++ b/Samples/BluetoothLE/cs/BluetoothLE.csproj @@ -12,7 +12,7 @@ en-US UAP 10.0.17763.0 - 10.0.17763.0 + 10.0.17134.0 14 true 512 @@ -132,7 +132,8 @@ MSBuild:Compile Designer - + + Scenario1_Discovery.xaml MSBuild:Compile Designer @@ -141,11 +142,13 @@ MSBuild:Compile Designer - + + Scenario3_ServerForeground.xaml MSBuild:Compile Designer - + + Scenario2_Client.xaml MSBuild:Compile Designer diff --git a/Samples/BluetoothLE/cs/DisplayHelpers.cs b/Samples/BluetoothLE/cs/DisplayHelpers.cs index 74db85b01c..d0128563e9 100644 --- a/Samples/BluetoothLE/cs/DisplayHelpers.cs +++ b/Samples/BluetoothLE/cs/DisplayHelpers.cs @@ -169,43 +169,6 @@ protected void OnPropertyChanged(string name) } } -#if x - public class GeneralPropertyValueConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, string language) - { - object property = null; - - var properties = value as IReadOnlyDictionary; - var propertyName = parameter as string; - if (properties != null && !string.IsNullOrEmpty(propertyName)) - { - property = properties[propertyName]; - } - - return property; - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - throw new NotImplementedException(); - } - } -#endif - // This inverts the sense of a boolean. - public class InvertConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, string language) - { - return !(bool)value; - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - return !(bool)value; - } - } - /// /// This enum assists in finding a string representation of a BT SIG assigned value for Service UUIDS /// Reference: https://developer.bluetooth.org/gatt/services/Pages/ServicesHome.aspx diff --git a/Samples/BluetoothLE/cs/Scenario1_Discovery.xaml.cs b/Samples/BluetoothLE/cs/Scenario1_Discovery.xaml.cs index 6c454e4960..dc2bdb8241 100644 --- a/Samples/BluetoothLE/cs/Scenario1_Discovery.xaml.cs +++ b/Samples/BluetoothLE/cs/Scenario1_Discovery.xaml.cs @@ -68,7 +68,10 @@ private void EnumerateButton_Click() EnumerateButton.Content = "Start enumerating"; rootPage.NotifyUser($"Device watcher stopped.", NotifyType.StatusMessage); } - } + } + + private bool Not(bool value) => !value; + #endregion #region Device discovery diff --git a/Samples/BluetoothLE/cs/Scenario2_Client.xaml.cs b/Samples/BluetoothLE/cs/Scenario2_Client.xaml.cs index 022d6881f0..7ca7c9db8b 100644 --- a/Samples/BluetoothLE/cs/Scenario2_Client.xaml.cs +++ b/Samples/BluetoothLE/cs/Scenario2_Client.xaml.cs @@ -62,6 +62,7 @@ public Scenario2_Client() protected override void OnNavigatedTo(NavigationEventArgs e) { + SelectedDeviceRun.Text = rootPage.SelectedBleDeviceName; if (string.IsNullOrEmpty(rootPage.SelectedBleDeviceId)) { ConnectButton.IsEnabled = false; @@ -107,7 +108,7 @@ private async void ConnectButton_Click() if (!await ClearBluetoothLEDeviceAsync()) { rootPage.NotifyUser("Error: Unable to reset state, try again.", NotifyType.ErrorMessage); - ConnectButton.IsEnabled = false; + ConnectButton.IsEnabled = true; return; } diff --git a/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs b/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs index 3683de51d3..e91d4af0f0 100644 --- a/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs +++ b/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs @@ -274,35 +274,33 @@ private void ServiceProvider_AdvertisementStatusChanged(GattServiceProvider send rootPage.NotifyUser($"New Advertisement Status: {sender.AdvertisementStatus}", NotifyType.StatusMessage); } + private async void ResultCharacteristic_ReadRequestedAsync(GattLocalCharacteristic sender, GattReadRequestedEventArgs args) { // BT_Code: Process a read request. using (args.GetDeferral()) { - await CoreApplication.MainView.CoreWindow.Dispatcher.RunTaskAsync(async () => + // Get the request information. This requires device access before an app can access the device's request. + GattReadRequest request = await args.GetRequestAsync(); + if (request == null) { - // Get the request information. This requires device access before an app can access the device's request. - GattReadRequest request = await args.GetRequestAsync(); - if (request == null) - { - // No access allowed to the device. Application should indicate this to the user. - rootPage.NotifyUser("Access to device not allowed", NotifyType.ErrorMessage); - return; - } + // No access allowed to the device. Application should indicate this to the user. + rootPage.NotifyUser("Access to device not allowed", NotifyType.ErrorMessage); + return; + } - var writer = new DataWriter(); - writer.ByteOrder = ByteOrder.LittleEndian; - writer.WriteInt32(resultVal); + var writer = new DataWriter(); + writer.ByteOrder = ByteOrder.LittleEndian; + writer.WriteInt32(resultVal); - // Can get details about the request such as the size and offset, as well as monitor the state to see if it has been completed/cancelled externally. - // request.Offset - // request.Length - // request.State - // request.StateChanged += + // Can get details about the request such as the size and offset, as well as monitor the state to see if it has been completed/cancelled externally. + // request.Offset + // request.Length + // request.State + // request.StateChanged += - // Gatt code to handle the response - request.RespondWithValue(writer.DetachBuffer()); - }); + // Gatt code to handle the response + request.RespondWithValue(writer.DetachBuffer()); } } diff --git a/Samples/BluetoothLE/cs/Scenario1_Discovery.xaml b/Samples/BluetoothLE/shared/Scenario1_Discovery.xaml similarity index 92% rename from Samples/BluetoothLE/cs/Scenario1_Discovery.xaml rename to Samples/BluetoothLE/shared/Scenario1_Discovery.xaml index b15c7eefcd..4f9a7da6bc 100644 --- a/Samples/BluetoothLE/cs/Scenario1_Discovery.xaml +++ b/Samples/BluetoothLE/shared/Scenario1_Discovery.xaml @@ -19,7 +19,6 @@ xmlns:local="using:SDKTemplate" mc:Ignorable="d"> - @@ -54,7 +53,7 @@