diff --git a/.build/build.cake b/.build/build.cake
index 95190256..b3511442 100644
--- a/.build/build.cake
+++ b/.build/build.cake
@@ -1,5 +1,5 @@
-#addin nuget:?package=Cake.Git&version=2.0.0
-#addin nuget:?package=Cake.FileHelpers&version=5.0.0
+#addin nuget:?package=Cake.Git&version=3.0.0
+#addin nuget:?package=Cake.FileHelpers&version=6.1.3
using Path = System.IO.Path;
using System.Xml.Linq;
@@ -68,10 +68,15 @@ Task("BuildLibs")
Task("BuildClients")
.Does(() =>
{
+ // Xamarin
BuildProject("BLE.Client", "BLE.Client", Path.Combine("clients", "netstandard2.0"));
BuildProject("BLE.Client", "BLE.Client.Droid", Path.Combine("clients", "android"));
BuildProject("BLE.Client", "BLE.Client.iOS", Path.Combine("clients", "ios"));
BuildProject("BLE.Client", "BLE.Client.macOS", Path.Combine("clients", "macOS"));
+ BuildProject("BLE.Client", "BLE.Client.UWP", Path.Combine("clients", "uwp"));
+ // .NET 7/8
+ BuildProject("BLE.Client", "BLE.Client.WinConsole", Path.Combine("clients", "wincon"));
+ BuildProject("BLE.Client", "BLE.Client.Maui", Path.Combine("clients", "maui"));
});
Task("Clean").Does (() =>
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..84dbdffb
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: janusw
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 40662c8a..610e106a 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -13,14 +13,19 @@ jobs:
winBuild:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: nuget/setup-nuget@v1
+ - uses: nuget/setup-nuget@v2
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 7.0.x
+ dotnet-version: 8.0.x
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '11'
- name: Install .NET MAUI
shell: pwsh
run: |
@@ -28,34 +33,34 @@ jobs:
& dotnet workload install maui --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json
& dotnet workload install android ios maccatalyst tvos macos maui wasm-tools maui-maccatalyst --source https://aka.ms/dotnet6/nuget/index.json --source https://api.nuget.org/v3/index.json
- name: Add msbuild to PATH
- uses: microsoft/setup-msbuild@v1.1
+ uses: microsoft/setup-msbuild@v2
- name: Clean
- uses: cake-build/cake-action@v1.4.1
+ uses: cake-build/cake-action@v2
with:
script-path: .build/build.cake
target: Clean
- name: Restore
- uses: cake-build/cake-action@v1.4.1
+ uses: cake-build/cake-action@v2
with:
script-path: .build/build.cake
target: Restore
- name: Build Libs
- uses: cake-build/cake-action@v1.4.1
+ uses: cake-build/cake-action@v2
with:
script-path: .build/build.cake
target: BuildLibs
- name: Build Clients
- uses: cake-build/cake-action@v1.4.1
+ uses: cake-build/cake-action@v2
with:
script-path: .build/build.cake
target: BuildClients
- name: Builds Tests
- uses: cake-build/cake-action@v1.4.1
+ uses: cake-build/cake-action@v2
with:
script-path: .build/build.cake
target: BuildTests
- name: Run Tests
- uses: cake-build/cake-action@v1.4.1
+ uses: cake-build/cake-action@v2
with:
script-path: .build/build.cake
target: RunTests
@@ -64,22 +69,25 @@ jobs:
- name: Build MVVMCross.Plugins.BLE NuGet
run: msbuild .\Source\MvvmCross.Plugins.BLE\MvvmCross.Plugins.BLE.csproj /p:Configuration=Release /t:restore,build,pack /p:PackageOutputPath=./nuget /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false
- name: Upload packages
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: nupkg
path: ./Source/*/nuget/*Plugin.BLE*.nupkg
macBuild:
- runs-on: macos-latest
+ runs-on: macos-13
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: nuget/setup-nuget@v1
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Setup XCode
+ uses: maxim-lobanov/setup-xcode@v1
with:
- dotnet-version: 7.0.x
+ xcode-version: '15'
- name: Install .NET MAUI
run: |
dotnet nuget locals all --clear
@@ -89,3 +97,28 @@ jobs:
run: dotnet build ./Source/Plugin.BLE/Plugin.BLE.csproj /p:Configuration=Release /t:restore,build,pack /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false
- name: Build MVVMCross.Plugins.BLE NuGet
run: dotnet build ./Source/MvvmCross.Plugins.BLE/MvvmCross.Plugins.BLE.csproj /p:Configuration=Release /t:restore,build,pack /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false
+ - name: Build MAUI sample
+ run: dotnet build ./Source/BLE.Client/BLE.Client.Maui/BLE.Client.Maui.csproj /p:Configuration=Release /t:restore,build /p:Version=$(git describe) /p:ContinuousIntegrationBuild=true /p:DeterministicSourcePaths=false
+
+ linuxBuild:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Install workloads
+ run: dotnet workload install android wasm-tools maui-android
+ - name: Install Android tools
+ run: |
+ ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager \
+ --sdk_root=$ANDROID_SDK_ROOT "platform-tools"
+ - name: Build Plugin.BLE NuGet
+ run: dotnet build ./Source/Plugin.BLE/Plugin.BLE.csproj -p:Configuration=Release -t:restore,build,pack -p:Version=$(git describe) -p:ContinuousIntegrationBuild=true -p:DeterministicSourcePaths=false
+ - name: Build MVVMCross.Plugins.BLE NuGet
+ run: dotnet build ./Source/MvvmCross.Plugins.BLE/MvvmCross.Plugins.BLE.csproj -p:Configuration=Release -t:restore,build,pack -p:Version=$(git describe) -p:ContinuousIntegrationBuild=true -p:DeterministicSourcePaths=false
+ - name: Build MAUI sample
+ run: dotnet build ./Source/BLE.Client/BLE.Client.Maui/BLE.Client.Maui.csproj -p:Configuration=Release -t:restore,build -p:Version=$(git describe) -p:ContinuousIntegrationBuild=true -p:DeterministicSourcePaths=false
diff --git a/.gitignore b/.gitignore
index c989a7f6..761d1d47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,7 +13,9 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
-# Build results
+# Build results
+.idea/
+.idea.BLE/
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
@@ -29,6 +31,12 @@ bld/
FAKE*
!FAKE*.cs
.fake/
+.idea/
+*.dll
+*.puibxml
+*.pdb
+*.DS_Store
+.mono/
# Visual Studo 2015 cache/options directory
.vs/
diff --git a/README.md b/README.md
index 1d827f93..98585506 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ This project was forked in order to support some functionality required by the B
| Xamarin.iOS | 7.0 | |
| Xamarin.Mac | 10.9 (Mavericks) | >= 2.1.0 |
| Xamarin.UWP | 1709 - 10.0.16299 | >= 2.2.0 |
-| MAUI (all 4 OS) | | >= 3.0.0 |
+| MAUI (Android, iOS, Mac, WinUI) | | >= 3.0.0 |
## Nuget Packages
@@ -51,7 +51,9 @@ Install-Package MvvmCross.Plugin.BLE
Install-Package MvvmCross.Plugin.BLE -Pre
```
-**Android**
+## Permissions
+
+### Android
Add these permissions to AndroidManifest.xml. For Marshmallow and above, please follow [Requesting Runtime Permissions in Android Marshmallow](https://devblogs.microsoft.com/xamarin/requesting-runtime-permissions-in-android-marshmallow/) and don't forget to prompt the user for the location permission.
@@ -67,45 +69,46 @@ Android 12 and above may require one or more of the following additional runtime
-
```
-
Add this line to your manifest if you want to declare that your app is available to BLE-capable devices **only**:
```xml
````
-**iOS**
+### iOS
On iOS you must add the following keys to your `Info.plist`
- UIBackgroundModes
-
-
- bluetooth-central
-
-
- bluetooth-peripheral
-
-
-
- NSBluetoothPeripheralUsageDescription
- YOUR CUSTOM MESSAGE
-
-
- NSBluetoothAlwaysUsageDescription
- YOUR CUSTOM MESSAGE
+```xml
+UIBackgroundModes
+
+
+ bluetooth-central
+
+ bluetooth-peripheral
+
+
+
+NSBluetoothPeripheralUsageDescription
+YOUR CUSTOM MESSAGE
+
+
+NSBluetoothAlwaysUsageDescription
+YOUR CUSTOM MESSAGE
+````
-**MacOS**
+### MacOS
On MacOS (version 11 and above) you must add the following keys to your `Info.plist`:
-
- NSBluetoothAlwaysUsageDescription
- YOUR CUSTOM MESSAGE
+```xml
+
+NSBluetoothAlwaysUsageDescription
+YOUR CUSTOM MESSAGE
+````
-**UWP**
+### UWP
Add this line to the Package Manifest (.appxmanifest):
@@ -117,7 +120,7 @@ Add this line to the Package Manifest (.appxmanifest):
We provide a sample Xamarin.Forms app, that is a basic bluetooth LE scanner. With this app, it's possible to
-- check the ble status
+- check the BLE status
- discover devices
- connect/disconnect
- discover the services
@@ -329,14 +332,14 @@ The BLE API implementation (especially on **Android**) has the following limitat
}
```
-- **Avoid caching of Characteristic or Service instances between connection sessions**. This includes saving a reference to them in your class between connection sessions etc. After a device has been disconnected all Service & Characteristic instances become **invalid**. Allways **use GetServiceAsync and GetCharacteristicAsync to get a valid instance**.
+- **Avoid caching of Characteristic or Service instances between connection sessions**. This includes saving a reference to them in your class between connection sessions etc. After a device has been disconnected all Service & Characteristic instances become **invalid**. Always **use GetServiceAsync and GetCharacteristicAsync to get a valid instance**.
### General BLE iOS, Android
-- Scanning: Avoid performing ble device operations like Connect, Read, Write etc while scanning for devices. Scanning is battery-intensive.
- - try to stop scanning before performing device operations (connect/read/write/etc)
- - try to stop scanning as soon as you find the desired device
- - never scan on a loop, and set a time limit on your scan
+- Scanning: Avoid performing BLE device operations like Connect, Read, Write etc while scanning for devices. Scanning is battery-intensive.
+ - Try to stop scanning before performing device operations (connect/read/write/etc).
+ - Try to stop scanning as soon as you find the desired device.
+ - Never scan on a loop, and set a time limit on your scan.
## How to build the nuget package
diff --git a/Source/.gitignore b/Source/.gitignore
new file mode 100644
index 00000000..d0027d1b
--- /dev/null
+++ b/Source/.gitignore
@@ -0,0 +1,213 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Mac OSX
+.DS_Store
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+.idea/
+.idea.BLE/
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+.build/out/
+.build/xunit*
+FAKE*
+!FAKE*.cs
+.fake/
+.idea/
+
+# Visual Studo 2015 cache/options directory
+.vs/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding addin-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+*.[Cc]ache
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+*.bak
+/.build/PluginNugetTest/testbuild
+.build/tools/
+tools/
+
+Resource.Designer.cs
diff --git a/Source/BLE.Client/BLE.Client.Droid/BLE.Client.Droid.csproj b/Source/BLE.Client/BLE.Client.Droid/BLE.Client.Droid.csproj
index acc71517..6aea4786 100644
--- a/Source/BLE.Client/BLE.Client.Droid/BLE.Client.Droid.csproj
+++ b/Source/BLE.Client/BLE.Client.Droid/BLE.Client.Droid.csproj
@@ -15,7 +15,7 @@
Properties\AndroidManifest.xmlResourcesAssets
- v12.0
+ v13.0false
@@ -117,25 +117,25 @@
8.0.2
- 3.1.0
+ 7.0.0
- 0.2.0.64
+ 1.0.0
- 5.0.0.2478
+ 5.0.0.2612
- 1.2.5.4
+ 1.6.0.1
- 1.0.0.12
+ 1.0.0.21
- 1.2.1.5
+ 1.3.1.2
- 1.1.1.13
+ 1.2.1.2
diff --git a/Source/BLE.Client/BLE.Client.Droid/Properties/AndroidManifest.xml b/Source/BLE.Client/BLE.Client.Droid/Properties/AndroidManifest.xml
index 3cf598ad..48f7a96e 100644
--- a/Source/BLE.Client/BLE.Client.Droid/Properties/AndroidManifest.xml
+++ b/Source/BLE.Client/BLE.Client.Droid/Properties/AndroidManifest.xml
@@ -1,6 +1,6 @@
-
+
@@ -10,4 +10,4 @@
-
\ No newline at end of file
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/App.xaml b/Source/BLE.Client/BLE.Client.Maui/App.xaml
new file mode 100644
index 00000000..b1298e26
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/App.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/App.xaml.cs b/Source/BLE.Client/BLE.Client.Maui/App.xaml.cs
new file mode 100644
index 00000000..b01c9fee
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/App.xaml.cs
@@ -0,0 +1,22 @@
+using BLE.Client.Maui.Services;
+
+namespace BLE.Client.Maui;
+
+public partial class App : Application
+{
+ private static IServiceProvider ServicesProvider;
+ public static IServiceProvider Services => ServicesProvider;
+ private static IAlertService AlertService;
+ public static IAlertService AlertSvc => AlertService;
+
+ public readonly static LogService Logger = new();
+
+ public App(IServiceProvider provider)
+ {
+ InitializeComponent();
+
+ ServicesProvider = provider;
+ AlertService = Services.GetService();
+ MainPage = new AppShell();
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.Maui/AppShell.xaml b/Source/BLE.Client/BLE.Client.Maui/AppShell.xaml
new file mode 100644
index 00000000..dcbe7129
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/AppShell.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/AppShell.xaml.cs b/Source/BLE.Client/BLE.Client.Maui/AppShell.xaml.cs
new file mode 100644
index 00000000..65d43bf8
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/AppShell.xaml.cs
@@ -0,0 +1,10 @@
+namespace BLE.Client.Maui;
+
+public partial class AppShell : Shell
+{
+ public AppShell()
+ {
+ InitializeComponent();
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/BLE.Client.Maui.csproj b/Source/BLE.Client/BLE.Client.Maui/BLE.Client.Maui.csproj
new file mode 100644
index 00000000..43fbcd09
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/BLE.Client.Maui.csproj
@@ -0,0 +1,98 @@
+
+
+
+ net8.0-android34.0
+ $(TargetFrameworks);net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);net8.0-windows10.0.19041
+ Exe
+ BLE.Client.Maui
+ true
+ true
+ enable
+ false
+
+
+ BLE.Client.Maui
+
+
+ com.companyname.ble.client.maui
+ a401d899-314c-4522-b345-bdf1731f57a5
+
+
+ 1.0
+ 1
+
+ 11.0
+ 14.0
+ 21.0
+ 10.0.17763.0
+ 10.0.17763.0
+ 6.5
+
+
+
+ false
+
+
+ 4
+
+
+ maccatalyst-arm64
+ false
+ Mac Developer
+ 3rd Party Mac Developer Installer
+
+
+ x64
+ win10-x64
+
+
+ x86
+ win10-x86
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/MauiProgram.cs b/Source/BLE.Client/BLE.Client.Maui/MauiProgram.cs
new file mode 100644
index 00000000..800e401e
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/MauiProgram.cs
@@ -0,0 +1,30 @@
+using BLE.Client.Maui.Services;
+namespace BLE.Client.Maui;
+
+public static class MauiProgram
+{
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder
+ .UseMauiApp()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
+ });
+ builder.Services.AddSingleton();
+
+#if DEBUG
+ //builder.Logging.AddDebug();
+#endif
+
+ return builder.Build();
+ }
+
+ public static bool IsAndroid => DeviceInfo.Current.Platform == DevicePlatform.Android;
+
+ public static bool IsMacCatalyst => DeviceInfo.Current.Platform == DevicePlatform.MacCatalyst;
+
+ public static bool IsMacOS => DeviceInfo.Current.Platform == DevicePlatform.macOS;
+}
diff --git a/Source/BLE.Client/BLE.Client.Maui/Models/BLEDevice.cs b/Source/BLE.Client/BLE.Client.Maui/Models/BLEDevice.cs
new file mode 100644
index 00000000..e4bf7f59
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Models/BLEDevice.cs
@@ -0,0 +1,27 @@
+using Plugin.BLE.Abstractions;
+
+namespace BLE.Client.Maui.Models
+{
+ public class BLEDevice
+ {
+ public Guid DeviceId { get; set; }
+
+ public string Name { get; set; }
+
+ public string ManufacturerData { get; set; }
+
+ public int Rssi { get; set; }
+
+ public bool IsConnectable { get; set; }
+
+ public DeviceState State { get; set; }
+
+
+ public override string ToString()
+ {
+ return $"{Name}: {DeviceId}: {Rssi}: {State}";
+
+ }
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Models/BLEDevices.cs b/Source/BLE.Client/BLE.Client.Maui/Models/BLEDevices.cs
new file mode 100644
index 00000000..2efa08d8
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Models/BLEDevices.cs
@@ -0,0 +1,10 @@
+namespace BLE.Client.Maui.Models
+{
+ public class BLEDevices : List
+ {
+ public BLEDevices()
+ {
+ }
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Models/DeviceState.cs b/Source/BLE.Client/BLE.Client.Maui/Models/DeviceState.cs
new file mode 100644
index 00000000..40d0f711
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Models/DeviceState.cs
@@ -0,0 +1,30 @@
+/*using System;
+namespace BLE.Client.Maui.Models
+{
+
+ ///
+ /// Determines the connection state of the device.
+ ///
+ public enum DeviceState
+ {
+ ///
+ /// Device is disconnected.
+ ///
+ Disconnected,
+
+ ///
+ /// Device is connecting.
+ ///
+ Connecting,
+
+ ///
+ /// Device is connected.
+ ///
+ Connected,
+
+ ///
+ /// OnAndroid: Device is connected to the system. In order to use this device please call connect it by using the Adapter.
+ ///
+ Limited
+ }
+}*/
\ No newline at end of file
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/AndroidManifest.xml b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/AndroidManifest.xml
new file mode 100644
index 00000000..5c706dda
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/MainActivity.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/MainActivity.cs
new file mode 100644
index 00000000..a793029d
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/MainActivity.cs
@@ -0,0 +1,11 @@
+using Android.App;
+using Android.Content.PM;
+using Android.OS;
+
+namespace BLE.Client.Maui;
+
+[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
+public class MainActivity : MauiAppCompatActivity
+{
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/MainApplication.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/MainApplication.cs
new file mode 100644
index 00000000..0090e3ab
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/MainApplication.cs
@@ -0,0 +1,16 @@
+using Android.App;
+using Android.Runtime;
+
+namespace BLE.Client.Maui;
+
+[Application]
+public class MainApplication : MauiApplication
+{
+ public MainApplication(IntPtr handle, JniHandleOwnership ownership)
+ : base(handle, ownership)
+ {
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/Resources/values/colors.xml b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/Resources/values/colors.xml
new file mode 100644
index 00000000..a3390047
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Android/Resources/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #0B4E9D
+ #2B0B98
+ #2B0B98
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs
new file mode 100644
index 00000000..cc10e485
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace BLE.Client.Maui;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/Info.plist b/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/Info.plist
new file mode 100644
index 00000000..7241e0bf
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/Info.plist
@@ -0,0 +1,41 @@
+
+
+
+
+ UIDeviceFamily
+
+ 6
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ NSBluetoothPeripheralUsageDescription
+ This needs BLE!
+ NSLocationAlwaysUsageDescription
+ This needs Location!
+ NSLocationUsageDescription
+ This needs Location!
+ NSBluetoothAlwaysUsageDescription
+ This needs BLE!
+ BluetoothPeripheralUsageDescription
+ The needs BLE!
+ Custom Property
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/Program.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/Program.cs
new file mode 100644
index 00000000..7d88e03e
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/MacCatalyst/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace BLE.Client.Maui;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Tizen/Main.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/Tizen/Main.cs
new file mode 100644
index 00000000..70579553
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Tizen/Main.cs
@@ -0,0 +1,17 @@
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Hosting;
+
+namespace BLE.Client.Maui;
+
+class Program : MauiApplication
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+
+ static void Main(string[] args)
+ {
+ var app = new Program();
+ app.Run(args);
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Tizen/tizen-manifest.xml b/Source/BLE.Client/BLE.Client.Maui/Platforms/Tizen/tizen-manifest.xml
new file mode 100644
index 00000000..c1d5829d
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Tizen/tizen-manifest.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ maui-appicon-placeholder
+
+
+
+
+ http://tizen.org/privilege/internet
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/App.xaml b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/App.xaml
new file mode 100644
index 00000000..e8b321c9
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/App.xaml.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/App.xaml.cs
new file mode 100644
index 00000000..55cd2335
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/App.xaml.cs
@@ -0,0 +1,25 @@
+using Microsoft.UI.Xaml;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace BLE.Client.Maui.WinUI;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : MauiWinUIApplication
+{
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/Package.appxmanifest b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/Package.appxmanifest
new file mode 100644
index 00000000..016db646
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/Package.appxmanifest
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+ $placeholder$
+ User Name
+ $placeholder$.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/app.manifest b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/app.manifest
new file mode 100644
index 00000000..f21c8a26
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/Windows/app.manifest
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/AppDelegate.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/AppDelegate.cs
new file mode 100644
index 00000000..cc10e485
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/AppDelegate.cs
@@ -0,0 +1,10 @@
+using Foundation;
+
+namespace BLE.Client.Maui;
+
+[Register("AppDelegate")]
+public class AppDelegate : MauiUIApplicationDelegate
+{
+ protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/Info.plist b/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/Info.plist
new file mode 100644
index 00000000..1f909a3d
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/Info.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UIRequiredDeviceCapabilities
+
+ arm64
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ XSAppIconAssets
+ Assets.xcassets/appicon.appiconset
+ CFBundleIdentifier
+ com.plugin.ble
+ NSBluetoothAlwaysUsageDescription
+ Need bluetooth permission.
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/Program.cs b/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/Program.cs
new file mode 100644
index 00000000..7d88e03e
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Platforms/iOS/Program.cs
@@ -0,0 +1,16 @@
+using ObjCRuntime;
+using UIKit;
+
+namespace BLE.Client.Maui;
+
+public class Program
+{
+ // This is the main entry point of the application.
+ static void Main(string[] args)
+ {
+ // if you want to use a different Application Delegate class from "AppDelegate"
+ // you can specify it here.
+ UIApplication.Main(args, null, typeof(AppDelegate));
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Properties/launchSettings.json b/Source/BLE.Client/BLE.Client.Maui/Properties/launchSettings.json
new file mode 100644
index 00000000..90f92d96
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Windows Machine": {
+ "commandName": "MsixPackage",
+ "nativeDebugging": false
+ }
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/AppIcon/appicon.svg b/Source/BLE.Client/BLE.Client.Maui/Resources/AppIcon/appicon.svg
new file mode 100644
index 00000000..31f16d68
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Resources/AppIcon/appicon.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/AppIcon/appiconfg.svg b/Source/BLE.Client/BLE.Client.Maui/Resources/AppIcon/appiconfg.svg
new file mode 100644
index 00000000..e7bc758f
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Resources/AppIcon/appiconfg.svg
@@ -0,0 +1,14 @@
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Fonts/OpenSans-Regular.ttf b/Source/BLE.Client/BLE.Client.Maui/Resources/Fonts/OpenSans-Regular.ttf
new file mode 100644
index 00000000..2c3020b0
Binary files /dev/null and b/Source/BLE.Client/BLE.Client.Maui/Resources/Fonts/OpenSans-Regular.ttf differ
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Fonts/OpenSans-Semibold.ttf b/Source/BLE.Client/BLE.Client.Maui/Resources/Fonts/OpenSans-Semibold.ttf
new file mode 100644
index 00000000..3e7fe530
Binary files /dev/null and b/Source/BLE.Client/BLE.Client.Maui/Resources/Fonts/OpenSans-Semibold.ttf differ
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Images/dotnet_bot.svg b/Source/BLE.Client/BLE.Client.Maui/Resources/Images/dotnet_bot.svg
new file mode 100644
index 00000000..e19b0127
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Resources/Images/dotnet_bot.svg
@@ -0,0 +1,95 @@
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Images/scanning.gif b/Source/BLE.Client/BLE.Client.Maui/Resources/Images/scanning.gif
new file mode 100644
index 00000000..e1964a71
Binary files /dev/null and b/Source/BLE.Client/BLE.Client.Maui/Resources/Images/scanning.gif differ
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Images/spinner.gif b/Source/BLE.Client/BLE.Client.Maui/Resources/Images/spinner.gif
new file mode 100644
index 00000000..36b9853b
Binary files /dev/null and b/Source/BLE.Client/BLE.Client.Maui/Resources/Images/spinner.gif differ
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Raw/AboutAssets.txt b/Source/BLE.Client/BLE.Client.Maui/Resources/Raw/AboutAssets.txt
new file mode 100644
index 00000000..808d6d3b
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Resources/Raw/AboutAssets.txt
@@ -0,0 +1,17 @@
+Any raw assets you want to be deployed with your application can be placed in
+this directory (and child directories). Deployment of the asset to your application
+is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
+
+
+
+These files will be deployed with you package and will be accessible using Essentials:
+
+ async Task LoadMauiAsset()
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
+ using var reader = new StreamReader(stream);
+
+ var contents = reader.ReadToEnd();
+ }
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Splash/splash.svg b/Source/BLE.Client/BLE.Client.Maui/Resources/Splash/splash.svg
new file mode 100644
index 00000000..05f41190
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Resources/Splash/splash.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Styles/Colors.xaml b/Source/BLE.Client/BLE.Client.Maui/Resources/Styles/Colors.xaml
new file mode 100644
index 00000000..0a8a6d65
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Resources/Styles/Colors.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+ #0B4E9D
+ #DFD8F7
+ #2B0B98
+ White
+ Black
+ #E1E1E1
+ #C8C8C8
+ #ACACAC
+ #919191
+ #6E6E6E
+ #404040
+ #212121
+ #141414
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #F7B548
+ #FFD590
+ #FFE5B9
+ #28C2D1
+ #7BDDEF
+ #C3F2F4
+ #3E8EED
+ #72ACF1
+ #A7CBF6
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Resources/Styles/Styles.xaml b/Source/BLE.Client/BLE.Client.Maui/Resources/Styles/Styles.xaml
new file mode 100644
index 00000000..d23a11dd
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Resources/Styles/Styles.xaml
@@ -0,0 +1,406 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Services/AlertService.cs b/Source/BLE.Client/BLE.Client.Maui/Services/AlertService.cs
new file mode 100644
index 00000000..3f393f7c
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Services/AlertService.cs
@@ -0,0 +1,48 @@
+// Alert Service taken from: https://stackoverflow.com/questions/72429055/how-to-displayalert-in-a-net-maui-viewmodel
+namespace BLE.Client.Maui.Services
+{
+ public class AlertService: IAlertService
+ {
+ public AlertService()
+ {
+ }
+
+ public Task ShowAlertAsync(string title, string message, string cancel = "OK")
+ {
+ return Application.Current.MainPage.DisplayAlert(title, message, cancel);
+ }
+
+ public Task ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No")
+ {
+ return Application.Current.MainPage.DisplayAlert(title, message, accept, cancel);
+ }
+
+
+ // ----- "Fire and forget" calls -----
+
+ ///
+ /// "Fire and forget". Method returns BEFORE showing alert.
+ ///
+ public void ShowAlert(string title, string message, string cancel = "OK")
+ {
+ Application.Current.MainPage.Dispatcher.Dispatch(async () =>
+ await ShowAlertAsync(title, message, cancel)
+ );
+ }
+
+ ///
+ /// "Fire and forget". Method returns BEFORE showing alert.
+ ///
+ /// Action to perform afterwards.
+ public void ShowConfirmation(string title, string message, Action callback,
+ string accept = "Yes", string cancel = "No")
+ {
+ Application.Current.MainPage.Dispatcher.Dispatch(async () =>
+ {
+ bool answer = await ShowConfirmationAsync(title, message, accept, cancel);
+ callback(answer);
+ });
+ }
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Services/IAlertService.cs b/Source/BLE.Client/BLE.Client.Maui/Services/IAlertService.cs
new file mode 100644
index 00000000..b331f2ff
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Services/IAlertService.cs
@@ -0,0 +1,17 @@
+// Alert Service taken from: https://stackoverflow.com/questions/72429055/how-to-displayalert-in-a-net-maui-viewmodel
+namespace BLE.Client.Maui.Services
+{
+ public interface IAlertService
+ {
+ // ----- async calls (use with "await" - MUST BE ON DISPATCHER THREAD) -----
+ Task ShowAlertAsync(string title, string message, string cancel = "OK");
+ Task ShowConfirmationAsync(string title, string message, string accept = "Yes", string cancel = "No");
+
+ // ----- "Fire and forget" calls -----
+ void ShowAlert(string title, string message, string cancel = "OK");
+ /// Action to perform afterwards.
+ void ShowConfirmation(string title, string message, Action callback,
+ string accept = "Yes", string cancel = "No");
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Services/LogService.cs b/Source/BLE.Client/BLE.Client.Maui/Services/LogService.cs
new file mode 100644
index 00000000..7df9a3fa
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Services/LogService.cs
@@ -0,0 +1,27 @@
+using System.Collections.ObjectModel;
+
+namespace BLE.Client.Maui.Services;
+
+///
+/// Runs everything on MainThread
+///
+public class LogService
+{
+ private readonly ObservableCollection LogMessages = [];
+ public ReadOnlyObservableCollection Messages { get; init; }
+
+ public LogService()
+ {
+ Messages = new(LogMessages);
+ }
+
+ public void ClearMessages()
+ {
+ MainThread.BeginInvokeOnMainThread(LogMessages.Clear);
+ }
+
+ public void AddMessage(string message)
+ {
+ MainThread.BeginInvokeOnMainThread(() => { LogMessages.Add(message); });
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.Maui/ViewModels/BLEDeviceViewModel.cs b/Source/BLE.Client/BLE.Client.Maui/ViewModels/BLEDeviceViewModel.cs
new file mode 100644
index 00000000..abf3770a
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/ViewModels/BLEDeviceViewModel.cs
@@ -0,0 +1,112 @@
+using System.Text;
+using Plugin.BLE.Abstractions;
+using Plugin.BLE.Abstractions.Contracts;
+
+namespace BLE.Client.Maui.ViewModels
+{
+ public class BLEDeviceViewModel : BaseViewModel
+ {
+ private Guid _deviceId = new();
+ public Guid DeviceId
+ {
+ get => _deviceId;
+ set
+ {
+ if (_deviceId != value)
+ {
+ _deviceId = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private string _name = string.Empty;
+ public string Name
+ {
+ get => _name;
+ set
+ {
+ if (_name != value)
+ {
+ _name = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private int _rssi = 0;
+ public int Rssi
+ {
+ get => _rssi;
+ set
+ {
+ if (_rssi != value)
+ {
+ _rssi = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private bool _isConnectable = false;
+ public bool IsConnectable
+ {
+ get => _isConnectable;
+ set
+ {
+ if (_isConnectable != value)
+ {
+ _isConnectable = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private DeviceState _state = DeviceState.Disconnected;
+ public DeviceState State
+ {
+ get => _state;
+ set
+ {
+ if (_state != value)
+ {
+ _state = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ public IReadOnlyList AdvertisementRecords;
+ public string Adverts
+ {
+ get => String.Join('\n', AdvertisementRecords.Select(advert => $"{advert.Type}: 0x{Convert.ToHexString(advert.Data)}"));
+ }
+
+ public BLEDeviceViewModel(IDevice device)
+ {
+ Update(device);
+ }
+
+ public void Update(IDevice device)
+ {
+ DeviceId = device.Id;
+ Name = device.Name;
+ Rssi = device.Rssi;
+ IsConnectable = device.IsConnectable;
+ AdvertisementRecords = device.AdvertisementRecords;
+ State = device.State;
+ }
+
+ public override string ToString()
+ {
+ var advertData = new StringBuilder();
+ foreach(var advert in AdvertisementRecords)
+ {
+ advertData.Append($"|{advert.Type}: 0x{Convert.ToHexString(advert.Data)}|");
+ }
+
+ return $"{Name}:{DeviceId}: Adverts: '{advertData}'";
+ }
+ }
+}
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/ViewModels/BLEScannerViewModel.cs b/Source/BLE.Client/BLE.Client.Maui/ViewModels/BLEScannerViewModel.cs
new file mode 100644
index 00000000..4245154a
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/ViewModels/BLEScannerViewModel.cs
@@ -0,0 +1,253 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Windows.Input;
+using Plugin.BLE;
+using Plugin.BLE.Abstractions.Contracts;
+using Plugin.BLE.Abstractions.EventArgs;
+using Plugin.BLE.Abstractions.Extensions;
+
+namespace BLE.Client.Maui.ViewModels
+{
+ public class BLEScannerViewModel : BaseViewModel
+ {
+ private readonly IBluetoothLE _bluetoothManager;
+ protected IAdapter Adapter;
+
+ public ObservableCollection BLEDevices { get; private init; } = [];
+
+ private bool _isScanning = false;
+ public bool IsScanning
+ {
+ get => _isScanning;
+ protected set
+ {
+ if (_isScanning != value) {
+ _isScanning = value;
+ DebugMessage($"Set IsScanning to {value}");
+ RaisePropertyChanged(nameof(IsScanning));
+ RaisePropertyChanged(nameof(Waiting));
+ RaisePropertyChanged(nameof(ScanState));
+ RaisePropertyChanged(nameof(ToggleScanningCmdLabelText));
+ }
+ }
+ }
+
+ #region Derived properties
+ public bool IsStateOn => _bluetoothManager.IsOn;
+ public string StateText => GetStateText();
+ public bool Waiting => !_isScanning;
+ public string ScanState => IsScanning ? "Scanning" : "Waiting";
+ public string ToggleScanningCmdLabelText => IsScanning ? "Cancel" : "Start Scan";
+ #endregion Derived properties
+
+ public BLEScannerViewModel()
+ {
+ _bluetoothManager = CrossBluetoothLE.Current;
+ Adapter = _bluetoothManager?.Adapter;
+
+ if (_bluetoothManager is null)
+ {
+ ShowMessage("BluetoothManager is null");
+ }
+ else if (Adapter is null)
+ {
+ ShowMessage("Adapter is null");
+ }
+ else
+ {
+ ConfigureBLE();
+ }
+
+ ToggleScanning = new Command(ToggleScanForDevices);
+ }
+
+ private string GetStateText()
+ {
+ var result = "Unknown BLE state.";
+ switch (_bluetoothManager.State)
+ {
+ case BluetoothState.Unknown:
+ result = "Unknown BLE state.";
+ break;
+ case BluetoothState.Unavailable:
+ result = "BLE is not available on this device.";
+ break;
+ case BluetoothState.Unauthorized:
+ result = "You are not allowed to use BLE.";
+ break;
+ case BluetoothState.TurningOn:
+ result = "BLE is warming up, please wait.";
+ break;
+ case BluetoothState.On:
+ result = "BLE is on.";
+ break;
+ case BluetoothState.TurningOff:
+ result = "BLE is turning off. That's sad!";
+ break;
+ case BluetoothState.Off:
+ result = "BLE is off. Turn it on!";
+ break;
+ }
+ return result;
+ }
+
+ private void ShowMessage(string message)
+ {
+ DebugMessage(message);
+ App.AlertSvc.ShowAlert("BLE Scanner", message);
+ }
+
+ private void DebugMessage(string message)
+ {
+ Debug.WriteLine(message);
+ App.Logger.AddMessage(message);
+ }
+
+ private void ConfigureBLE()
+ {
+ DebugMessage("Configuring BLE...");
+ _bluetoothManager.StateChanged += OnBluetoothStateChanged;
+
+ // Set up scanner
+ Adapter.ScanMode = ScanMode.LowLatency;
+ Adapter.ScanTimeout = 30000; // ms
+ Adapter.ScanTimeoutElapsed += Adapter_ScanTimeoutElapsed;
+ Adapter.DeviceAdvertised += OnDeviceAdvertised;
+ Adapter.DeviceDiscovered += OnDeviceDiscovered;
+ DebugMessage("Configuring BLE... DONE");
+ }
+ private void OnBluetoothStateChanged(object sender, BluetoothStateChangedArgs e)
+ {
+ DebugMessage("OnBluetoothStateChanged from " + e.OldState + " to " + e.NewState);
+ RaisePropertyChanged(nameof(IsStateOn));
+ RaisePropertyChanged(nameof(StateText));
+ }
+
+ #region Scan & Discover
+ public ICommand ToggleScanning { get; init; }
+ CancellationTokenSource _scanCancellationTokenSource = null;
+
+ private void Adapter_ScanTimeoutElapsed(object sender, EventArgs e)
+ {
+ DebugMessage("Adapter_ScanTimeoutElapsed");
+ // Cleanup will happen inside ScanForDevicesAsync
+ }
+
+ private void OnDeviceAdvertised(object sender, DeviceEventArgs args)
+ {
+ DebugMessage("OnDeviceAdvertised");
+ AddOrUpdateDevice(args.Device);
+ DebugMessage("OnDeviceAdvertised done");
+ }
+ private void OnDeviceDiscovered(object sender, DeviceEventArgs args)
+ {
+ DebugMessage("OnDeviceDiscovered");
+ AddOrUpdateDevice(args.Device);
+ DebugMessage("OnDeviceDiscovered done");
+ }
+
+ private void AddOrUpdateDevice(IDevice device)
+ {
+ MainThread.BeginInvokeOnMainThread(() => {
+ var vm = BLEDevices.FirstOrDefault(d => d.DeviceId == device.Id);
+ if (vm != null)
+ {
+ DebugMessage($"Update Device: {device.Id}");
+ vm.Update(device);
+ }
+ else
+ {
+ DebugMessage($"Add Device: {device.Id}");
+ vm = new BLEDeviceViewModel(device);
+ BLEDevices.Add(vm);
+ }
+ });
+ }
+
+ private void ToggleScanForDevices()
+ {
+ if (!IsScanning)
+ {
+ IsScanning = true;
+ DebugMessage($"Starting Scanning");
+ ScanForDevicesAsync();
+ DebugMessage($"Started Scan");
+ }
+ else
+ {
+ DebugMessage($"Request Stopping Scan");
+ _scanCancellationTokenSource?.Cancel();
+ DebugMessage($"Stop Scanning Requested");
+ }
+ }
+ private async void ScanForDevicesAsync()
+ {
+ if (!IsStateOn)
+ {
+ ShowMessage("Bluetooth is not ON.\nPlease turn on Bluetooth and try again.");
+ IsScanning = false;
+ return;
+ }
+ if (!await HasCorrectPermissions())
+ {
+ DebugMessage("Aborting scan attempt");
+ IsScanning = false;
+ return;
+ }
+ DebugMessage("StartScanForDevices called");
+ BLEDevices.Clear();
+ await UpdateConnectedDevices();
+
+ _scanCancellationTokenSource = new();
+
+ DebugMessage("call Adapter.StartScanningForDevicesAsync");
+ await Adapter.StartScanningForDevicesAsync(_scanCancellationTokenSource.Token);
+ DebugMessage("back from Adapter.StartScanningForDevicesAsync");
+
+ // Scanning stopped (for whichever reason) -> cleanup
+ _scanCancellationTokenSource.Dispose();
+ _scanCancellationTokenSource = null;
+ IsScanning = false;
+ }
+
+ private async Task HasCorrectPermissions()
+ {
+ DebugMessage("Verifying Bluetooth permissions..");
+ var permissionResult = await Permissions.CheckStatusAsync();
+ if (permissionResult != PermissionStatus.Granted)
+ {
+ permissionResult = await Permissions.RequestAsync();
+ }
+ DebugMessage($"Result of requesting Bluetooth permissions: '{permissionResult}'");
+ if (permissionResult != PermissionStatus.Granted)
+ {
+ DebugMessage("Permissions not available, direct user to settings screen.");
+ ShowMessage("Permission denied. Not scanning.");
+ AppInfo.ShowSettingsUI();
+ return false;
+ }
+
+ return true;
+ }
+
+ private async Task UpdateConnectedDevices()
+ {
+ foreach (var connectedDevice in Adapter.ConnectedDevices)
+ {
+ //update rssi for already connected devices (so that 0 is not shown in the list)
+ try
+ {
+ await connectedDevice.UpdateRssiAsync();
+ }
+ catch (Exception ex)
+ {
+ ShowMessage($"Failed to update RSSI for {connectedDevice.Name}. Error: {ex.Message}");
+ }
+
+ AddOrUpdateDevice(connectedDevice);
+ }
+ }
+
+ #endregion Scan & Discover
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.Maui/ViewModels/BaseViewModel.cs b/Source/BLE.Client/BLE.Client.Maui/ViewModels/BaseViewModel.cs
new file mode 100644
index 00000000..7d3ee12f
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/ViewModels/BaseViewModel.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace BLE.Client.Maui.ViewModels;
+
+public abstract class BaseViewModel : INotifyPropertyChanged
+{
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.Maui/ViewModels/LogViewModel.cs b/Source/BLE.Client/BLE.Client.Maui/ViewModels/LogViewModel.cs
new file mode 100644
index 00000000..625538be
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/ViewModels/LogViewModel.cs
@@ -0,0 +1,16 @@
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+
+namespace BLE.Client.Maui.ViewModels;
+
+public class LogViewModel
+{
+ public ICommand ClearLogMessages { get; init; } = new Command(ClearMessages);
+
+ public static ReadOnlyObservableCollection Messages => App.Logger.Messages;
+
+ private static void ClearMessages()
+ {
+ App.Logger.ClearMessages();
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.Maui/Views/BLEScanner.xaml b/Source/BLE.Client/BLE.Client.Maui/Views/BLEScanner.xaml
new file mode 100644
index 00000000..22a633f3
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Views/BLEScanner.xaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Views/BLEScanner.xaml.cs b/Source/BLE.Client/BLE.Client.Maui/Views/BLEScanner.xaml.cs
new file mode 100644
index 00000000..8467eca0
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Views/BLEScanner.xaml.cs
@@ -0,0 +1,15 @@
+using BLE.Client.Maui.ViewModels;
+namespace BLE.Client.Maui.Views;
+
+public partial class BLEScanner : ContentPage
+{
+ private readonly BLEScannerViewModel _viewModel;
+ public BLEScanner()
+ {
+ InitializeComponent();
+ _viewModel = new();
+ BindingContext = _viewModel;
+
+ }
+
+}
\ No newline at end of file
diff --git a/Source/BLE.Client/BLE.Client.Maui/Views/LogViewer.xaml b/Source/BLE.Client/BLE.Client.Maui/Views/LogViewer.xaml
new file mode 100644
index 00000000..8f45e491
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Views/LogViewer.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.Maui/Views/LogViewer.xaml.cs b/Source/BLE.Client/BLE.Client.Maui/Views/LogViewer.xaml.cs
new file mode 100644
index 00000000..531e25b1
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.Maui/Views/LogViewer.xaml.cs
@@ -0,0 +1,14 @@
+using BLE.Client.Maui.ViewModels;
+
+namespace BLE.Client.Maui.Views;
+
+public partial class LogViewer : ContentPage
+{
+ private readonly LogViewModel logViewModel = new();
+
+ public LogViewer ()
+ {
+ InitializeComponent();
+ BindingContext = logViewModel;
+ }
+}
\ No newline at end of file
diff --git a/Source/BLE.Client/BLE.Client.UWP/BLE.Client.UWP.csproj b/Source/BLE.Client/BLE.Client.UWP/BLE.Client.UWP.csproj
index fb0c1de0..7a4127fc 100644
--- a/Source/BLE.Client/BLE.Client.UWP/BLE.Client.UWP.csproj
+++ b/Source/BLE.Client/BLE.Client.UWP/BLE.Client.UWP.csproj
@@ -17,7 +17,6 @@
512{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}true
- BLE.Client.UWP_TemporaryKey.pfxtrue
@@ -105,7 +104,6 @@
Designer
-
@@ -171,7 +169,7 @@
7.1.0.514
- 6.0.0
+ 7.0.06.2.14
@@ -183,16 +181,16 @@
8.0.2
- 2.11.0
+ 3.0.1
- 3.1.0
+ 7.0.02.0.0
- 5.0.0.2478
+ 5.0.0.2612
diff --git a/Source/BLE.Client/BLE.Client.WinConsole.Installer/BLE.Client.WinConsole.Installer.vdproj b/Source/BLE.Client/BLE.Client.WinConsole.Installer/BLE.Client.WinConsole.Installer.vdproj
new file mode 100644
index 00000000..14f2e748
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole.Installer/BLE.Client.WinConsole.Installer.vdproj
@@ -0,0 +1,731 @@
+"DeployProject"
+{
+"VSVersion" = "3:800"
+"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}"
+"IsWebType" = "8:FALSE"
+"ProjectName" = "8:BLE.Client.WinConsole.Installer"
+"LanguageId" = "3:1033"
+"CodePage" = "3:1252"
+"UILanguageId" = "3:1033"
+"SccProjectName" = "8:"
+"SccLocalPath" = "8:"
+"SccAuxPath" = "8:"
+"SccProvider" = "8:"
+ "Hierarchy"
+ {
+ "Entry"
+ {
+ "MsmKey" = "8:_8A3CB92018754E60ABC94DFEE1EC9976"
+ "OwnerKey" = "8:_UNDEFINED"
+ "MsmSig" = "8:_UNDEFINED"
+ }
+ }
+ "Configurations"
+ {
+ "Debug"
+ {
+ "DisplayName" = "8:Debug"
+ "IsDebugOnly" = "11:TRUE"
+ "IsReleaseOnly" = "11:FALSE"
+ "OutputFilename" = "8:Debug\\BLE.Client.WinConsole.Installer.msi"
+ "PackageFilesAs" = "3:2"
+ "PackageFileSize" = "3:-2147483648"
+ "CabType" = "3:1"
+ "Compression" = "3:2"
+ "SignOutput" = "11:FALSE"
+ "CertificateFile" = "8:"
+ "PrivateKeyFile" = "8:"
+ "TimeStampServer" = "8:"
+ "InstallerBootstrapper" = "3:2"
+ "BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
+ {
+ "Enabled" = "11:TRUE"
+ "PromptEnabled" = "11:TRUE"
+ "PrerequisitesLocation" = "2:1"
+ "Url" = "8:"
+ "ComponentsUrl" = "8:"
+ "Items"
+ {
+ "{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:Microsoft.NetCore.DesktopRuntime.8.0.x64"
+ {
+ "Name" = "8:.NET Desktop Runtime 8.0.3 (x64)"
+ "ProductCode" = "8:Microsoft.NetCore.DesktopRuntime.8.0.x64"
+ }
+ }
+ }
+ }
+ "Release"
+ {
+ "DisplayName" = "8:Release"
+ "IsDebugOnly" = "11:FALSE"
+ "IsReleaseOnly" = "11:TRUE"
+ "OutputFilename" = "8:Release\\BLE.Client.WinConsole.Installer.msi"
+ "PackageFilesAs" = "3:2"
+ "PackageFileSize" = "3:-2147483648"
+ "CabType" = "3:1"
+ "Compression" = "3:2"
+ "SignOutput" = "11:FALSE"
+ "CertificateFile" = "8:"
+ "PrivateKeyFile" = "8:"
+ "TimeStampServer" = "8:"
+ "InstallerBootstrapper" = "3:2"
+ "BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
+ {
+ "Enabled" = "11:TRUE"
+ "PromptEnabled" = "11:TRUE"
+ "PrerequisitesLocation" = "2:1"
+ "Url" = "8:"
+ "ComponentsUrl" = "8:"
+ }
+ }
+ }
+ "Deployable"
+ {
+ "CustomAction"
+ {
+ }
+ "DefaultFeature"
+ {
+ "Name" = "8:DefaultFeature"
+ "Title" = "8:"
+ "Description" = "8:"
+ }
+ "ExternalPersistence"
+ {
+ "LaunchCondition"
+ {
+ "{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_B98DDA069ECC4C7BA675DEB168EF4B1B"
+ {
+ "Name" = "8:.NET Core"
+ "Message" = "8:[VSDNETCOREMSG]"
+ "AllowLaterVersions" = "11:FALSE"
+ "InstallUrl" = "8:https://dotnet.microsoft.com/download/dotnet-core/[NetCoreVerMajorDotMinor]"
+ "IsNETCore" = "11:TRUE"
+ "Architecture" = "2:0"
+ "Runtime" = "2:0"
+ }
+ }
+ }
+ "File"
+ {
+ }
+ "FileType"
+ {
+ }
+ "Folder"
+ {
+ "{3C67513D-01DD-4637-8A68-80971EB9504F}:_53A832D5D5FC44948044A2EE06375438"
+ {
+ "DefaultLocation" = "8:[ProgramFiles64Folder][Manufacturer]\\[ProductName]"
+ "Name" = "8:#1925"
+ "AlwaysCreate" = "11:FALSE"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Property" = "8:TARGETDIR"
+ "Folders"
+ {
+ }
+ }
+ "{1525181F-901A-416C-8A58-119130FE478E}:_C8AC3B9072C640BABD8901A312F3D0DC"
+ {
+ "Name" = "8:#1916"
+ "AlwaysCreate" = "11:FALSE"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Property" = "8:DesktopFolder"
+ "Folders"
+ {
+ }
+ }
+ "{1525181F-901A-416C-8A58-119130FE478E}:_DC8C657D40314C6280170FB1216F225B"
+ {
+ "Name" = "8:#1919"
+ "AlwaysCreate" = "11:FALSE"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Property" = "8:ProgramMenuFolder"
+ "Folders"
+ {
+ }
+ }
+ }
+ "LaunchCondition"
+ {
+ }
+ "Locator"
+ {
+ }
+ "MsiBootstrapper"
+ {
+ "LangId" = "3:1033"
+ "RequiresElevation" = "11:FALSE"
+ }
+ "Product"
+ {
+ "Name" = "8:Microsoft Visual Studio"
+ "ProductName" = "8:BLE.Client.WinConsole"
+ "ProductCode" = "8:{5F449E95-7E87-4E64-B500-80A6AF1D97DA}"
+ "PackageCode" = "8:{B9329BAB-7161-4B70-A339-EFC0E40F2D22}"
+ "UpgradeCode" = "8:{2DC56EBC-822F-4B47-93C7-DCBD97994976}"
+ "AspNetVersion" = "8:4.0.30319.0"
+ "RestartWWWService" = "11:FALSE"
+ "RemovePreviousVersions" = "11:TRUE"
+ "DetectNewerInstalledVersion" = "11:TRUE"
+ "InstallAllUsers" = "11:FALSE"
+ "ProductVersion" = "8:1.0.2"
+ "Manufacturer" = "8:Plugin.BLE"
+ "ARPHELPTELEPHONE" = "8:"
+ "ARPHELPLINK" = "8:"
+ "Title" = "8:BLE Client WinConsole "
+ "Subject" = "8:"
+ "ARPCONTACT" = "8:Ask Bojesen"
+ "Keywords" = "8:"
+ "ARPCOMMENTS" = "8:v1.0.2"
+ "ARPURLINFOABOUT" = "8:https://github.com/dotnet-bluetooth-le/dotnet-bluetooth-le"
+ "ARPPRODUCTICON" = "8:"
+ "ARPIconIndex" = "3:0"
+ "SearchPath" = "8:"
+ "UseSystemSearchPath" = "11:TRUE"
+ "TargetPlatform" = "3:1"
+ "PreBuildEvent" = "8:"
+ "PostBuildEvent" = "8:"
+ "RunPostBuildEvent" = "3:0"
+ }
+ "Registry"
+ {
+ "HKLM"
+ {
+ "Keys"
+ {
+ "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_4B206CDA93494F9587D66B1F4B24AA4C"
+ {
+ "Name" = "8:Software"
+ "Condition" = "8:"
+ "AlwaysCreate" = "11:FALSE"
+ "DeleteAtUninstall" = "11:FALSE"
+ "Transitive" = "11:FALSE"
+ "Keys"
+ {
+ "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_8C2BD607D2084C3392D722606F6A9DDC"
+ {
+ "Name" = "8:[Manufacturer]"
+ "Condition" = "8:"
+ "AlwaysCreate" = "11:FALSE"
+ "DeleteAtUninstall" = "11:FALSE"
+ "Transitive" = "11:FALSE"
+ "Keys"
+ {
+ }
+ "Values"
+ {
+ }
+ }
+ }
+ "Values"
+ {
+ }
+ }
+ }
+ }
+ "HKCU"
+ {
+ "Keys"
+ {
+ "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_78ACCAEA90F94E7CBF01E36BF39F774E"
+ {
+ "Name" = "8:Software"
+ "Condition" = "8:"
+ "AlwaysCreate" = "11:FALSE"
+ "DeleteAtUninstall" = "11:FALSE"
+ "Transitive" = "11:FALSE"
+ "Keys"
+ {
+ "{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_8AE3135316C441A6A428CDE2BC1A21E3"
+ {
+ "Name" = "8:[Manufacturer]"
+ "Condition" = "8:"
+ "AlwaysCreate" = "11:FALSE"
+ "DeleteAtUninstall" = "11:FALSE"
+ "Transitive" = "11:FALSE"
+ "Keys"
+ {
+ }
+ "Values"
+ {
+ }
+ }
+ }
+ "Values"
+ {
+ }
+ }
+ }
+ }
+ "HKCR"
+ {
+ "Keys"
+ {
+ }
+ }
+ "HKU"
+ {
+ "Keys"
+ {
+ }
+ }
+ "HKPU"
+ {
+ "Keys"
+ {
+ }
+ }
+ }
+ "Sequences"
+ {
+ }
+ "Shortcut"
+ {
+ "{970C0BB2-C7D0-45D7-ABFA-7EC378858BC0}:_B6412D24240749AC80FE5779DD1FAE72"
+ {
+ "Name" = "8:BLE.Client.WinConsole"
+ "Arguments" = "8:"
+ "Description" = "8:"
+ "ShowCmd" = "3:1"
+ "IconIndex" = "3:0"
+ "Transitive" = "11:FALSE"
+ "Target" = "8:_8A3CB92018754E60ABC94DFEE1EC9976"
+ "Folder" = "8:_C8AC3B9072C640BABD8901A312F3D0DC"
+ "WorkingFolder" = "8:_53A832D5D5FC44948044A2EE06375438"
+ "Icon" = "8:"
+ "Feature" = "8:"
+ }
+ }
+ "UserInterface"
+ {
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_173BC258F76A4A6DBCAA6935B64EF703"
+ {
+ "Name" = "8:#1900"
+ "Sequence" = "3:1"
+ "Attributes" = "3:1"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_773BB445D0FA45F8B95F54A1E476F735"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Welcome"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdWelcomeDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "CopyrightWarning"
+ {
+ "Name" = "8:CopyrightWarning"
+ "DisplayName" = "8:#1002"
+ "Description" = "8:#1102"
+ "Type" = "3:3"
+ "ContextData" = "8:"
+ "Attributes" = "3:0"
+ "Setting" = "3:1"
+ "Value" = "8:#1202"
+ "DefaultValue" = "8:#1202"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "Welcome"
+ {
+ "Name" = "8:Welcome"
+ "DisplayName" = "8:#1003"
+ "Description" = "8:#1103"
+ "Type" = "3:3"
+ "ContextData" = "8:"
+ "Attributes" = "3:0"
+ "Setting" = "3:1"
+ "Value" = "8:#1203"
+ "DefaultValue" = "8:#1203"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_F5FD36DE78A84787B02785A1FAFB99FC"
+ {
+ "Sequence" = "3:200"
+ "DisplayName" = "8:Installation Folder"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdFolderDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "InstallAllUsersVisible"
+ {
+ "Name" = "8:InstallAllUsersVisible"
+ "DisplayName" = "8:#1059"
+ "Description" = "8:#1159"
+ "Type" = "3:5"
+ "ContextData" = "8:1;True=1;False=0"
+ "Attributes" = "3:0"
+ "Setting" = "3:0"
+ "Value" = "3:1"
+ "DefaultValue" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_FCCE4F694F7A48829140DD6439FE50A2"
+ {
+ "Sequence" = "3:300"
+ "DisplayName" = "8:Confirm Installation"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdConfirmDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_3737EBE5CA6D4104B13CBF871F5BB05D"
+ {
+ "Name" = "8:#1901"
+ "Sequence" = "3:1"
+ "Attributes" = "3:2"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_D63A3BFF9CA24CDB85DC4AD6D6941B2E"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Progress"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdProgressDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "ShowProgress"
+ {
+ "Name" = "8:ShowProgress"
+ "DisplayName" = "8:#1009"
+ "Description" = "8:#1109"
+ "Type" = "3:5"
+ "ContextData" = "8:1;True=1;False=0"
+ "Attributes" = "3:0"
+ "Setting" = "3:0"
+ "Value" = "3:1"
+ "DefaultValue" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_4939CDFA026E46F388FBA537AB2CC845"
+ {
+ "Name" = "8:#1900"
+ "Sequence" = "3:2"
+ "Attributes" = "3:1"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_9306FE08876C4DD0979CCE592FE51882"
+ {
+ "Sequence" = "3:200"
+ "DisplayName" = "8:Installation Folder"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdAdminFolderDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_D86ADB6FDDD14E86BE4F44E78D3D31B8"
+ {
+ "Sequence" = "3:300"
+ "DisplayName" = "8:Confirm Installation"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdAdminConfirmDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_D8EC8F2720834F9FB304E10AF1F7687C"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Welcome"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdAdminWelcomeDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "CopyrightWarning"
+ {
+ "Name" = "8:CopyrightWarning"
+ "DisplayName" = "8:#1002"
+ "Description" = "8:#1102"
+ "Type" = "3:3"
+ "ContextData" = "8:"
+ "Attributes" = "3:0"
+ "Setting" = "3:1"
+ "Value" = "8:#1202"
+ "DefaultValue" = "8:#1202"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "Welcome"
+ {
+ "Name" = "8:Welcome"
+ "DisplayName" = "8:#1003"
+ "Description" = "8:#1103"
+ "Type" = "3:3"
+ "ContextData" = "8:"
+ "Attributes" = "3:0"
+ "Setting" = "3:1"
+ "Value" = "8:#1203"
+ "DefaultValue" = "8:#1203"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_574AFB5E74304D999110525E6333460E"
+ {
+ "Name" = "8:#1902"
+ "Sequence" = "3:1"
+ "Attributes" = "3:3"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_E453D62D238E4572A71FD8E553406181"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Finished"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdFinishedDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "UpdateText"
+ {
+ "Name" = "8:UpdateText"
+ "DisplayName" = "8:#1058"
+ "Description" = "8:#1158"
+ "Type" = "3:15"
+ "ContextData" = "8:"
+ "Attributes" = "3:0"
+ "Setting" = "3:1"
+ "Value" = "8:#1258"
+ "DefaultValue" = "8:#1258"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_577EC3D50B084F5B864E1A2DFB5B3BF5"
+ {
+ "UseDynamicProperties" = "11:FALSE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdBasicDialogs.wim"
+ }
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_8AFD094F6D3E47CF8105BFAB4C3AA967"
+ {
+ "Name" = "8:#1902"
+ "Sequence" = "3:2"
+ "Attributes" = "3:3"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_8DB9DB0F41D94153B1E65C511D46CFB4"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Finished"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdAdminFinishedDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{DF760B10-853B-4699-99F2-AFF7185B4A62}:_AC604DF7CFEE44A5B8C66E51F5CDB0AB"
+ {
+ "Name" = "8:#1901"
+ "Sequence" = "3:2"
+ "Attributes" = "3:2"
+ "Dialogs"
+ {
+ "{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_B7CA052E0E894BA9A06C5AD6C2821A5D"
+ {
+ "Sequence" = "3:100"
+ "DisplayName" = "8:Progress"
+ "UseDynamicProperties" = "11:TRUE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdAdminProgressDlg.wid"
+ "Properties"
+ {
+ "BannerBitmap"
+ {
+ "Name" = "8:BannerBitmap"
+ "DisplayName" = "8:#1001"
+ "Description" = "8:#1101"
+ "Type" = "3:8"
+ "ContextData" = "8:Bitmap"
+ "Attributes" = "3:4"
+ "Setting" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ "ShowProgress"
+ {
+ "Name" = "8:ShowProgress"
+ "DisplayName" = "8:#1009"
+ "Description" = "8:#1109"
+ "Type" = "3:5"
+ "ContextData" = "8:1;True=1;False=0"
+ "Attributes" = "3:0"
+ "Setting" = "3:0"
+ "Value" = "3:1"
+ "DefaultValue" = "3:1"
+ "UsePlugInResources" = "11:TRUE"
+ }
+ }
+ }
+ }
+ }
+ "{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_D3F3E5884DCD4727B97DDB31EC46F3DC"
+ {
+ "UseDynamicProperties" = "11:FALSE"
+ "IsDependency" = "11:FALSE"
+ "SourcePath" = "8:\\VsdUserInterface.wim"
+ }
+ }
+ "MergeModule"
+ {
+ }
+ "ProjectOutput"
+ {
+ "{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_8A3CB92018754E60ABC94DFEE1EC9976"
+ {
+ "SourcePath" = "8:..\\BLE.Client.WinConsole\\obj\\Release\\net8.0-windows10.0.22621.0\\apphost.exe"
+ "TargetName" = "8:"
+ "Tag" = "8:"
+ "Folder" = "8:_53A832D5D5FC44948044A2EE06375438"
+ "Condition" = "8:"
+ "Transitive" = "11:FALSE"
+ "Vital" = "11:TRUE"
+ "ReadOnly" = "11:FALSE"
+ "Hidden" = "11:FALSE"
+ "System" = "11:FALSE"
+ "Permanent" = "11:FALSE"
+ "SharedLegacy" = "11:FALSE"
+ "PackageAs" = "3:1"
+ "Register" = "3:1"
+ "Exclude" = "11:FALSE"
+ "IsDependency" = "11:FALSE"
+ "IsolateTo" = "8:"
+ "ProjectOutputGroupRegister" = "3:1"
+ "OutputConfiguration" = "8:"
+ "OutputGroupCanonicalName" = "8:PublishItems"
+ "OutputProjectGuid" = "8:{39287F60-65DE-4077-90F2-57CF725E92B6}"
+ "ShowKeyOutput" = "11:TRUE"
+ "ExcludeFilters"
+ {
+ }
+ }
+ }
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.WinConsole/BLE.Client.WinConsole.csproj b/Source/BLE.Client/BLE.Client.WinConsole/BLE.Client.WinConsole.csproj
new file mode 100644
index 00000000..1b68f6ed
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole/BLE.Client.WinConsole.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0-windows10.0.22621.0
+ disable
+ enable
+ 1.0.2
+
+
+
+
+
+
+
diff --git a/Source/BLE.Client/BLE.Client.WinConsole/BleAddressSelector.cs b/Source/BLE.Client/BLE.Client.WinConsole/BleAddressSelector.cs
new file mode 100644
index 00000000..fdbac410
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole/BleAddressSelector.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BLE.Client.WinConsole
+{
+ public static class BleAddressSelector
+ {
+ static string bleaddressTxtPath = Path.Combine(Path.GetTempPath(), "bleaddress.txt");
+ static string? bleaddress = null;
+
+ public static bool DoesBleAddressExists()
+ {
+ if (File.Exists(bleaddressTxtPath))
+ {
+ bleaddress = File.ReadAllText(bleaddressTxtPath);
+ return true;
+ }
+ return false;
+ }
+
+ public static string GetBleAddress()
+ {
+ if (bleaddress is null)
+ {
+ if (File.Exists(bleaddressTxtPath))
+ {
+ bleaddress = File.ReadAllText(bleaddressTxtPath);
+ }
+ else
+ {
+ NewBleAddress();
+ }
+ }
+ if (bleaddress is null)
+ {
+ throw new Exception("BleAddressSelector says bleaddress is null");
+ }
+ return bleaddress;
+ }
+
+ public static void SetBleAddress(string? bleaddressIn)
+ {
+ if (bleaddressIn is null || bleaddressIn.Length != 12)
+ {
+ Console.WriteLine("Wrong BLE Address entered");
+ throw new Exception("Wrong BLE Address entered");
+ }
+ bleaddress = bleaddressIn.ToUpperInvariant();
+ File.WriteAllText(bleaddressTxtPath, bleaddress);
+ }
+
+ public static Task NewBleAddress()
+ {
+ Console.Write("Enter BLE Address (12 hex chars): ");
+ SetBleAddress(Console.ReadLine());
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.WinConsole/ConsoleTracer.cs b/Source/BLE.Client/BLE.Client.WinConsole/ConsoleTracer.cs
new file mode 100644
index 00000000..db7344fb
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole/ConsoleTracer.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BLE.Client.WinConsole
+{
+ ///
+ /// A class, which can log trace to the console without blocking the caller
+ ///
+ public class ConsoleTracer : IDisposable
+ {
+ record Entry
+ (
+ DateTime Time,
+ string Format,
+ object[] Args
+ );
+
+ private readonly DateTime time0;
+ private readonly Stopwatch stopwatch;
+ private readonly Task worker;
+ private bool disposing = false;
+ private readonly AutoResetEvent newEntry = new AutoResetEvent(false);
+ ConcurrentQueue entries = new ConcurrentQueue();
+
+ public ConsoleTracer()
+ {
+ time0 = DateTime.Now;
+ stopwatch = Stopwatch.StartNew();
+ worker = new Task(WriteWorker);
+ worker.Start();
+ }
+
+ ///
+ /// Trace something to the console - adding to queue - not blocking the caller
+ ///
+ ///
+ ///
+ public void Trace(string format, params object[] args)
+ {
+ var time = GetTime();
+ entries.Enqueue(new Entry(time, format, args));
+ newEntry.Set();
+ }
+
+ ///
+ /// Get a tracer with a prefix
+ ///
+ ///
+ ///
+ public Action GetPrefixedTrace(string prefix)
+ {
+ return new Action((format, args) => Trace(prefix + " - " + format, args));
+ }
+
+ void WriteWorker()
+ {
+ while (!disposing && newEntry.WaitOne())
+ {
+ while (entries.TryDequeue(out Entry entry))
+ {
+ //Console.WriteLine(entry.Time.ToString("yyyy-MM-ddTHH:mm:ss.fff ") + entry.Format + " ", entry.Args);
+ Console.WriteLine(entry.Time.ToString("HH:mm:ss.fff ") + entry.Format + " ", entry.Args);
+ }
+ }
+ Console.WriteLine("Console Tracer is Finished.");
+ }
+
+ private DateTime GetTime()
+ {
+ return time0.AddTicks(stopwatch.ElapsedTicks);
+ }
+
+ public void Dispose()
+ {
+ disposing = true;
+ newEntry.Set();
+ worker.Wait(100);
+ }
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.WinConsole/Demo.cs b/Source/BLE.Client/BLE.Client.WinConsole/Demo.cs
new file mode 100644
index 00000000..16dc0353
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole/Demo.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+public record Demo
+(
+ string Description,
+ Func Method
+);
+
diff --git a/Source/BLE.Client/BLE.Client.WinConsole/PluginDemos.cs b/Source/BLE.Client/BLE.Client.WinConsole/PluginDemos.cs
new file mode 100644
index 00000000..bc37e754
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole/PluginDemos.cs
@@ -0,0 +1,499 @@
+using Plugin.BLE;
+using Plugin.BLE.Abstractions;
+using Plugin.BLE.Abstractions.Contracts;
+using Plugin.BLE.Abstractions.Extensions;
+using Plugin.BLE.Extensions;
+using Plugin.BLE.Windows;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Enumeration;
+
+namespace BLE.Client.WinConsole
+{
+ internal class PluginDemos
+ {
+ private readonly IBluetoothLE bluetoothLE;
+ public IAdapter Adapter { get; }
+ private readonly Action? writer;
+ private readonly List discoveredDevices;
+ private bool scanningDone = false;
+ private ConsoleKey consoleKey = ConsoleKey.None;
+ private IDevice? reconnectDevice;
+ private CancellationTokenSource escKeyCancellationTokenSource = new CancellationTokenSource();
+
+ public PluginDemos(Action? writer = null)
+ {
+ discoveredDevices = new List();
+ bluetoothLE = CrossBluetoothLE.Current;
+ Adapter = CrossBluetoothLE.Current.Adapter;
+ Adapter.DeviceConnected += Adapter_DeviceConnected;
+ Adapter.DeviceDisconnected += Adapter_DeviceDisconnected;
+ Adapter.DeviceConnectionLost += Adapter_DeviceConnectionLost;
+ Adapter.DeviceConnectionError += Adapter_DeviceConnectionError;
+ this.writer = writer;
+ }
+
+ private void Adapter_DeviceConnectionError(object? sender, Plugin.BLE.Abstractions.EventArgs.DeviceErrorEventArgs e)
+ {
+ Write($"Adapter_DeviceConnectionError {e.Device.Id.ToHexBleAddress()} with name: {e.Device.Name}");
+ }
+
+ private void Adapter_DeviceDisconnected(object? sender, Plugin.BLE.Abstractions.EventArgs.DeviceEventArgs e)
+ {
+ Write($"Adapter_DeviceDisconnected {e.Device.Id.ToHexBleAddress()} with name: {e.Device.Name}");
+ }
+
+ private void Adapter_DeviceConnected(object? sender, Plugin.BLE.Abstractions.EventArgs.DeviceEventArgs e)
+ {
+ Write($"Adapter_DeviceConnected {e.Device.Id.ToHexBleAddress()} with name: {e.Device.Name}");
+ }
+
+ private void Write(string format, params object[] args)
+ {
+ writer?.Invoke(format, args);
+ }
+
+ public async Task TurnBluetoothOn()
+ {
+ await bluetoothLE.TrySetStateAsync(true);
+ }
+
+ public async Task TurnBluetoothOff()
+ {
+ await bluetoothLE.TrySetStateAsync(false);
+ }
+
+ public IDevice ConnectToKnown(Guid id)
+ {
+ IDevice dev = Adapter.ConnectToKnownDeviceAsync(id).Result;
+ return dev;
+ }
+
+ public async Task Connect_Disconnect()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ var connectParameters = new ConnectParameters(connectionParameterSet: ConnectionParameterSet.ThroughputOptimized);
+ IDevice dev = await Adapter.ConnectToKnownDeviceAsync(id, connectParameters);
+ Write("Waiting 5 secs");
+ await Task.Delay(5000);
+ Write("Disconnecting");
+ await Adapter.DisconnectDeviceAsync(dev);
+ dev.Dispose();
+ Write("Test_Connect_Disconnect done");
+ }
+
+ public async Task ShowBondState()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ IDevice dev = await Adapter.ConnectToKnownDeviceAsync(id);
+ Write("BondState: " + dev.BondState);
+ dev.Dispose();
+ }
+
+ public async Task Connect_Read_Services_Disconnect_Loop()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ var connectParameters = new ConnectParameters(connectionParameterSet: ConnectionParameterSet.Balanced);
+ new Task(ConsoleKeyReader).Start();
+ using (IDevice dev = await Adapter.ConnectToKnownDeviceAsync(id, connectParameters))
+ {
+ int count = 1;
+ while(true)
+ {
+ await Task.Delay(100);
+ Write($"---------------- {count++} ------- (Esc to stop) ------");
+ if (dev.State != DeviceState.Connected)
+ {
+ Write("Connecting");
+ await Adapter.ConnectToDeviceAsync(dev);
+ }
+ Write("Reading services");
+
+ var services = await dev.GetServicesAsync();
+ List charlist = new List();
+ foreach (var service in services)
+ {
+ var characteristics = await service.GetCharacteristicsAsync();
+ charlist.AddRange(characteristics);
+ }
+
+ foreach (var service in services)
+ {
+ service.Dispose();
+ }
+ charlist.Clear();
+ Write("Waiting 3 secs");
+ await Task.Delay(3000);
+ Write("Disconnecting");
+ await Adapter.DisconnectDeviceAsync(dev);
+ Write("Test_Connect_Disconnect done");
+ if (consoleKey == ConsoleKey.Escape)
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ public async Task Connect_Read_Services_Dispose_Loop()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ var connectParameters = new ConnectParameters(connectionParameterSet: ConnectionParameterSet.Balanced);
+ new Task(ConsoleKeyReader).Start();
+ int count = 1;
+ while (true)
+ {
+ await Task.Delay(100);
+ Write($"---------------- {count++} ------- (Esc to stop) ------");
+ IDevice dev = await Adapter.ConnectToKnownDeviceAsync(id, connectParameters);
+ Write("Reading services");
+ var services = await dev.GetServicesAsync();
+ List charlist = new List();
+ foreach (var service in services)
+ {
+ var characteristics = await service.GetCharacteristicsAsync();
+ charlist.AddRange(characteristics);
+ foreach (Characteristic characteristic in characteristics)
+ {
+ if (characteristic.Properties.HasFlag(CharacteristicPropertyType.Indicate)
+ || characteristic.Properties.HasFlag(CharacteristicPropertyType.Notify))
+ {
+ Write($"Characteristic.Properties: {characteristic.Properties}");
+ try
+ {
+ await characteristic.StartUpdatesAsync();
+ } catch { }
+ }
+ }
+ }
+
+ foreach (var service in services)
+ {
+ service.Dispose();
+ }
+ foreach (Characteristic characteristic in charlist)
+ {
+ try
+ {
+ await characteristic.StopUpdatesAsync();
+ } catch { }
+ }
+ charlist.Clear();
+ Write("Waiting 3 secs");
+ await Task.Delay(3000);
+ await Adapter.DisconnectDeviceAsync(dev);
+ Write("Disposing");
+ dev.Dispose();
+ }
+ }
+
+ private void ConsoleKeyReader()
+ {
+ while (consoleKey != ConsoleKey.Escape)
+ {
+ consoleKey = Console.ReadKey().Key;
+ }
+ Write("Escape key pressed - stopping...");
+ escKeyCancellationTokenSource.Cancel();
+ }
+
+ private async Task ConnectWorker(Guid id)
+ {
+ while (consoleKey != ConsoleKey.Escape)
+ {
+ try
+ {
+ Write("Trying to connect to device (Escape key to abort)");
+ reconnectDevice = await Adapter.ConnectToKnownDeviceAsync(id, cancellationToken: escKeyCancellationTokenSource.Token);
+ Write("Reading all services and characteristics");
+ var services = await reconnectDevice.GetServicesAsync();
+ List characteristics = new List();
+ foreach (var service in services)
+ {
+ var newcharacteristics = await service.GetCharacteristicsAsync();
+ characteristics.AddRange(newcharacteristics);
+ }
+ await Task.Delay(1000);
+ Write(new string('-', 80));
+ Write("Connected successfully!");
+ Write("To test connection lost: Move the device out of range / power off the device");
+ Write(new string('-', 80));
+ break;
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ public async Task Connect_ConnectionLost_Reconnect()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ var consoleReaderTask = new Task(ConsoleKeyReader);
+ consoleReaderTask.Start();
+ await ConnectWorker(id);
+ consoleReaderTask.Wait();
+ }
+
+ private async void Adapter_DeviceConnectionLost(object? sender, Plugin.BLE.Abstractions.EventArgs.DeviceErrorEventArgs e)
+ {
+ Write($"Adapter_DeviceConnectionLost {e.Device.Id.ToHexBleAddress()} with name: {e.Device.Name}");
+ if (reconnectDevice is not null && reconnectDevice.Id == e.Device.Id)
+ {
+ reconnectDevice.Dispose();
+ reconnectDevice = null;
+ await Task.Delay(1000);
+ Write(new string('-', 80));
+ Write("Lost connection!");
+ Write("To test reconnect: Move the device back in range / power on the device");
+ Write(new string('-', 80));
+ _ = ConnectWorker(e.Device.Id);
+ }
+ }
+
+ public async Task Connect_Change_Parameters_Disconnect()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ var connectParameters = new ConnectParameters(connectionParameterSet: ConnectionParameterSet.Balanced);
+ IDevice dev = await Adapter.ConnectToKnownDeviceAsync(id, connectParameters);
+ Write("Waiting 5 secs");
+ await Task.Delay(5000);
+ connectParameters = new ConnectParameters(connectionParameterSet: ConnectionParameterSet.ThroughputOptimized);
+ dev.UpdateConnectionParameters(connectParameters);
+ Write("Waiting 5 secs");
+ await Task.Delay(5000);
+ connectParameters = new ConnectParameters(connectionParameterSet: ConnectionParameterSet.Balanced);
+ dev.UpdateConnectionParameters(connectParameters);
+ Write("Waiting 5 secs");
+ await Task.Delay(5000);
+ Write("Disconnecting");
+ await Adapter.DisconnectDeviceAsync(dev);
+ dev.Dispose();
+ Write("Test_Connect_Disconnect done");
+ }
+
+ public async Task BondAsync()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ IDevice dev = await Adapter.ConnectToKnownDeviceAsync(id);
+ await Adapter.BondAsync(dev);
+ }
+
+ public Task GetBondedDevices()
+ {
+ int idx = 0;
+ foreach (var dev in Adapter.BondedDevices)
+ {
+ Write($"{idx++} Bonded device: {dev.Name} : {dev.Id}");
+ }
+ return Task.FromResult(true);
+ }
+
+ public async Task Pair_Connect_Disconnect()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ var id = bleaddress.ToBleDeviceGuid();
+ ulong bleAddressulong = id.ToBleAddress();
+ DeviceInformation? deviceInformation = null;
+ using (BluetoothLEDevice nativeDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(bleAddressulong))
+ {
+ deviceInformation = await DeviceInformation.CreateFromIdAsync(nativeDevice.DeviceId);
+ }
+
+ if (!deviceInformation.Pairing.IsPaired && deviceInformation.Pairing.CanPair)
+ {
+ Write("Starting custom pairing...");
+ deviceInformation.Pairing.Custom.PairingRequested += Custom_PairingRequested;
+ DevicePairingResult result = await deviceInformation.Pairing.Custom.PairAsync(DevicePairingKinds.ConfirmOnly, DevicePairingProtectionLevel.Encryption);
+ Write("Pairing result: " + result.Status);
+ }
+ else
+ {
+ Write("Already paired");
+ }
+ Write("Calling Adapter.ConnectToKnownDeviceAsync");
+ IDevice dev = await Adapter.ConnectToKnownDeviceAsync(id);
+ Write($"Calling Adapter.ConnectToKnownDeviceAsync done with {dev.Name}");
+ await Task.Delay(1000);
+ await dev.RequestMtuAsync(517);
+ Write("Waiting 3 secs");
+ await Task.Delay(3000);
+ Write("Disconnecting");
+ await Adapter.DisconnectDeviceAsync(dev);
+ dev.Dispose();
+ Write("Custom_Pair_Connect_Disconnect done");
+ }
+
+ private void Custom_PairingRequested(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs args)
+ {
+ Write("Custom_PairingRequested -> Accept");
+ args.Accept();
+ }
+
+ public async Task DoTheScanning(ScanMode scanMode = ScanMode.LowPower, int time_ms = 2000)
+ {
+
+ if (!bluetoothLE.IsOn)
+ {
+ Write("Bluetooth is not On - it is {0}", bluetoothLE.State);
+ return;
+ }
+ Write("Bluetooth is on");
+ Write("Scanning now for " + time_ms + " ms...");
+ var cancellationTokenSource = new CancellationTokenSource(time_ms);
+ discoveredDevices.Clear();
+
+ int index = 1;
+ Adapter.DeviceDiscovered += (s, a) =>
+ {
+ if (scanningDone)
+ {
+ return;
+ }
+ var dev = a.Device;
+ Write($"{index++}: DeviceDiscovered: {0} with Name = {1}", dev.Id.ToHexBleAddress(), dev.Name);
+ discoveredDevices.Add(a.Device);
+ };
+ Adapter.ScanMode = scanMode;
+ await Adapter.StartScanningForDevicesAsync(cancellationToken: cancellationTokenSource.Token);
+ scanningDone = true;
+ }
+
+ internal async Task DiscoverAndSelect()
+ {
+ if (!bluetoothLE.IsOn)
+ {
+ Console.WriteLine("Bluetooth is off - cannot discover");
+ return;
+ }
+ await DoTheScanning();
+ int index = 1;
+ await Task.Delay(200);
+ Console.WriteLine();
+ foreach (var dev in discoveredDevices)
+ {
+ Console.WriteLine($"{index++}: {dev.Id.ToHexBleAddress()} with Name = {dev.Name}");
+ }
+ if (discoveredDevices.Count == 0)
+ {
+ Console.Write("NO BLE Devices discovered");
+ return;
+ }
+ Console.WriteLine();
+ Console.Write($"Select BLE address index with value {1} to {discoveredDevices.Count}: ");
+ if (int.TryParse(Console.ReadLine(), out int selectedIndex))
+ {
+ IDevice selecteddev = discoveredDevices[selectedIndex - 1];
+ Console.WriteLine($"Selected {selectedIndex}: {selecteddev.Id.ToHexBleAddress()} with Name = {selecteddev.Name}");
+ BleAddressSelector.SetBleAddress(selecteddev.Id.ToHexBleAddress());
+ }
+ }
+
+ private void WriteAdvertisementRecords(IDevice device)
+ {
+ if (device.AdvertisementRecords is null)
+ {
+ Write("{0} {1} has no AdvertisementRecords...", device.Name, device.State);
+ return;
+ }
+ Write("{0} {1} with {2} AdvertisementRecords", device.Name, device.State, device.AdvertisementRecords.Count);
+ foreach (var ar in device.AdvertisementRecords)
+ {
+ switch (ar.Type)
+ {
+ case AdvertisementRecordType.CompleteLocalName:
+ Write(ar.ToString() + " = " + Encoding.UTF8.GetString(ar.Data));
+ break;
+ default:
+ Write(ar.ToString());
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Connect to a device with a specific name
+ /// Assumes that DoTheScanning has been called and that the device is advertising
+ ///
+ ///
+ ///
+ public async Task ConnectTest(string name)
+ {
+ if (!scanningDone)
+ {
+ Write("ConnectTest({0}) Failed - Call the DoTheScanning() method first!");
+ return null;
+ }
+ Thread.Sleep(10);
+ foreach (var device in discoveredDevices)
+ {
+ if (device.Name.Contains(name))
+ {
+ await Adapter.ConnectToDeviceAsync(device);
+ return device;
+ }
+ }
+ return null;
+ }
+
+ public Task RunGetSystemConnectedOrPairedDevices()
+ {
+ IReadOnlyList devs = Adapter.GetSystemConnectedOrPairedDevices();
+ Task.Delay(200);
+ Write($"GetSystemConnectedOrPairedDevices found {devs.Count} devices:");
+ foreach (var dev in devs)
+ {
+ Write("{0}: {1}", dev.Id.ToHexBleAddress(), dev.Name);
+ }
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// This demonstrates a bug where the known services is not cleared at disconnect (2023-11-03)
+ ///
+ public async Task ShowNumberOfServices()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ Write("Connecting to device with address = {0}", bleaddress);
+ IDevice dev = await Adapter.ConnectToKnownDeviceAsync(bleaddress.ToBleDeviceGuid()) ?? throw new Exception("null");
+ string name = dev.Name;
+ Write("Connected to {0} {1} {2}", name, dev.Id.ToHexBleAddress(), dev.State);
+ Write("Calling dev.GetServicesAsync()...");
+ var services = await dev.GetServicesAsync();
+ Write("Found {0} services", services.Count);
+ Thread.Sleep(1000);
+ Write("Disconnecting from {0} {1}", name, dev.Id.ToHexBleAddress());
+ await Adapter.DisconnectDeviceAsync(dev);
+ Thread.Sleep(1000);
+ Write("ReConnecting to device {0} {1}...", name, dev.Id.ToHexBleAddress());
+ await Adapter.ConnectToDeviceAsync(dev);
+ Write("Connect Done.");
+ Thread.Sleep(1000);
+ Write("Calling dev.GetServicesAsync()...");
+ services = await dev.GetServicesAsync();
+ Write("Found {0} services", services.Count);
+ await Adapter.DisconnectDeviceAsync(dev);
+ Thread.Sleep(1000);
+ }
+
+ internal Task Disconnect(IDevice dev)
+ {
+ return Adapter.DisconnectDeviceAsync(dev);
+ }
+
+
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.WinConsole/Program.cs b/Source/BLE.Client/BLE.Client.WinConsole/Program.cs
new file mode 100644
index 00000000..f97f9131
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole/Program.cs
@@ -0,0 +1,84 @@
+using BLE.Client.WinConsole;
+using Plugin.BLE;
+using Plugin.BLE.Abstractions.Contracts;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Windows.Media.Capture;
+
+Console.WriteLine("Hello, BLE World!");
+Console.WriteLine($"Environment.OSVersion.Version.Build: {Environment.OSVersion.Version.Build}");
+using (var ct = new ConsoleTracer())
+{
+
+ Plugin.BLE.Abstractions.Trace.TraceImplementation = ct.GetPrefixedTrace("Plugin.BLE");
+ var ppemos = new PluginDemos(ct.GetPrefixedTrace(" DEMO"));
+ var wdemos = new WindowsDemos(ct.GetPrefixedTrace(" DEMO"));
+ var demoDict = new Dictionary
+ {
+
+ {ConsoleKey.B, new Demo("Turn Bluetooth ON", ppemos.TurnBluetoothOn) },
+ {ConsoleKey.N, new Demo("Turn Bluetooth OFF", ppemos.TurnBluetoothOff) },
+ {ConsoleKey.D1, new Demo("Discover and set the BleAddress", ppemos.DiscoverAndSelect) },
+ {ConsoleKey.D2, new Demo("Set the BleAddress", BleAddressSelector.NewBleAddress) },
+ {ConsoleKey.D3, new Demo("Connect -> Disconnect", ppemos.Connect_Disconnect) },
+ {ConsoleKey.D4, new Demo("Pair -> Connect -> Disconnect", ppemos.Pair_Connect_Disconnect) },
+ {ConsoleKey.D5, new Demo("Connect -> Change Parameters -> Disconnect", ppemos.Connect_Change_Parameters_Disconnect) },
+ {ConsoleKey.D6, new Demo("Run GetSystemConnectedOrPairedDevices", ppemos.RunGetSystemConnectedOrPairedDevices) },
+ {ConsoleKey.D7, new Demo("Loop: Connect -> Read services -> Disconnect", ppemos.Connect_Read_Services_Disconnect_Loop) },
+ {ConsoleKey.D8, new Demo("Loop: Connect -> Read services -> Dispose", ppemos.Connect_Read_Services_Dispose_Loop) },
+ {ConsoleKey.D9, new Demo("Connect -> Loop: ConnectionLost -> Connect", ppemos.Connect_ConnectionLost_Reconnect) },
+ {ConsoleKey.Q, new Demo("Adapter.BondAsync", ppemos.BondAsync) },
+ {ConsoleKey.W, new Demo("Adapter.BondedDevices", ppemos.GetBondedDevices) },
+ {ConsoleKey.E, new Demo("Device.BondState", ppemos.ShowBondState) },
+ {ConsoleKey.A, new Demo("Pure Windows: Connect -> Disconnect", wdemos.Connect_Disconnect) },
+ {ConsoleKey.S, new Demo("Pure Windows: Unpair all BLE devices", wdemos.UnPairAllBleDevices) },
+ };
+
+ while (true)
+ {
+
+ Console.WriteLine();
+ if (BleAddressSelector.DoesBleAddressExists())
+ {
+ Console.WriteLine($"Using BLE Address: {BleAddressSelector.GetBleAddress()}");
+ }
+ else
+ {
+ Console.WriteLine("No Ble address has been set - use key '1' or '2' to set the BLE address)");
+ }
+ Console.WriteLine("List of tests to run for key:");
+ Console.WriteLine();
+ Console.WriteLine(ConsoleKey.Escape + ": Quit!");
+
+ foreach (var demo in demoDict)
+ {
+ Console.WriteLine(demo.Key + ": " + demo.Value.Description);
+ }
+
+ var key = Console.ReadKey();
+ if (key.Key == ConsoleKey.Escape)
+ {
+ break;
+ }
+ if (demoDict.TryGetValue(key.Key, out Demo? chosendemo))
+ {
+ Console.WriteLine();
+ Console.WriteLine($"Running: {chosendemo.Description}");
+ Console.WriteLine("-------------------------------------------------------");
+ if (chosendemo is null)
+ {
+ throw new Exception("No such demo!");
+ }
+ await chosendemo.Method();
+ }
+ else
+ {
+ Console.WriteLine($"{key} -> No such test. Remember {ConsoleKey.Escape} -> Quit!");
+ }
+ await Task.Delay(200);
+ Console.WriteLine("-------------------------------------------------------");
+ }
+}
+
+
diff --git a/Source/BLE.Client/BLE.Client.WinConsole/WindowsDemos.cs b/Source/BLE.Client/BLE.Client.WinConsole/WindowsDemos.cs
new file mode 100644
index 00000000..a3ee3365
--- /dev/null
+++ b/Source/BLE.Client/BLE.Client.WinConsole/WindowsDemos.cs
@@ -0,0 +1,127 @@
+using Plugin.BLE.Abstractions.Contracts;
+using Plugin.BLE.Windows;
+using Plugin.BLE;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Diagnostics;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+using Windows.Devices.Bluetooth;
+using WBluetooth = Windows.Devices.Bluetooth;
+using Plugin.BLE.Extensions;
+using System.Threading;
+using Windows.Devices.Enumeration;
+
+namespace BLE.Client.WinConsole
+{
+ ///
+ /// The purpose of this demonstration class is to show/test Windows BluetoothLEDevice without using the Plugin
+ ///
+ public class WindowsDemos
+ {
+ private readonly Action? writer;
+ ManualResetEvent disconnectedSignal = new ManualResetEvent(false);
+ ManualResetEvent connectedSignal = new ManualResetEvent(false);
+
+ public WindowsDemos(Action? writer = null)
+ {
+ this.writer = writer;
+ }
+
+ private void Write(string format, params object[] args)
+ {
+ writer?.Invoke(format, args);
+ }
+
+ public async Task Connect_Disconnect()
+ {
+ string bleaddress = BleAddressSelector.GetBleAddress();
+ ulong bleaddressUl = bleaddress.ToBleDeviceGuid().ToBleAddress();
+ WBluetooth.BluetoothLEDevice dev = await WBluetooth.BluetoothLEDevice.FromBluetoothAddressAsync(bleaddressUl);
+ dev.RequestPreferredConnectionParameters(BluetoothLEPreferredConnectionParameters.ThroughputOptimized);
+ dev.ConnectionStatusChanged += Dev_ConnectionStatusChanged;
+ var devId = BluetoothDeviceId.FromId(dev.DeviceId);
+ Write("Connecting...");
+ var stopwatch = Stopwatch.StartNew();
+ GattSession gattSession = await GattSession.FromDeviceIdAsync(devId);
+ gattSession = await GattSession.FromDeviceIdAsync(devId);
+ gattSession.MaintainConnection = true;
+ gattSession.SessionStatusChanged += GattSession_SessionStatusChanged;
+ gattSession.MaxPduSizeChanged += GattSession_MaxPduSizeChanged;
+ if (!connectedSignal.WaitOne(10000))
+ {
+ Write("Not Connected!!!");
+ return;
+ }
+ Write("Connected in {0} ms", stopwatch.ElapsedMilliseconds);
+ var conpar = dev.GetConnectionParameters();
+ Write($"Connected with Latency = {conpar.ConnectionLatency}, "
+ + $"Interval = {conpar.ConnectionInterval}, Timeout = {conpar.LinkTimeout}, MaxPdu = {gattSession.MaxPduSize}");
+
+ Thread.Sleep(100);
+ Write("Now Sleeing 4 secs...");
+ Thread.Sleep(4000);
+ disconnectedSignal.Reset();
+ Write("Disconnecting...");
+ stopwatch = Stopwatch.StartNew();
+
+ gattSession.MaintainConnection = false;
+ gattSession.Dispose();
+ dev.Dispose();
+ if (!disconnectedSignal.WaitOne(10000))
+ {
+ Write("Not Disconnected!!!");
+ return;
+ }
+ Write("Disconnected in {0} ms", stopwatch.ElapsedMilliseconds);
+ }
+
+ public async Task UnPairAllBleDevices()
+ {
+ string aqsFilter = BluetoothLEDevice.GetDeviceSelector();
+ var collection = await DeviceInformation.FindAllAsync(aqsFilter);
+ foreach (DeviceInformation di in collection)
+ {
+ try
+ {
+ DeviceUnpairingResult res = await di.Pairing.UnpairAsync();
+ Write($"Unpairing {di.Name}: {res.Status}");
+ }
+ catch (Exception ex)
+ {
+ Write($"Exception when unpairing {di.Name}: {ex.Message}");
+ }
+ }
+ }
+
+ private void GattSession_MaxPduSizeChanged(GattSession sender, object args)
+ {
+ Write("MaxPduSizeChanged: {0}", sender.MaxPduSize);
+ }
+
+ private void GattSession_SessionStatusChanged(GattSession sender, GattSessionStatusChangedEventArgs args)
+ {
+ Write("SessionStatusChanged: {0}", args.Status);
+ }
+
+ private void Dev_ConnectionStatusChanged(BluetoothLEDevice sender, object args)
+ {
+ Write("ConnectionStatusChanged:{0}", sender.ConnectionStatus);
+ switch (sender.ConnectionStatus)
+ {
+ case BluetoothConnectionStatus.Disconnected:
+ disconnectedSignal.Set();
+ break;
+ case BluetoothConnectionStatus.Connected:
+ connectedSignal.Set();
+ break;
+ default:
+ Write("Unknown BluetoothConnectionStatus: {0}", sender.ConnectionStatus);
+ break;
+ }
+ }
+ }
+}
diff --git a/Source/BLE.Client/BLE.Client.iOS/BLE.Client.iOS.csproj b/Source/BLE.Client/BLE.Client.iOS/BLE.Client.iOS.csproj
index f4589537..b8570709 100644
--- a/Source/BLE.Client/BLE.Client.iOS/BLE.Client.iOS.csproj
+++ b/Source/BLE.Client/BLE.Client.iOS/BLE.Client.iOS.csproj
@@ -192,15 +192,15 @@
8.0.2
- 3.1.0
+ 7.0.0
- 0.2.0.64
+ 1.0.0
- 5.0.0.2478
+ 5.0.0.2612
-
+
\ No newline at end of file
diff --git a/Source/BLE.Client/BLE.Client.macOS/BLE.Client.macOS.csproj b/Source/BLE.Client/BLE.Client.macOS/BLE.Client.macOS.csproj
index e5bd9132..d46482af 100644
--- a/Source/BLE.Client/BLE.Client.macOS/BLE.Client.macOS.csproj
+++ b/Source/BLE.Client/BLE.Client.macOS/BLE.Client.macOS.csproj
@@ -105,7 +105,7 @@
8.0.2
- 5.0.0.2478
+ 5.0.0.2612
diff --git a/Source/BLE.Client/BLE.Client/BLE.Client.csproj b/Source/BLE.Client/BLE.Client/BLE.Client.csproj
index 4b035576..a566de6f 100644
--- a/Source/BLE.Client/BLE.Client/BLE.Client.csproj
+++ b/Source/BLE.Client/BLE.Client/BLE.Client.csproj
@@ -11,11 +11,11 @@
-
+
-
-
+
+
diff --git a/Source/BLE.Client/BLE.Client/ViewModels/DescriptorListViewModel.cs b/Source/BLE.Client/BLE.Client/ViewModels/DescriptorListViewModel.cs
index 8cb2d1c4..bdc7cb64 100644
--- a/Source/BLE.Client/BLE.Client/ViewModels/DescriptorListViewModel.cs
+++ b/Source/BLE.Client/BLE.Client/ViewModels/DescriptorListViewModel.cs
@@ -10,7 +10,7 @@ public class DescriptorListViewModel : BaseViewModel
private readonly IMvxNavigationService _navigation;
private ICharacteristic _characteristic;
- public IReadOnlyList Descriptors { get; private set;}
+ public IReadOnlyList Descriptors { get; private set; }
public DescriptorListViewModel(IAdapter adapter, IMvxNavigationService navigation) : base(adapter)
{
@@ -53,7 +53,7 @@ public IDescriptor SelectedDescriptor
{
var bundle = new MvxBundle(new Dictionary(Bundle.Data) { { DescriptorIdKey, value.Id.ToString() } });
- _navigation.Navigate(bundle);
+ _navigation.Navigate(bundle);
}
RaisePropertyChanged();
diff --git a/Source/BLE.Client/BLE.Client/ViewModels/DeviceListViewModel.cs b/Source/BLE.Client/BLE.Client/ViewModels/DeviceListViewModel.cs
index da8e89dc..844ae516 100644
--- a/Source/BLE.Client/BLE.Client/ViewModels/DeviceListViewModel.cs
+++ b/Source/BLE.Client/BLE.Client/ViewModels/DeviceListViewModel.cs
@@ -38,8 +38,8 @@ public Guid PreviousGuid
}
}
- public MvxCommand RefreshCommand => new MvxCommand(() => TryStartScanning(refresh:true,filter:false));
- public MvxCommand RefreshFilteredScanCommand => new MvxCommand(() => TryStartScanning(refresh:true,filter:true));
+ public MvxCommand RefreshCommand => new MvxCommand(() => TryStartScanning(refresh: true, filter: false));
+ public MvxCommand RefreshFilteredScanCommand => new MvxCommand(() => TryStartScanning(refresh: true, filter: true));
public MvxCommand EmptyDevicesCommand => new MvxCommand(() =>
{
@@ -349,7 +349,7 @@ private async Task ScanForDevicesFiltered()
scanFilterOptions.ServiceUuids = list.ToArray();
}
- await Adapter.StartScanningForDevicesAsync(scanFilterOptions,_cancellationTokenSource.Token);
+ await Adapter.StartScanningForDevicesAsync(scanFilterOptions, _cancellationTokenSource.Token);
}
private async Task UpdateConnectedDevices()
diff --git a/Source/BLE.Mac.slnf b/Source/BLE.Mac.slnf
index dce79361..9c264733 100644
--- a/Source/BLE.Mac.slnf
+++ b/Source/BLE.Mac.slnf
@@ -2,10 +2,11 @@
"solution": {
"path": "BLE.sln",
"projects": [
- "Plugin.BLE\\Plugin.BLE.csproj",
- "Plugin.BLE.Tests\\Plugin.BLE.Tests.csproj",
+ "BLE.Client\\BLE.Client.Maui\\BLE.Client.Maui.csproj",
+ "BLE.Client\\BLE.Client\\BLE.Client.csproj",
"MvvmCross.Plugins.BLE\\MvvmCross.Plugins.BLE.csproj",
- "BLE.Client\\BLE.Client\\BLE.Client.csproj"
+ "Plugin.BLE.Tests\\Plugin.BLE.Tests.csproj",
+ "Plugin.BLE\\Plugin.BLE.csproj"
]
}
-}
+}
\ No newline at end of file
diff --git a/Source/BLE.sln b/Source/BLE.sln
index 5318b9cb..ad7021e7 100644
--- a/Source/BLE.sln
+++ b/Source/BLE.sln
@@ -25,10 +25,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.BLE", "Plugin.BLE\Pl
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLE.Client", "BLE.Client\BLE.Client\BLE.Client.csproj", "{DFE97BE0-070B-43AD-BF37-50FD42B542D1}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLE.Client.Droid", "BLE.Client\BLE.Client.Droid\BLE.Client.Droid.csproj", "{7B0E4EFD-EEB8-4CE9-9C8D-7A4BF734E9A7}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLE.Client.iOS", "BLE.Client\BLE.Client.iOS\BLE.Client.iOS.csproj", "{D0FBE2BA-799B-4071-A45D-8FB037006C9C}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{BF30732D-F8EA-4DDE-B892-81D51D02A5D4}"
ProjectSection(SolutionItems) = preProject
..\.build\build.bat = ..\.build\build.bat
@@ -40,10 +36,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".build", ".build", "{BF3073
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.BLE.Tests", "Plugin.BLE.Tests\Plugin.BLE.Tests.csproj", "{F8374603-78C8-4CB8-A622-EC9226424474}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLE.Client.Droid", "BLE.Client\BLE.Client.Droid\BLE.Client.Droid.csproj", "{7B0E4EFD-EEB8-4CE9-9C8D-7A4BF734E9A7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLE.Client.iOS", "BLE.Client\BLE.Client.iOS\BLE.Client.iOS.csproj", "{D0FBE2BA-799B-4071-A45D-8FB037006C9C}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLE.Client.macOS", "BLE.Client\BLE.Client.macOS\BLE.Client.macOS.csproj", "{B202A1F9-B9B8-4F5F-A011-C8EA5E485CED}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BLE.Client.UWP", "BLE.Client\BLE.Client.UWP\BLE.Client.UWP.csproj", "{25E04E05-F867-4F64-813D-AAFE0BA171B0}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLE.Client.Maui", "BLE.Client\BLE.Client.Maui\BLE.Client.Maui.csproj", "{D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BLE.Client.WinConsole", "BLE.Client\BLE.Client.WinConsole\BLE.Client.WinConsole.csproj", "{39287F60-65DE-4077-90F2-57CF725E92B6}"
+EndProject
+Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "BLE.Client.WinConsole.Installer", "BLE.Client\BLE.Client.WinConsole.Installer\BLE.Client.WinConsole.Installer.vdproj", "{14D4A306-E53A-40A5-9427-5CA068B1CB5F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@@ -244,6 +250,62 @@ Global
{DFE97BE0-070B-43AD-BF37-50FD42B542D1}.Release|x64.Build.0 = Release|Any CPU
{DFE97BE0-070B-43AD-BF37-50FD42B542D1}.Release|x86.ActiveCfg = Release|Any CPU
{DFE97BE0-070B-43AD-BF37-50FD42B542D1}.Release|x86.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x64.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x86.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|Any CPU.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM64.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM64.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhone.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhone.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x64.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x64.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x86.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x86.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x64.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x86.Build.0 = Debug|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM64.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhone.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x64.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x64.Build.0 = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x86.ActiveCfg = Release|Any CPU
+ {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x86.Build.0 = Release|Any CPU
{7B0E4EFD-EEB8-4CE9-9C8D-7A4BF734E9A7}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{7B0E4EFD-EEB8-4CE9-9C8D-7A4BF734E9A7}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{7B0E4EFD-EEB8-4CE9-9C8D-7A4BF734E9A7}.Ad-Hoc|Any CPU.Deploy.0 = Release|Any CPU
@@ -358,62 +420,6 @@ Global
{D0FBE2BA-799B-4071-A45D-8FB037006C9C}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
{D0FBE2BA-799B-4071-A45D-8FB037006C9C}.Release|x64.ActiveCfg = Release|iPhone
{D0FBE2BA-799B-4071-A45D-8FB037006C9C}.Release|x86.ActiveCfg = Release|iPhone
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x64.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Ad-Hoc|x86.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|Any CPU.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM64.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|ARM64.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhone.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhone.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x64.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x64.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x86.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.AppStore|x86.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM64.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|ARM64.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhone.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhone.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x64.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x64.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x86.ActiveCfg = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Debug|x86.Build.0 = Debug|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|Any CPU.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM64.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|ARM64.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhone.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhone.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x64.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x64.Build.0 = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x86.ActiveCfg = Release|Any CPU
- {F8374603-78C8-4CB8-A622-EC9226424474}.Release|x86.Build.0 = Release|Any CPU
{B202A1F9-B9B8-4F5F-A011-C8EA5E485CED}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{B202A1F9-B9B8-4F5F-A011-C8EA5E485CED}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{B202A1F9-B9B8-4F5F-A011-C8EA5E485CED}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU
@@ -540,6 +546,156 @@ Global
{25E04E05-F867-4F64-813D-AAFE0BA171B0}.Release|x86.ActiveCfg = Release|x86
{25E04E05-F867-4F64-813D-AAFE0BA171B0}.Release|x86.Build.0 = Release|x86
{25E04E05-F867-4F64-813D-AAFE0BA171B0}.Release|x86.Deploy.0 = Release|x86
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|ARM.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|x64.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Ad-Hoc|x86.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|Any CPU.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|ARM.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|ARM.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|ARM64.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|ARM64.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|iPhone.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|iPhone.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|x64.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|x64.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|x86.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.AppStore|x86.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|ARM.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|ARM.Deploy.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|ARM64.Deploy.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|x64.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|x64.Deploy.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|x86.Build.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Debug|x86.Deploy.0 = Debug|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|ARM.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|ARM.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|ARM.Deploy.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|ARM64.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|ARM64.Deploy.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|iPhone.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|x64.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|x64.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|x64.Deploy.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|x86.ActiveCfg = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|x86.Build.0 = Release|Any CPU
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E}.Release|x86.Deploy.0 = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|ARM.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|ARM.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|ARM64.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|ARM64.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|x64.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|x64.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|x86.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.AppStore|x86.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|ARM.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|x64.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Debug|x86.Build.0 = Debug|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|ARM.ActiveCfg = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|ARM.Build.0 = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|ARM64.Build.0 = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|iPhone.Build.0 = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|x64.ActiveCfg = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|x64.Build.0 = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|x86.ActiveCfg = Release|Any CPU
+ {39287F60-65DE-4077-90F2-57CF725E92B6}.Release|x86.Build.0 = Release|Any CPU
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Ad-Hoc|Any CPU.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Ad-Hoc|ARM.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Ad-Hoc|ARM64.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Ad-Hoc|iPhone.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Ad-Hoc|x64.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Ad-Hoc|x86.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.AppStore|Any CPU.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.AppStore|ARM.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.AppStore|ARM64.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.AppStore|iPhone.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.AppStore|iPhoneSimulator.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.AppStore|x64.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.AppStore|x86.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Debug|Any CPU.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Debug|ARM.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Debug|ARM64.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Debug|iPhone.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Debug|iPhoneSimulator.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Debug|x64.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Debug|x86.ActiveCfg = Debug
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Release|Any CPU.ActiveCfg = Release
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Release|ARM.ActiveCfg = Release
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Release|ARM64.ActiveCfg = Release
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Release|iPhone.ActiveCfg = Release
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Release|iPhoneSimulator.ActiveCfg = Release
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Release|x64.ActiveCfg = Release
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F}.Release|x86.ActiveCfg = Release
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -550,6 +706,9 @@ Global
{D0FBE2BA-799B-4071-A45D-8FB037006C9C} = {A06042F5-F69D-4191-875E-5ADE9CF42075}
{B202A1F9-B9B8-4F5F-A011-C8EA5E485CED} = {A06042F5-F69D-4191-875E-5ADE9CF42075}
{25E04E05-F867-4F64-813D-AAFE0BA171B0} = {A06042F5-F69D-4191-875E-5ADE9CF42075}
+ {D04F2D04-A33F-4E09-9EE2-29D7B2A6CD4E} = {A06042F5-F69D-4191-875E-5ADE9CF42075}
+ {39287F60-65DE-4077-90F2-57CF725E92B6} = {A06042F5-F69D-4191-875E-5ADE9CF42075}
+ {14D4A306-E53A-40A5-9427-5CA068B1CB5F} = {A06042F5-F69D-4191-875E-5ADE9CF42075}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B5D5105C-EA52-467B-8CCC-6777900C3B95}
diff --git a/Source/MvvmCross.Plugins.BLE/MvvmCross.Plugins.BLE.csproj b/Source/MvvmCross.Plugins.BLE/MvvmCross.Plugins.BLE.csproj
index f60b297b..4a15be5b 100644
--- a/Source/MvvmCross.Plugins.BLE/MvvmCross.Plugins.BLE.csproj
+++ b/Source/MvvmCross.Plugins.BLE/MvvmCross.Plugins.BLE.csproj
@@ -1,10 +1,11 @@
- net6.0-android;net6.0-ios;net6.0-maccatalyst;net7.0-android;net7.0-ios;net7.0-maccatalyst
- $(TargetFrameworks);MonoAndroid10.0;Xamarin.iOS10;Xamarin.Mac20;uap10.0.19041;net6.0-windows10.0.19041;net7.0-windows10.0.19041
+ net7.0-android33.0;net8.0-android34.0
+ $(TargetFrameworks);net7.0-ios;net7.0-maccatalyst;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);MonoAndroid13.0;Xamarin.iOS10;Xamarin.Mac20;uap10.0.19041;net7.0-windows10.0.19041;net8.0-windows10.0.19041MVVMCross.Plugins.BLEMVVMCross.Plugins.BLE
- 3.0.0-beta.4
+ 3.1.0$(AssemblyName) ($(TargetFramework))Adrian Seceleanu, Sven-Michael Stübe, Janus WeilMvvmCross.Plugin.BLE
@@ -14,13 +15,13 @@
Apache-2.0Adrian Seceleanu, Sven-Michael Stübe, Janus Weilhttps://github.com/dotnet-bluetooth-le/dotnet-bluetooth-le/
- MVVMCross Plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and UWP.
+ MVVMCross Plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and Windows.
- xamarin, maui, pcl, xam.pcl, uwp, bluetooth, ble, .net maui, xamarin.forms, ios
+ xamarin, maui, pcl, xam.pcl, bluetooth, ble, .net maui, xamarin.forms, android, ios
MVVMCross Bluetooth Low Energy (BLE) Plugin for .NET MAUI and Xamarin
- MVVMCross Plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and UWP.
+ MVVMCross Plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and Windows.
Read the full documentation on the projects page.
Apache-2.0
@@ -52,6 +53,11 @@
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+ win10-x64;win10-x86;win10-arm64
+ true
+
+
all
@@ -74,7 +80,7 @@
-
+
@@ -82,7 +88,7 @@
-
+
@@ -93,7 +99,7 @@
-
+
@@ -105,7 +111,7 @@
-
+
@@ -113,7 +119,7 @@
-
+
@@ -128,4 +134,7 @@
C:\Program Files\Microsoft Visual Studio\2022\Preview\Common7\IDE\Extensions\Xamarin.VisualStudio\Xamarin.Mac.dll
+
+
+
diff --git a/Source/Plugin.BLE.Tests/CharacteristicBaseTests.cs b/Source/Plugin.BLE.Tests/CharacteristicBaseTests.cs
index 010f6467..3fc01805 100644
--- a/Source/Plugin.BLE.Tests/CharacteristicBaseTests.cs
+++ b/Source/Plugin.BLE.Tests/CharacteristicBaseTests.cs
@@ -6,7 +6,7 @@
using Xunit;
namespace Plugin.BLE.Tests
-{
+{
public class CharacteristicBaseTests
{
[Theory(DisplayName = "Setting WriteType to not supported type throws InvalidOperationException")]
diff --git a/Source/Plugin.BLE.Tests/Plugin.BLE.Tests.csproj b/Source/Plugin.BLE.Tests/Plugin.BLE.Tests.csproj
index 111927c8..e5e7bdc4 100644
--- a/Source/Plugin.BLE.Tests/Plugin.BLE.Tests.csproj
+++ b/Source/Plugin.BLE.Tests/Plugin.BLE.Tests.csproj
@@ -1,16 +1,16 @@
- net7.0
+ net8.0false
-
-
-
+
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
@@ -21,4 +21,4 @@
-
\ No newline at end of file
+
diff --git a/Source/Plugin.BLE/Android/Adapter.cs b/Source/Plugin.BLE/Android/Adapter.cs
index 30b115cb..a8c952dd 100644
--- a/Source/Plugin.BLE/Android/Adapter.cs
+++ b/Source/Plugin.BLE/Android/Adapter.cs
@@ -3,12 +3,16 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Android.App;
using Android.Bluetooth;
using Android.Bluetooth.LE;
+using Android.Content;
using Android.OS;
using Java.Util;
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
+using Plugin.BLE.Android.Extensions;
+using Plugin.BLE.BroadcastReceivers;
using Plugin.BLE.Extensions;
using Object = Java.Lang.Object;
using Trace = Plugin.BLE.Abstractions.Trace;
@@ -29,22 +33,40 @@ public class Adapter : AdapterBase
///
public object ConnectedDeviceRegistryLock { get; } = new object();
+ private readonly Dictionary> _bondingTcsForAddress = new();
+
public Adapter(BluetoothManager bluetoothManager)
{
_bluetoothManager = bluetoothManager;
_bluetoothAdapter = bluetoothManager.Adapter;
+ //bonding
+ var bondStatusBroadcastReceiver = new BondStatusBroadcastReceiver(this);
+ Application.Context.RegisterReceiver(bondStatusBroadcastReceiver,
+ new IntentFilter(BluetoothDevice.ActionBondStateChanged));
+
+ //forward events from broadcast receiver
+ bondStatusBroadcastReceiver.BondStateChanged += (s, args) =>
+ {
+ HandleDeviceBondStateChanged(args);
- // TODO: bonding
- //var bondStatusBroadcastReceiver = new BondStatusBroadcastReceiver();
- //Application.Context.RegisterReceiver(bondStatusBroadcastReceiver,
- // new IntentFilter(BluetoothDevice.ActionBondStateChanged));
+ if (!_bondingTcsForAddress.TryGetValue(args.Address, out var tcs))
+ {
+ return;
+ }
- ////forward events from broadcast receiver
- //bondStatusBroadcastReceiver.BondStateChanged += (s, args) =>
- //{
- // //DeviceBondStateChanged(this, args);
- //};
+ if (args.State == DeviceBondState.Bonding)
+ {
+ return;
+ }
+
+ if (args.State == DeviceBondState.Bonded)
+ {
+ tcs.TrySetResult(true);
+ }
+
+ tcs.TrySetException(new Exception("Bonding failed."));
+ };
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
{
@@ -56,6 +78,43 @@ public Adapter(BluetoothManager bluetoothManager)
}
}
+ public override Task BondAsync(IDevice device)
+ {
+ if (device == null)
+ {
+ throw new ArgumentNullException(nameof(device));
+ }
+
+ if (!(device.NativeDevice is BluetoothDevice nativeDevice))
+ {
+ throw new ArgumentException("Invalid device type");
+ }
+
+ if (nativeDevice.BondState == Bond.Bonded)
+ {
+ return Task.CompletedTask;
+ }
+
+ var deviceAddress = nativeDevice.Address!;
+ if (_bondingTcsForAddress.TryGetValue(deviceAddress, out var tcs))
+ {
+ tcs.TrySetException(new Exception("Bonding failed on old try."));
+ _bondingTcsForAddress.Remove(deviceAddress);
+ }
+
+ var taskCompletionSource = new TaskCompletionSource();
+
+ _bondingTcsForAddress.Add(nativeDevice.Address!, taskCompletionSource);
+
+ if (!nativeDevice.CreateBond())
+ {
+ _bondingTcsForAddress.Remove(nativeDevice.Address);
+ throw new Exception("Bonding failed");
+ }
+
+ return taskCompletionSource.Task;
+ }
+
protected override Task StartScanningForDevicesNativeAsync(ScanFilterOptions scanFilterOptions, bool allowDuplicatesKey, CancellationToken scanCancellationToken)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop)
@@ -139,7 +198,7 @@ private void StartScanningNew(ScanFilterOptions scanFilterOptions)
{
Trace.Message($"Device address {deviceAddress} is invalid. The correct format is \"01:02:03:AB:CD:EF\"");
}
-
+
}
}
if (scanFilterOptions.HasDeviceNames)
@@ -151,7 +210,7 @@ private void StartScanningNew(ScanFilterOptions scanFilterOptions)
scanFilters.Add(sfb.Build());
}
}
-
+
}
var ssb = new ScanSettings.Builder()
@@ -178,7 +237,7 @@ private void StartScanningNew(ScanFilterOptions scanFilterOptions)
Trace.Message("Using ScanMatchMode Agressive");
}
}
-
+
#if NET6_0_OR_GREATER
if (OperatingSystem.IsAndroidVersionAtLeast(26))
#else
@@ -252,11 +311,14 @@ protected override void DisconnectDeviceNative(IDevice device)
((Device)device).Disconnect();
}
- public override async Task ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters = default(ConnectParameters), CancellationToken cancellationToken = default(CancellationToken))
+ public override async Task ConnectToKnownDeviceNativeAsync(Guid deviceGuid, ConnectParameters connectParameters = default(ConnectParameters), CancellationToken cancellationToken = default(CancellationToken))
{
var macBytes = deviceGuid.ToByteArray().Skip(10).Take(6).ToArray();
var nativeDevice = _bluetoothAdapter.GetRemoteDevice(macBytes);
-
+ if (nativeDevice == null)
+ throw new Abstractions.Exceptions.DeviceConnectionException(deviceGuid,"", $"[Adapter] Device {deviceGuid} not found.");
+ if (!nativeDevice.SupportsBLE())
+ throw new Abstractions.Exceptions.DeviceConnectionException(deviceGuid,"", $"[Adapter] Device {deviceGuid} does not support BLE.");
var device = new Device(this, nativeDevice, null);
await ConnectToDeviceAsync(device, connectParameters, cancellationToken);
@@ -271,9 +333,9 @@ public override IReadOnlyList GetSystemConnectedOrPairedDevices(Guid[]
}
//add dualMode type too as they are BLE too ;)
- var connectedDevices = _bluetoothManager.GetConnectedDevices(ProfileType.Gatt).Where(d => d.Type == BluetoothDeviceType.Le || d.Type == BluetoothDeviceType.Dual);
+ var connectedDevices = _bluetoothManager.GetConnectedDevices(ProfileType.Gatt).Where(d => d.SupportsBLE());
- var bondedDevices = _bluetoothAdapter.BondedDevices.Where(d => d.Type == BluetoothDeviceType.Le || d.Type == BluetoothDeviceType.Dual);
+ var bondedDevices = _bluetoothAdapter.BondedDevices.Where(d => d.SupportsBLE());
return connectedDevices.Union(bondedDevices, new DeviceComparer()).Select(d => new Device(this, d, null)).Cast().ToList();
}
@@ -341,7 +403,14 @@ async Task WaitAsync()
}
}
- public override bool supportsExtendedAdvertising()
+ protected override IReadOnlyList GetBondedDevices()
+ {
+ var bondedDevices = _bluetoothAdapter.BondedDevices.Where(d => d.SupportsBLE());
+
+ return bondedDevices.Select(d => new Device(this, d, null, 0)).Cast().ToList();
+ }
+
+ public override bool SupportsExtendedAdvertising()
{
#if NET6_0_OR_GREATER
if (OperatingSystem.IsAndroidVersionAtLeast(26))
@@ -357,7 +426,7 @@ public override bool supportsExtendedAdvertising()
}
}
- public override bool supportsCodedPHY()
+ public override bool SupportsCodedPHY()
{
#if NET6_0_OR_GREATER
if (OperatingSystem.IsAndroidVersionAtLeast(26))
diff --git a/Source/Plugin.BLE/Android/BleImplementation.cs b/Source/Plugin.BLE/Android/BleImplementation.cs
index 508b0592..f5f58f26 100644
--- a/Source/Plugin.BLE/Android/BleImplementation.cs
+++ b/Source/Plugin.BLE/Android/BleImplementation.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using Android.App;
using Android.Bluetooth;
using Android.Content;
@@ -86,5 +87,15 @@ protected override BluetoothState GetInitialStateNative()
protected override IAdapter CreateNativeAdapter()
=> new Adapter(_bluetoothManager);
+
+ public override Task TrySetStateAsync(bool on)
+ {
+ const string ACTION_REQUEST_DISABLE = "android.bluetooth.adapter.action.REQUEST_DISABLE";
+
+ var intent = new Intent(on ? BluetoothAdapter.ActionRequestEnable : ACTION_REQUEST_DISABLE);
+ intent.SetFlags(ActivityFlags.NewTask);
+ Application.Context.StartActivity(intent);
+ return Task.FromResult(true);
+ }
}
}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Android/BroadcastReceivers/BluetoothStatusBroadcastReceiver.cs b/Source/Plugin.BLE/Android/BroadcastReceivers/BluetoothStatusBroadcastReceiver.cs
index 641f7d86..443ed38d 100644
--- a/Source/Plugin.BLE/Android/BroadcastReceivers/BluetoothStatusBroadcastReceiver.cs
+++ b/Source/Plugin.BLE/Android/BroadcastReceivers/BluetoothStatusBroadcastReceiver.cs
@@ -30,7 +30,7 @@ public override void OnReceive(Context context, Intent intent)
return;
}
- var btState = (State) state;
+ var btState = (State)state;
_stateChangedHandler?.Invoke(btState.ToBluetoothState());
}
}
diff --git a/Source/Plugin.BLE/Android/BroadcastReceivers/BondStatusBroadcastReceiver.cs b/Source/Plugin.BLE/Android/BroadcastReceivers/BondStatusBroadcastReceiver.cs
index 96bba489..ece753fa 100644
--- a/Source/Plugin.BLE/Android/BroadcastReceivers/BondStatusBroadcastReceiver.cs
+++ b/Source/Plugin.BLE/Android/BroadcastReceivers/BondStatusBroadcastReceiver.cs
@@ -1,41 +1,54 @@
using System;
using Android.Bluetooth;
using Android.Content;
-using Plugin.BLE.Abstractions;
+using Android.OS;
using Plugin.BLE.Abstractions.EventArgs;
using Plugin.BLE.Android;
+using Plugin.BLE.Extensions;
namespace Plugin.BLE.BroadcastReceivers
{
- //[BroadcastReceiver]
public class BondStatusBroadcastReceiver : BroadcastReceiver
{
+ private readonly Adapter _broadcastAdapter;
+
public event EventHandler BondStateChanged;
+ public BondStatusBroadcastReceiver(Adapter adapter)
+ {
+ _broadcastAdapter = adapter;
+ }
+
public override void OnReceive(Context context, Intent intent)
{
- var bondState = (Bond)intent.GetIntExtra(BluetoothDevice.ExtraBondState, (int)Bond.None);
- //ToDo
- var device = new Device(null, (BluetoothDevice)intent.GetParcelableExtra(BluetoothDevice.ExtraDevice), null);
- Console.WriteLine(bondState.ToString());
+ if (BondStateChanged == null)
+ {
+ return;
+ }
- if (BondStateChanged == null) return;
+ var extraBondState = (Bond)intent.GetIntExtra(BluetoothDevice.ExtraBondState, (int)Bond.None);
- switch (bondState)
+ BluetoothDevice bluetoothDevice;
+
+#if NET6_0_OR_GREATER
+ if (OperatingSystem.IsAndroidVersionAtLeast(33))
+#else
+ if (Build.VERSION.SdkInt >= BuildVersionCodes.Tiramisu)
+#endif
{
- case Bond.None:
- BondStateChanged(this, new DeviceBondStateChangedEventArgs() { Device = device, State = DeviceBondState.NotBonded });
- break;
-
- case Bond.Bonding:
- BondStateChanged(this, new DeviceBondStateChangedEventArgs() { Device = device, State = DeviceBondState.Bonding });
- break;
+ bluetoothDevice = (BluetoothDevice)intent.GetParcelableExtra(BluetoothDevice.ExtraDevice, Java.Lang.Class.FromType(typeof(BluetoothDevice)));
+ }
+ else
+ {
+ bluetoothDevice = (BluetoothDevice)intent.GetParcelableExtra(BluetoothDevice.ExtraDevice);
+ }
+
+ var device = new Device(_broadcastAdapter, bluetoothDevice, null);
- case Bond.Bonded:
- BondStateChanged(this, new DeviceBondStateChangedEventArgs() { Device = device, State = DeviceBondState.Bonded });
- break;
+ var address = bluetoothDevice?.Address ?? string.Empty;
- }
+ var bondState = extraBondState.FromNative();
+ BondStateChanged(this, new DeviceBondStateChangedEventArgs() { Address = address, Device = device, State = bondState });
}
}
-}
\ No newline at end of file
+}
diff --git a/Source/Plugin.BLE/Android/Device.cs b/Source/Plugin.BLE/Android/Device.cs
index 20406324..a2e84cf1 100644
--- a/Source/Plugin.BLE/Android/Device.cs
+++ b/Source/Plugin.BLE/Android/Device.cs
@@ -14,6 +14,8 @@
using Trace = Plugin.BLE.Abstractions.Trace;
using System.Threading;
using Java.Util;
+using Plugin.BLE.Extensions;
+using Plugin.BLE.Abstractions.Extensions;
namespace Plugin.BLE.Android
{
@@ -37,13 +39,15 @@ public class Device : DeviceBase
/// the registration must be disposed to avoid disconnecting after a connection
///
private CancellationTokenRegistration _connectCancellationTokenRegistration;
+
+ private TaskCompletionSource _bondCompleteTaskCompletionSource;
///
- /// the connect paramaters used when connecting to this device
+ /// the connect parameters used when connecting to this device
///
public ConnectParameters ConnectParameters { get; private set; }
- public Device(Adapter adapter, BluetoothDevice nativeDevice, BluetoothGatt gatt, int rssi = 0, byte[] advertisementData = null, bool isConnectable = true)
+ public Device(Adapter adapter, BluetoothDevice nativeDevice, BluetoothGatt gatt, int rssi = 0, byte[] advertisementData = null, bool isConnectable = true)
: base(adapter, nativeDevice)
{
Update(nativeDevice, gatt);
@@ -106,6 +110,11 @@ protected override async Task GetServiceNativeAsync(Guid id)
private async Task> DiscoverServicesInternal()
{
+ if (_gatt == null)
+ {
+ Trace.Message("[Warning]: Can't discover services {0}. Gatt is null.", Name);
+ }
+
return await TaskBuilder
.FromEvent, EventHandler, EventHandler>(
execute: () =>
@@ -117,7 +126,14 @@ private async Task> DiscoverServicesInternal()
},
getCompleteHandler: (complete, reject) => ((sender, args) =>
{
- complete(_gatt.Services.Select(service => new Service(service, _gatt, _gattCallback, this)).ToList());
+ if (_gatt.Services == null)
+ {
+ complete(new List());
+ }
+ else
+ {
+ complete(_gatt.Services.Select(service => new Service(service, _gatt, _gattCallback, this)).ToList());
+ }
}),
subscribeComplete: handler => _gattCallback.ServicesDiscovered += handler,
unsubscribeComplete: handler => _gattCallback.ServicesDiscovered -= handler,
@@ -152,6 +168,13 @@ public void Connect(ConnectParameters connectParameters, CancellationToken cance
}
}
+ public Task BondAsync()
+ {
+ _bondCompleteTaskCompletionSource = new TaskCompletionSource();
+ NativeDevice.CreateBond();
+ return _bondCompleteTaskCompletionSource.Task;
+ }
+
private void ConnectToGattForceBleTransportAPI(bool autoconnect, CancellationToken cancellationToken)
{
//This parameter is present from API 18 but only public from API 23
@@ -199,8 +222,8 @@ private void ConnectToGattForceBleTransportAPI(bool autoconnect, CancellationTok
private void DisconnectAndClose(BluetoothGatt gatt)
{
- gatt.Disconnect();
- gatt.Close();
+ gatt?.Disconnect();
+ gatt?.Close();
}
///
@@ -287,64 +310,72 @@ private Guid ParseDeviceId()
return new Guid(deviceGuid);
}
- public static List ParseScanRecord(byte[] scanRecord)
+ public List ParseScanRecord(byte[] scanRecord)
{
var records = new List();
if (scanRecord == null)
return records;
- int index = 0;
- while (index < scanRecord.Length)
+ try
{
- byte length = scanRecord[index++];
- //Done once we run out of records
- // 1 byte for type and length-1 bytes for data
- if (length == 0) break;
-
- int type = scanRecord[index];
- //Done if our record isn't a valid type
- if (type == 0) break;
-
- if (!Enum.IsDefined(typeof(AdvertisementRecordType), type))
+ int index = 0;
+ while (index < scanRecord.Length)
{
- Trace.Message("Advertisment record type not defined: {0}", type);
- break;
- }
+ byte length = scanRecord[index++];
+ //Done once we run out of records
+ // 1 byte for type and length-1 bytes for data
+ if (length == 0) break;
- //data length is length -1 because type takes the first byte
- byte[] data = new byte[length - 1];
- Array.Copy(scanRecord, index + 1, data, 0, length - 1);
+ int type = scanRecord[index];
+ //Done if our record isn't a valid type
+ if (type == 0) break;
- // don't forget that data is little endian so reverse
- // Supplement to Bluetooth Core Specification 1
- // NOTE: all relevant devices are already little endian, so this is not necessary for any type except UUIDs
- //var record = new AdvertisementRecord((AdvertisementRecordType)type, data.Reverse().ToArray());
-
- switch ((AdvertisementRecordType)type)
- {
- case AdvertisementRecordType.ServiceDataUuid32Bit:
- case AdvertisementRecordType.SsUuids128Bit:
- case AdvertisementRecordType.SsUuids16Bit:
- case AdvertisementRecordType.SsUuids32Bit:
- case AdvertisementRecordType.UuidCom32Bit:
- case AdvertisementRecordType.UuidsComplete128Bit:
- case AdvertisementRecordType.UuidsComplete16Bit:
- case AdvertisementRecordType.UuidsIncomple16Bit:
- case AdvertisementRecordType.UuidsIncomplete128Bit:
- Array.Reverse(data);
+ if (!Enum.IsDefined(typeof(AdvertisementRecordType), type))
+ {
+ Trace.Message("Advertisment record type not defined: {0}", type);
break;
- }
- var record = new AdvertisementRecord((AdvertisementRecordType)type, data);
+ }
- //Trace.Message(record.ToString());
+ //Trace.Message(record.ToString());
+ //data length is length -1 because type takes the first byte
+ byte[] data = new byte[length - 1];
+ Array.Copy(scanRecord, index + 1, data, 0, length - 1);
- records.Add(record);
+ // don't forget that data is little endian so reverse
+ // Supplement to Bluetooth Core Specification 1
+ // NOTE: all relevant devices are already little endian, so this is not necessary for any type except UUIDs
+ //var record = new AdvertisementRecord((AdvertisementRecordType)type, data.Reverse().ToArray());
- //Advance
- index += length;
+ switch ((AdvertisementRecordType)type)
+ {
+ case AdvertisementRecordType.ServiceDataUuid32Bit:
+ case AdvertisementRecordType.SsUuids128Bit:
+ case AdvertisementRecordType.SsUuids16Bit:
+ case AdvertisementRecordType.SsUuids32Bit:
+ case AdvertisementRecordType.UuidCom32Bit:
+ case AdvertisementRecordType.UuidsComplete128Bit:
+ case AdvertisementRecordType.UuidsComplete16Bit:
+ case AdvertisementRecordType.UuidsIncomple16Bit:
+ case AdvertisementRecordType.UuidsIncomplete128Bit:
+ Array.Reverse(data);
+ break;
+ }
+ var record = new AdvertisementRecord((AdvertisementRecordType)type, data);
+
+ Trace.Message(record.ToString());
+
+ records.Add(record);
+
+ //Advance
+ index += length;
+ }
+ }
+ catch(Exception)
+ {
+ //There may be a situation where scanRecord contains incorrect data.
+ Trace.Message("Failed to parse advertisementData. Device address: {0}, Data: {1}", NativeDevice.Address, scanRecord.ToHexString());
}
-
return records;
}
@@ -447,10 +478,11 @@ protected override bool UpdateConnectionIntervalNative(ConnectionInterval interv
}
}
- public override bool IsConnectable { get; protected set; }
+ public override bool IsConnectable { get; protected set; }
- public override bool SupportsIsConnectable {
+ public override bool SupportsIsConnectable
+ {
get =>
#if NET6_0_OR_GREATER
OperatingSystem.IsAndroidVersionAtLeast(26);
@@ -458,6 +490,20 @@ public override bool SupportsIsConnectable {
(Build.VERSION.SdkInt >= BuildVersionCodes.O);
#endif
}
+
+ protected override DeviceBondState GetBondState()
+ {
+ if (NativeDevice == null)
+ {
+ Trace.Message($"[Warning]: Can't get bond state of {Name}. NativeDevice is null.");
+ return DeviceBondState.NotSupported;
+ }
+ return NativeDevice.BondState.FromNative();
+ }
+ public override bool UpdateConnectionParameters(ConnectParameters connectParameters = default)
+ {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/Source/Plugin.BLE/Android/Extensions/BluetoothDeviceExtension.cs b/Source/Plugin.BLE/Android/Extensions/BluetoothDeviceExtension.cs
new file mode 100644
index 00000000..2e869a8c
--- /dev/null
+++ b/Source/Plugin.BLE/Android/Extensions/BluetoothDeviceExtension.cs
@@ -0,0 +1,12 @@
+using Android.Bluetooth;
+
+namespace Plugin.BLE.Android.Extensions
+{
+ public static class BluetoothDeviceExtension
+ {
+ public static bool SupportsBLE(this BluetoothDevice d)
+ {
+ return d.Type == BluetoothDeviceType.Le || d.Type == BluetoothDeviceType.Dual;
+ }
+ }
+}
diff --git a/Source/Plugin.BLE/Android/Extensions/DeviceBondStateExtension.cs b/Source/Plugin.BLE/Android/Extensions/DeviceBondStateExtension.cs
new file mode 100644
index 00000000..b9d73c0c
--- /dev/null
+++ b/Source/Plugin.BLE/Android/Extensions/DeviceBondStateExtension.cs
@@ -0,0 +1,24 @@
+using Android.Bluetooth;
+using Plugin.BLE.Abstractions;
+
+namespace Plugin.BLE.Extensions
+{
+ internal static class DeviceBondStateExtension
+ {
+ public static DeviceBondState FromNative(this Bond bondState)
+ {
+ switch (bondState)
+ {
+ case Bond.None:
+ return DeviceBondState.NotBonded;
+ case Bond.Bonding:
+ return DeviceBondState.Bonding;
+ case Bond.Bonded:
+ return DeviceBondState.Bonded;
+ default:
+ return DeviceBondState.NotSupported;
+ }
+ }
+
+ }
+}
diff --git a/Source/Plugin.BLE/Android/Extensions/ScanMatchModeExtension.cs b/Source/Plugin.BLE/Android/Extensions/ScanMatchModeExtension.cs
index ff71abad..6bad16de 100644
--- a/Source/Plugin.BLE/Android/Extensions/ScanMatchModeExtension.cs
+++ b/Source/Plugin.BLE/Android/Extensions/ScanMatchModeExtension.cs
@@ -8,7 +8,7 @@ internal static class ScanMatchModeExtension
{
public static BluetoothScanMatchMode ToNative(this ScanMatchMode matchMode)
{
- switch(matchMode)
+ switch (matchMode)
{
case ScanMatchMode.AGRESSIVE:
return BluetoothScanMatchMode.Aggressive;
diff --git a/Source/Plugin.BLE/Android/GattCallback.cs b/Source/Plugin.BLE/Android/GattCallback.cs
index 2bd2e68c..b66a7ff6 100644
--- a/Source/Plugin.BLE/Android/GattCallback.cs
+++ b/Source/Plugin.BLE/Android/GattCallback.cs
@@ -96,7 +96,7 @@ public override void OnConnectionStateChange(BluetoothGatt gatt, GattStatus stat
break;
}
else
- {
+ {
//connection must have been lost, because the callback was not triggered by calling disconnect
Trace.Message($"Disconnected '{_device.Name}' by lost connection");
diff --git a/Source/Plugin.BLE/Android/Service.cs b/Source/Plugin.BLE/Android/Service.cs
index 9b2742e3..16c15dab 100644
--- a/Source/Plugin.BLE/Android/Service.cs
+++ b/Source/Plugin.BLE/Android/Service.cs
@@ -17,7 +17,7 @@ public class Service : ServiceBase
public override Guid Id => Guid.ParseExact(NativeService.Uuid.ToString(), "d");
public override bool IsPrimary => NativeService.Type == GattServiceType.Primary;
- public Service(BluetoothGattService nativeService, BluetoothGatt gatt, IGattCallback gattCallback, IDevice device)
+ public Service(BluetoothGattService nativeService, BluetoothGatt gatt, IGattCallback gattCallback, IDevice device)
: base(device, nativeService)
{
_gatt = gatt;
diff --git a/Source/Plugin.BLE/Apple/Adapter.cs b/Source/Plugin.BLE/Apple/Adapter.cs
index aab50012..20fcf1dc 100644
--- a/Source/Plugin.BLE/Apple/Adapter.cs
+++ b/Source/Plugin.BLE/Apple/Adapter.cs
@@ -152,6 +152,11 @@ public Adapter(CBCentralManager centralManager, IBleCentralManagerDelegate bleCe
};
}
+ public override Task BondAsync(IDevice device)
+ {
+ throw new NotSupportedException();
+ }
+
protected override async Task StartScanningForDevicesNativeAsync(ScanFilterOptions scanFilterOptions, bool allowDuplicatesKey, CancellationToken scanCancellationToken)
{
#if NET6_0_OR_GREATER || MACCATALYST
@@ -223,7 +228,7 @@ private static Guid ParseDeviceGuid(CBPeripheral peripherial)
///
/// The to known device async.
/// Device GUID.
- public override async Task ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters = default(ConnectParameters), CancellationToken cancellationToken = default(CancellationToken))
+ public override async Task ConnectToKnownDeviceNativeAsync(Guid deviceGuid, ConnectParameters connectParameters = default(ConnectParameters), CancellationToken cancellationToken = default(CancellationToken))
{
#if NET6_0_OR_GREATER || MACCATALYST
await WaitForState(CBManagerState.PoweredOn, cancellationToken, true);
@@ -233,7 +238,7 @@ private static Guid ParseDeviceGuid(CBPeripheral peripherial)
#endif
if (cancellationToken.IsCancellationRequested)
- throw new TaskCanceledException("ConnectToKnownDeviceAsync cancelled");
+ throw new TaskCanceledException("ConnectToKnownDeviceNativeAsync cancelled");
//FYI attempted to use tobyte array insetead of string but there was a problem with byte ordering Guid->NSUui
var uuid = new NSUuid(deviceGuid.ToString());
@@ -259,7 +264,7 @@ private static Guid ParseDeviceGuid(CBPeripheral peripherial)
);
if (peripherial == null)
- throw new Exception($"[Adapter] Device {deviceGuid} not found.");
+ throw new Abstractions.Exceptions.DeviceConnectionException(deviceGuid, "", $"[Adapter] Device {deviceGuid} not found.");
}
var device = new Device(this, peripherial, _bleCentralManagerDelegate, peripherial.Name, peripherial.RSSI?.Int32Value ?? 0, new List());
@@ -294,6 +299,11 @@ public override IReadOnlyList GetKnownDevicesByIds(Guid[] ids)
return nativeDevices.Select(d => new Device(this, d, _bleCentralManagerDelegate)).Cast().ToList();
}
+ protected override IReadOnlyList GetBondedDevices()
+ {
+ return null; // not supported
+ }
+
#if NET6_0_OR_GREATER || MACCATALYST
private async Task WaitForState(CBManagerState state, CancellationToken cancellationToken, bool configureAwait = false)
#else
@@ -607,7 +617,7 @@ async Task WaitAsync()
}
#if NET6_0_OR_GREATER || __IOS__
- public override bool supportsExtendedAdvertising()
+ public override bool SupportsExtendedAdvertising()
{
#if NET6_0_OR_GREATER
if (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsTvOSVersionAtLeast(13) || OperatingSystem.IsMacCatalystVersionAtLeast(13))
diff --git a/Source/Plugin.BLE/Apple/BleCentralManagerDelegate.cs b/Source/Plugin.BLE/Apple/BleCentralManagerDelegate.cs
index 408e3b3b..9c413706 100644
--- a/Source/Plugin.BLE/Apple/BleCentralManagerDelegate.cs
+++ b/Source/Plugin.BLE/Apple/BleCentralManagerDelegate.cs
@@ -20,7 +20,7 @@ public interface IBleCentralManagerDelegate
public class BleCentralManagerDelegate : CBCentralManagerDelegate, IBleCentralManagerDelegate
{
-#region IBleCentralManagerDelegate events
+ #region IBleCentralManagerDelegate events
private event EventHandler _willRestoreState;
@@ -91,9 +91,9 @@ event EventHandler IBleCentralManagerDelegate.ConnectedPe
remove => _connectedPeripheral -= value;
}
-#endregion
+ #endregion
-#region Event wiring
+ #region Event wiring
public override void WillRestoreState(CBCentralManager central, NSDictionary dict)
{
@@ -139,6 +139,6 @@ public override void ConnectedPeripheral(CBCentralManager central, CBPeripheral
_connectedPeripheral?.Invoke(this, new CBPeripheralEventArgs(peripheral));
}
-#endregion
+ #endregion
}
}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Apple/BleImplementation.cs b/Source/Plugin.BLE/Apple/BleImplementation.cs
index a31cfdfa..800059a6 100644
--- a/Source/Plugin.BLE/Apple/BleImplementation.cs
+++ b/Source/Plugin.BLE/Apple/BleImplementation.cs
@@ -4,6 +4,7 @@
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Extensions;
using Plugin.BLE.iOS;
+using System.Threading.Tasks;
namespace Plugin.BLE
{
@@ -66,5 +67,11 @@ private CBCentralInitOptions CreateInitOptions()
ShowPowerAlert = _showPowerAlert
};
}
+
+ public override Task TrySetStateAsync(bool on)
+ {
+ Trace.Message("WARNING TrySetStateAsync is not implemented for Apple");
+ return Task.FromResult(false);
+ }
}
}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Apple/Characteristic.cs b/Source/Plugin.BLE/Apple/Characteristic.cs
index df16d63b..4dceddf4 100644
--- a/Source/Plugin.BLE/Apple/Characteristic.cs
+++ b/Source/Plugin.BLE/Apple/Characteristic.cs
@@ -34,14 +34,14 @@ public override byte[] Value
{
return new byte[0];
}
-
+
return value.ToArray();
}
- }
+ }
public override CharacteristicPropertyType Properties => (CharacteristicPropertyType)(int)NativeCharacteristic.Properties;
- public Characteristic(CBCharacteristic nativeCharacteristic, CBPeripheral parentDevice, IService service, IBleCentralManagerDelegate bleCentralManagerDelegate)
+ public Characteristic(CBCharacteristic nativeCharacteristic, CBPeripheral parentDevice, IService service, IBleCentralManagerDelegate bleCentralManagerDelegate)
: base(service, nativeCharacteristic)
{
_parentDevice = parentDevice;
@@ -111,7 +111,7 @@ protected override Task> GetDescriptorsNativeAsync()
#endif
{
Trace.Message($"Read characterteristic value: {Value?.ToHexString()}");
- int resultCode = (args.Error == null) ? 0 : NSErrorToGattStatus(args.Error);
+ int resultCode = NSErrorToGattStatus(args.Error);
complete((Value, resultCode));
}
},
@@ -134,7 +134,7 @@ protected override Task WriteNativeAsync(byte[] data, CharacteristicWriteTy
if (writeType.ToNative() == CBCharacteristicWriteType.WithResponse)
{
task = TaskBuilder.FromEvent, EventHandler>(
- execute: () =>
+ execute: () =>
{
if (_parentDevice.State != CBPeripheralState.Connected)
throw exception;
@@ -144,7 +144,7 @@ protected override Task WriteNativeAsync(byte[] data, CharacteristicWriteTy
if (args.Characteristic.UUID != NativeCharacteristic.UUID)
return;
- complete((args.Error == null) ? 0 : NSErrorToGattStatus(args.Error));
+ complete(NSErrorToGattStatus(args.Error));
},
subscribeComplete: handler => _parentDevice.WroteCharacteristicValue += handler,
unsubscribeComplete: handler => _parentDevice.WroteCharacteristicValue -= handler,
@@ -190,7 +190,8 @@ protected override Task WriteNativeAsync(byte[] data, CharacteristicWriteTy
unsubscribeReject: handler => _bleCentralManagerDelegate.DisconnectedPeripheral -= handler);
}
- else {
+ else
+ {
task = Task.FromResult(0);
}
}
@@ -240,7 +241,7 @@ protected override Task StartUpdatesNativeAsync(CancellationToken cancellationTo
reject(new Exception($"Device {Service.Device.Id} disconnected while starting updates for characteristic with {Id}."));
}),
subscribeReject: handler => _bleCentralManagerDelegate.DisconnectedPeripheral += handler,
- unsubscribeReject: handler => _bleCentralManagerDelegate.DisconnectedPeripheral -= handler,
+ unsubscribeReject: handler => _bleCentralManagerDelegate.DisconnectedPeripheral -= handler,
token: cancellationToken);
}
@@ -287,6 +288,9 @@ protected override Task StopUpdatesNativeAsync(CancellationToken cancellationTok
protected int NSErrorToGattStatus(NSError error)
{
+ if (error == null)
+ return 0;
+
switch (error.Domain)
{
case "CBATTErrorDomain":
diff --git a/Source/Plugin.BLE/Apple/Descriptor.cs b/Source/Plugin.BLE/Apple/Descriptor.cs
index bc243eaa..190420cc 100644
--- a/Source/Plugin.BLE/Apple/Descriptor.cs
+++ b/Source/Plugin.BLE/Apple/Descriptor.cs
@@ -35,7 +35,7 @@ public override byte[] Value
private readonly CBPeripheral _parentDevice;
private readonly IBleCentralManagerDelegate _bleCentralManagerDelegate;
- public Descriptor(CBDescriptor nativeDescriptor, CBPeripheral parentDevice, ICharacteristic characteristic, IBleCentralManagerDelegate bleCentralManagerDelegate)
+ public Descriptor(CBDescriptor nativeDescriptor, CBPeripheral parentDevice, ICharacteristic characteristic, IBleCentralManagerDelegate bleCentralManagerDelegate)
: base(characteristic, nativeDescriptor)
{
_parentDevice = parentDevice;
diff --git a/Source/Plugin.BLE/Apple/Device.cs b/Source/Plugin.BLE/Apple/Device.cs
index 246d2915..7ebe850d 100644
--- a/Source/Plugin.BLE/Apple/Device.cs
+++ b/Source/Plugin.BLE/Apple/Device.cs
@@ -173,10 +173,19 @@ protected override bool UpdateConnectionIntervalNative(ConnectionInterval interv
Trace.Message("Cannot update connection inteval on iOS.");
return false;
}
-
+
public override bool IsConnectable { get; protected set; }
public override bool SupportsIsConnectable { get => true; }
+
+ protected override DeviceBondState GetBondState()
+ {
+ return DeviceBondState.NotSupported;
+ }
+ public override bool UpdateConnectionParameters(ConnectParameters connectParameters = default)
+ {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/Source/Plugin.BLE/Apple/Service.cs b/Source/Plugin.BLE/Apple/Service.cs
index 266c8bdf..54b7e4f7 100644
--- a/Source/Plugin.BLE/Apple/Service.cs
+++ b/Source/Plugin.BLE/Apple/Service.cs
@@ -18,7 +18,7 @@ public class Service : ServiceBase
public override Guid Id => NativeService.UUID.GuidFromUuid();
public override bool IsPrimary => NativeService.Primary;
- public Service(CBService nativeService, IDevice device, IBleCentralManagerDelegate bleCentralManagerDelegate)
+ public Service(CBService nativeService, IDevice device, IBleCentralManagerDelegate bleCentralManagerDelegate)
: base(device, nativeService)
{
_device = device.NativeDevice as CBPeripheral;
diff --git a/Source/Plugin.BLE/Plugin.BLE.csproj b/Source/Plugin.BLE/Plugin.BLE.csproj
index 6400c6fd..d0d110fb 100644
--- a/Source/Plugin.BLE/Plugin.BLE.csproj
+++ b/Source/Plugin.BLE/Plugin.BLE.csproj
@@ -1,11 +1,11 @@
- netstandard2.0;net6.0-android;net6.0-ios;net6.0-maccatalyst;net7.0-android;net7.0-ios;net7.0-maccatalyst
-
- $(TargetFrameworks);MonoAndroid10.0;Xamarin.iOS10;Xamarin.Mac20;net6.0-windows10.0.19041;net7.0-windows10.0.19041
+ netstandard2.0;net7.0-android33.0;net8.0-android34.0
+ $(TargetFrameworks);net7.0-ios;net7.0-maccatalyst;net8.0-ios;net8.0-maccatalyst
+ $(TargetFrameworks);MonoAndroid13.0;Xamarin.iOS10;Xamarin.Mac20;uap10.0.19041;net7.0-windows10.0.19041;net8.0-windows10.0.19041;net8.0-windows10.0.22000.0Plugin.BLEPlugin.BLE
- 3.0.0-beta.4
+ 3.1.0$(AssemblyName) ($(TargetFramework))Adrian Seceleanu, Sven-Michael Stübe, Janus WeilPlugin.BLE
@@ -15,14 +15,13 @@
Apache-2.0Adrian Seceleanu, Sven-Michael Stübe, Janus Weilhttps://github.com/dotnet-bluetooth-le/dotnet-bluetooth-le/
- Plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and UWP.
+ Plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and Windows.
- xamarin, maui, pcl, xam.pcl, uwp, bluetooth, ble, .net maui, xamarin.forms, ios
+ xamarin, maui, pcl, xam.pcl, bluetooth, ble, .net maui, xamarin.forms, android, ios
Bluetooth Low Energy (BLE) Plugin for .NET MAUI and Xamarin
- Xamarin and MAUI plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and UWP.
- Read the full documentation on the projects page.
+ Xamarin and MAUI plugin to access Bluetooth Low Energy functionality on Android, iOS, macOS, and Windows.
Apache-2.0https://github.com/dotnet-bluetooth-le/dotnet-bluetooth-le/
@@ -45,14 +44,21 @@
-
- true
- true
- $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
-
-
-
-
+
+
+ true
+
+ true
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+ true
@@ -82,7 +88,6 @@
-
diff --git a/Source/Plugin.BLE/Shared/AdapterBase.cs b/Source/Plugin.BLE/Shared/AdapterBase.cs
index 0c088060..59296676 100644
--- a/Source/Plugin.BLE/Shared/AdapterBase.cs
+++ b/Source/Plugin.BLE/Shared/AdapterBase.cs
@@ -49,6 +49,10 @@ public abstract class AdapterBase : IAdapter
///
public event EventHandler DeviceConnectionError;
///
+ /// Occurs when the bonding state of a device changed.
+ ///
+ public event EventHandler DeviceBondStateChanged;
+ ///
/// Occurs when the scan has been stopped due the timeout after ms.
///
public event EventHandler ScanTimeoutElapsed;
@@ -73,8 +77,7 @@ public bool IsScanning
/// Default:
///
public ScanMode ScanMode { get; set; } = ScanMode.LowPower;
-
-
+
///
/// Scan match mode defines how agressively we look for adverts
///
@@ -100,13 +103,18 @@ public bool IsScanning
///
public IReadOnlyList ConnectedDevices => ConnectedDeviceRegistry.Values.ToList();
+ ///
+ /// List of all bonded devices (or null if the device does not support this information).
+ ///
+ public IReadOnlyList BondedDevices => GetBondedDevices();
+
///
/// Starts scanning for BLE devices that fulfill the .
/// DeviceDiscovered will only be called, if returns true for the discovered device.
///
- public async Task StartScanningForDevicesAsync(ScanFilterOptions scanFilterOptions,
- Func deviceFilter = null,
- bool allowDuplicatesKey = false,
+ public async Task StartScanningForDevicesAsync(ScanFilterOptions scanFilterOptions,
+ Func deviceFilter = null,
+ bool allowDuplicatesKey = false,
CancellationToken cancellationToken = default)
{
if (IsScanning)
@@ -235,7 +243,7 @@ public async Task ConnectToDeviceAsync(IDevice device, ConnectParameters connect
subscribeReject: handler => DeviceConnectionError += handler,
unsubscribeReject: handler => DeviceConnectionError -= handler,
- token: cts.Token);
+ token: cts.Token, mainthread: false);
}
catch (Exception)
{
@@ -363,6 +371,36 @@ public void HandleConnectionFail(IDevice device, string errorMessage)
ErrorMessage = errorMessage
});
}
+
+ ///
+ public abstract Task BondAsync(IDevice device);
+
+ ///
+ /// Handle bond state changed information.
+ ///
+ ///
+ protected void HandleDeviceBondStateChanged(DeviceBondStateChangedEventArgs args)
+ {
+ DeviceBondStateChanged?.Invoke(this, args);
+ }
+
+ ///
+ /// Connects to a device with a known GUID without scanning and if in range. Does not scan for devices.
+ ///
+ public async Task ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters = default, CancellationToken cancellationToken = default)
+ {
+ if (DiscoveredDevicesRegistry.TryGetValue(deviceGuid, out IDevice discoveredDevice))
+ {
+ await ConnectToDeviceAsync(discoveredDevice, connectParameters, cancellationToken);
+ return discoveredDevice;
+ }
+
+ var connectedDevice = await ConnectToKnownDeviceNativeAsync(deviceGuid, connectParameters, cancellationToken);
+ if (!DiscoveredDevicesRegistry.ContainsKey(deviceGuid))
+ DiscoveredDevicesRegistry.TryAdd(deviceGuid, connectedDevice);
+
+ return connectedDevice;
+ }
///
/// Native implementation of StartScanningForDevicesAsync.
@@ -383,9 +421,9 @@ public void HandleConnectionFail(IDevice device, string errorMessage)
protected abstract Task ConnectNativeAsync(Guid uuid, Func deviceFilter, CancellationToken cancellationToken = default(CancellationToken));
///
- /// Connects to a device with a known GUID without scanning and if in range. Does not scan for devices.
+ /// Native implementation of ConnectToKnownDeviceAsync.
///
- public abstract Task ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters = default, CancellationToken cancellationToken = default);
+ public abstract Task ConnectToKnownDeviceNativeAsync(Guid deviceGuid, ConnectParameters connectParameters = default, CancellationToken cancellationToken = default);
///
/// Returns all BLE devices connected to the system.
///
@@ -394,15 +432,19 @@ public void HandleConnectionFail(IDevice device, string errorMessage)
/// Returns a list of paired BLE devices for the given UUIDs.
///
public abstract IReadOnlyList GetKnownDevicesByIds(Guid[] ids);
+ ///
+ /// Returns all BLE device bonded to the system.
+ ///
+ protected abstract IReadOnlyList GetBondedDevices();
///
/// Indicates whether extended advertising (BLE5) is supported.
///
- public virtual bool supportsExtendedAdvertising() => false;
+ public virtual bool SupportsExtendedAdvertising() => false;
///
/// Indicates whether the Coded PHY feature (BLE5) is supported.
///
- public virtual bool supportsCodedPHY() => false;
+ public virtual bool SupportsCodedPHY() => false;
}
}
diff --git a/Source/Plugin.BLE/Shared/AdvertisementRecord.cs b/Source/Plugin.BLE/Shared/AdvertisementRecord.cs
index 6372fa07..83a1824d 100644
--- a/Source/Plugin.BLE/Shared/AdvertisementRecord.cs
+++ b/Source/Plugin.BLE/Shared/AdvertisementRecord.cs
@@ -166,20 +166,19 @@ public enum AdvertisementRecordType
SecureConnectionsRandomValue = 0x23,
///
- /// «3D Information Data» 3D Synchronization Profile, v1.0 or later
+ /// «Broadcast Name» Public Broadcast Profile v1.0 or later
///
- Information3DData = 0x3D,
+ BroadcastName = 0x30,
///
- /// «Manufacturer Specific Data» Bluetooth Core Specification:
+ /// «3D Information Data» 3D Synchronization Profile, v1.0 or later
///
- ManufacturerSpecificData = 0xFF,
+ Information3DData = 0x3D,
///
- /// The is connectable flag. This is only reliable for the ios imlementation. The android stack does not expose this in the client.
+ /// «Manufacturer Specific Data» Bluetooth Core Specification:
///
- // obsolete
- // IsConnectable = 0xAA
+ ManufacturerSpecificData = 0xFF
}
///
@@ -213,4 +212,4 @@ public override string ToString()
return string.Format("Adv rec [Type {0}; Data {1}]", Type, Data.ToHexString());
}
}
-}
\ No newline at end of file
+}
diff --git a/Source/Plugin.BLE/Shared/BleImplementationBase.cs b/Source/Plugin.BLE/Shared/BleImplementationBase.cs
index 69385d87..4adde41f 100644
--- a/Source/Plugin.BLE/Shared/BleImplementationBase.cs
+++ b/Source/Plugin.BLE/Shared/BleImplementationBase.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;
using Plugin.BLE.Abstractions.Utils;
@@ -17,7 +18,6 @@ public abstract class BleImplementationBase : IBluetoothLE
/// Occurs when the state of the Bluetooth adapter changes.
///
public event EventHandler StateChanged;
-
///
/// Indicates whether the device supports BLE.
///
@@ -77,13 +77,22 @@ private IAdapter CreateAdapter()
/// Native implementation of Initialize.
///
protected abstract void InitializeNative();
+
///
/// Get initial state of native adapter.
///
protected abstract BluetoothState GetInitialStateNative();
+
///
/// Create the native adapter.
///
protected abstract IAdapter CreateNativeAdapter();
+
+ ///
+ /// Try set the state of the Bluetooth on/off
+ ///
+ ///
+ /// true if the the method executed with success otherwice false
+ public abstract Task TrySetStateAsync(bool on);
}
}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Shared/CharacteristicWriteType.cs b/Source/Plugin.BLE/Shared/CharacteristicWriteType.cs
index ca3ed866..1f3b73d5 100644
--- a/Source/Plugin.BLE/Shared/CharacteristicWriteType.cs
+++ b/Source/Plugin.BLE/Shared/CharacteristicWriteType.cs
@@ -9,7 +9,7 @@ public enum CharacteristicWriteType
/// Value should be written with response if supported, else without response.
///
Default,
-
+
///
/// Value should be written with response.
///
diff --git a/Source/Plugin.BLE/Shared/ConnectParameter.cs b/Source/Plugin.BLE/Shared/ConnectParameter.cs
index 1574f5c6..d84f1374 100644
--- a/Source/Plugin.BLE/Shared/ConnectParameter.cs
+++ b/Source/Plugin.BLE/Shared/ConnectParameter.cs
@@ -16,6 +16,12 @@ public struct ConnectParameters
///
public bool ForceBleTransport { get; }
+ ///
+ /// Windows only, mapped to:
+ /// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothlepreferredconnectionparameters
+ ///
+ public ConnectionParameterSet ConnectionParameterSet { get; }
+
///
/// Default-constructed connection parameters (all parameters set to false).
///
@@ -26,10 +32,15 @@ public struct ConnectParameters
///
/// Android only: Whether to directly connect to the remote device (false) or to automatically connect as soon as the remote device becomes available (true). The default is false.
/// Android only: For Dual Mode device, force transport mode to LE. The default is false.
- public ConnectParameters(bool autoConnect = false, bool forceBleTransport = false)
+ /// Windows only: Default is None, where this has no effect - use eg. ThroughputOptimized for firmware upload to a device
+ public ConnectParameters(
+ bool autoConnect = false,
+ bool forceBleTransport = false,
+ ConnectionParameterSet connectionParameterSet = ConnectionParameterSet.None)
{
AutoConnect = autoConnect;
ForceBleTransport = forceBleTransport;
+ ConnectionParameterSet = connectionParameterSet;
}
}
}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Shared/ConnectionInterval.cs b/Source/Plugin.BLE/Shared/ConnectionInterval.cs
index 6e1da3ed..2c487f90 100644
--- a/Source/Plugin.BLE/Shared/ConnectionInterval.cs
+++ b/Source/Plugin.BLE/Shared/ConnectionInterval.cs
@@ -24,6 +24,6 @@ public enum ConnectionInterval
/// This is mapped to CONNECTION_PRIORITY_LOW_POWER,
/// see https://developer.android.com/reference/android/bluetooth/BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER
///
- Low = 2
+ Low = 2
}
}
diff --git a/Source/Plugin.BLE/Shared/ConnectionParameterSet.cs b/Source/Plugin.BLE/Shared/ConnectionParameterSet.cs
new file mode 100644
index 00000000..d8dd6190
--- /dev/null
+++ b/Source/Plugin.BLE/Shared/ConnectionParameterSet.cs
@@ -0,0 +1,26 @@
+namespace Plugin.BLE.Abstractions
+{
+ ///
+ /// Only supported in Windows. Mapped to this
+ /// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothlepreferredconnectionparameters
+ ///
+ public enum ConnectionParameterSet
+ {
+ ///
+ /// Not setting any prefered connection type
+ ///
+ None,
+ ///
+ /// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothlepreferredconnectionparameters.balanced
+ ///
+ Balanced,
+ ///
+ /// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothlepreferredconnectionparameters.poweroptimized
+ ///
+ PowerOptimized,
+ ///
+ /// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothlepreferredconnectionparameters.throughputoptimized
+ ///
+ ThroughputOptimized
+ }
+}
diff --git a/Source/Plugin.BLE/Shared/Contracts/IAdapter.cs b/Source/Plugin.BLE/Shared/Contracts/IAdapter.cs
index 20dbc444..d3ad7977 100644
--- a/Source/Plugin.BLE/Shared/Contracts/IAdapter.cs
+++ b/Source/Plugin.BLE/Shared/Contracts/IAdapter.cs
@@ -38,6 +38,13 @@ public interface IAdapter
///
event EventHandler DeviceConnectionError;
///
+ /// Occurs when the bonding state of a device changed
+ /// Android: Supported
+ /// iOS: Not supported
+ /// Windows: Not supported
+ ///
+ event EventHandler DeviceBondStateChanged;
+ ///
/// Occurs when the scan has been stopped due the timeout after ms.
///
event EventHandler ScanTimeoutElapsed;
@@ -76,6 +83,29 @@ public interface IAdapter
///
IReadOnlyList ConnectedDevices { get; }
+ ///
+ /// Initiates a bonding request.
+ /// To establish an additional security level in the communication between server and client pairing or bonding is used.
+ /// Pairing does the key exchange and encryption/decryption for one connection between server and client.
+ /// Bonding does pairing and remembers the keys in a secure storage so that they can be used for the next connection.
+ /// You have to subscribe to Adapter.DeviceBondStateChanged to get the current state. Typically first bonding and later bonded.
+ /// Important:
+ /// On iOS:
+ /// Initiating a bonding request is not supported by iOS. The function simply returns false.
+ /// On Android: Added in API level 19.
+ /// Android system services will handle the necessary user interactions to confirm and complete the bonding process.
+ /// For apps targeting Build.VERSION_CODES#R or lower, this requires the Manifest.permission#BLUETOOTH_ADMIN permission
+ /// which can be gained with a simple 'uses-permission' manifest tag. For apps targeting Build.VERSION_CODES#S or or higher,
+ /// this requires the Manifest.permission#BLUETOOTH_CONNECT permission which can be gained with Activity.requestPermissions(String[], int).
+ ///
+ public Task BondAsync(IDevice device);
+
+ ///
+ /// List of currently bonded devices.
+ /// The property is null if the OS doesn't provide this information
+ ///
+ IReadOnlyList BondedDevices { get; }
+
///
/// Starts scanning for BLE devices that fulfill the .
/// DeviceDiscovered will only be called, if returns true for the discovered device.
@@ -138,7 +168,7 @@ public interface IAdapter
///
/// Connection parameters. Contains platform specific parameters needed to achieved connection. The default value is None.
/// The token to monitor for cancellation requests. The default value is None.
- ///
+ /// The connected device.
Task ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters = default, CancellationToken cancellationToken = default);
///
@@ -157,7 +187,6 @@ public interface IAdapter
/// IMPORTANT: Only considered by iOS due to platform limitations. Filters devices by advertised services. SET THIS VALUE FOR ANY RESULTS
/// List of IDevices connected to the OS. In case of no devices the list is empty.
IReadOnlyList GetSystemConnectedOrPairedDevices(Guid[] services = null);
-
///
/// Connects to a device, using Apple's recommended flow.
@@ -175,12 +204,12 @@ public interface IAdapter
/// Indicates whether extended advertising (BLE5) is supported.
///
/// true if extended advertising is supported, otherwise false.
- bool supportsExtendedAdvertising();
+ bool SupportsExtendedAdvertising();
///
/// Indicates whether the Coded PHY feature (BLE5) is supported.
///
/// true if extended advertising is supported, otherwise false.
- bool supportsCodedPHY();
+ bool SupportsCodedPHY();
}
-}
\ No newline at end of file
+}
diff --git a/Source/Plugin.BLE/Shared/Contracts/IBluetoothLE.cs b/Source/Plugin.BLE/Shared/Contracts/IBluetoothLE.cs
index 08da6cdc..a534206c 100644
--- a/Source/Plugin.BLE/Shared/Contracts/IBluetoothLE.cs
+++ b/Source/Plugin.BLE/Shared/Contracts/IBluetoothLE.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using Plugin.BLE.Abstractions.EventArgs;
namespace Plugin.BLE.Abstractions.Contracts
@@ -29,6 +30,14 @@ public interface IBluetoothLE
///
bool IsOn { get; }
+ ///
+ /// Try to set the state of the Bluetooth (on/off).
+ /// Supported on Android and Windows.
+ ///
+ ///
+ /// true if the the method executed with success otherwice false
+ Task TrySetStateAsync(bool on);
+
///
/// Adapter to that provides access to the physical bluetooth adapter.
///
diff --git a/Source/Plugin.BLE/Shared/Contracts/ICharacteristic.cs b/Source/Plugin.BLE/Shared/Contracts/ICharacteristic.cs
index ec9a7980..5381484e 100644
--- a/Source/Plugin.BLE/Shared/Contracts/ICharacteristic.cs
+++ b/Source/Plugin.BLE/Shared/Contracts/ICharacteristic.cs
@@ -69,7 +69,7 @@ public interface ICharacteristic
/// Indicates wheter the characteristic supports notify or not.
///
bool CanUpdate { get; }
-
+
///
/// Returns the parent service. Use this to access the device.
///
diff --git a/Source/Plugin.BLE/Shared/Contracts/IDevice.cs b/Source/Plugin.BLE/Shared/Contracts/IDevice.cs
index 89ddc700..caf0f576 100644
--- a/Source/Plugin.BLE/Shared/Contracts/IDevice.cs
+++ b/Source/Plugin.BLE/Shared/Contracts/IDevice.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -21,7 +21,7 @@ public interface IDevice : IDisposable
string Name { get; }
///
- /// Last known rssi value in decibals.
+ /// Last known RSSI value in decibels.
/// Can be updated via .
///
int Rssi { get; }
@@ -67,15 +67,15 @@ public interface IDevice : IDisposable
Task GetServiceAsync(Guid id, CancellationToken cancellationToken = default);
///
- /// Updates the rssi value.
+ /// Updates the RSSI value.
///
///
- /// Important:
- /// On Android: This function will only work if the device is connected. The Rssi value will be determined once on the discovery of the device.
+ /// This method is only supported on Android, iOS and MacOS, but not on Windows.
+ /// On Android: This function will only work if the device is connected. The RSSI value will be determined once on the discovery of the device.
///
///
- /// A task that represents the asynchronous read operation. The Result property will contain a boolean that inticates if the update was successful.
- /// The Task will finish after Rssi has been updated.
+ /// A task that represents the asynchronous read operation. The Result property will contain a boolean that indicates if the update was successful.
+ /// The Task will finish after the RSSI has been updated.
///
Task UpdateRssiAsync();
@@ -87,6 +87,7 @@ public interface IDevice : IDisposable
/// Important:
/// On Android: This function will only work with API level 21 and higher. Other API level will get an default value as function result.
/// On iOS: Requesting MTU sizes is not supported by iOS. The function will return the current negotiated MTU between master / slave.
+ /// On Windows: Requesting MTU sizes is not directly supported. Windows will always try and negotiate the maximum MTU between master / slave. The function will return the current negotiated MTU between master / slave.
///
///
/// A task that represents the asynchronous operation. The result contains the negotiated MTU size between master and slave
@@ -107,7 +108,6 @@ public interface IDevice : IDisposable
/// The requested interval (High/Low/Normal)
bool UpdateConnectionInterval(ConnectionInterval interval);
-
///
/// Gets the information if the device has hinted during advertising that the device is connectable.
/// This information is not pat of an advertising record. It's determined from the PDU header.
@@ -120,5 +120,22 @@ public interface IDevice : IDisposable
/// True, if device supports IsConnectable else False
///
bool SupportsIsConnectable { get; }
+
+ ///
+ /// Gets the bonding state of a device.
+ ///
+ DeviceBondState BondState { get; }
+
+ ///
+ /// Updates the connection parameters if already connected
+ ///
+ ///
+ /// Only implemented for Windows
+ ///
+ /// Connection parameters. Contains platform specific parameters needed to achieve connection. The default value is None.
+ ///
+ /// The Result property will contain a boolean that indicates if the update was successful.
+ ///
+ bool UpdateConnectionParameters(ConnectParameters connectParameters = default);
}
-}
\ No newline at end of file
+}
diff --git a/Source/Plugin.BLE/Shared/DeviceBase.cs b/Source/Plugin.BLE/Shared/DeviceBase.cs
index 9ce107fb..9af2c089 100644
--- a/Source/Plugin.BLE/Shared/DeviceBase.cs
+++ b/Source/Plugin.BLE/Shared/DeviceBase.cs
@@ -218,7 +218,7 @@ public void ClearServices()
{
this.CancelEverythingAndReInitialize();
- lock (_servicesLock)
+ lock (KnownServices)
{
if (KnownServices != null)
{
@@ -268,8 +268,32 @@ public override int GetHashCode()
return Id.GetHashCode();
}
+ ///
+ /// Reflects if the device is connectable.
+ /// Only supported if is true.
+ ///
public abstract bool IsConnectable { get; protected set; }
+ ///
+ /// Shows whether the device supports the .
+ ///
public abstract bool SupportsIsConnectable { get; }
+
+ ///
+ /// Gets the of the device.
+ ///
+ protected abstract DeviceBondState GetBondState();
+
+ ///
+ /// Updates the connection parameters if already connected
+ ///
+ ///
+ ///
+ public abstract bool UpdateConnectionParameters(ConnectParameters connectParameters = default);
+
+ ///
+ /// Gets the of the device.
+ ///
+ public DeviceBondState BondState => GetBondState();
}
}
diff --git a/Source/Plugin.BLE/Shared/DeviceBondState.cs b/Source/Plugin.BLE/Shared/DeviceBondState.cs
index e84b5213..40ec2db1 100644
--- a/Source/Plugin.BLE/Shared/DeviceBondState.cs
+++ b/Source/Plugin.BLE/Shared/DeviceBondState.cs
@@ -20,6 +20,10 @@ public enum DeviceBondState
/// Indicates the remote device is bonded (paired),
/// see https://developer.android.com/reference/android/bluetooth/BluetoothDevice#BOND_BONDED
///
- Bonded
+ Bonded,
+ ///
+ /// Indicates that the device does not support information about the bonding state.
+ ///
+ NotSupported = -1,
}
}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Shared/DeviceState.cs b/Source/Plugin.BLE/Shared/DeviceState.cs
index 7951e552..b5890f03 100644
--- a/Source/Plugin.BLE/Shared/DeviceState.cs
+++ b/Source/Plugin.BLE/Shared/DeviceState.cs
@@ -21,7 +21,9 @@ public enum DeviceState
Connected,
///
- /// OnAndroid: Device is connected to the system. In order to use this device please call connect it by using the Adapter.
+ /// Android: Device is connected to the system. In order to use this device please call connect it by using the Adapter.
+ /// Windows: Device is connected to the system, but the connect sequence has not been established in this Plugin.
+ /// iOS: Not used
///
Limited
}
diff --git a/Source/Plugin.BLE/Shared/EventArgs/DeviceBondStateChangedEventArgs.cs b/Source/Plugin.BLE/Shared/EventArgs/DeviceBondStateChangedEventArgs.cs
index ff582c8c..863ad67f 100644
--- a/Source/Plugin.BLE/Shared/EventArgs/DeviceBondStateChangedEventArgs.cs
+++ b/Source/Plugin.BLE/Shared/EventArgs/DeviceBondStateChangedEventArgs.cs
@@ -7,13 +7,19 @@ namespace Plugin.BLE.Abstractions.EventArgs
///
public class DeviceBondStateChangedEventArgs : System.EventArgs
{
+ ///
+ /// The device address.
+ ///
+ public string Address { get; set; }
+
///
/// The device.
///
public IDevice Device { get; set; }
+
///
/// The bond state.
///
public DeviceBondState State { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/Source/Plugin.BLE/Shared/Extensions/AdapterExtension.cs b/Source/Plugin.BLE/Shared/Extensions/AdapterExtension.cs
index 8e8db5b8..1f4fd175 100644
--- a/Source/Plugin.BLE/Shared/Extensions/AdapterExtension.cs
+++ b/Source/Plugin.BLE/Shared/Extensions/AdapterExtension.cs
@@ -34,7 +34,7 @@ public static Task StartScanningForDevicesAsync(this IAdapter adapter, Cancellat
/// A task that represents the asynchronous read operation. The Task will finish after the scan has ended.
public static Task StartScanningForDevicesAsync(this IAdapter adapter, Guid[] serviceUuids, CancellationToken cancellationToken = default)
{
- return adapter.StartScanningForDevicesAsync(new ScanFilterOptions(){ServiceUuids = serviceUuids}, null, cancellationToken: cancellationToken);
+ return adapter.StartScanningForDevicesAsync(new ScanFilterOptions() { ServiceUuids = serviceUuids }, null, cancellationToken: cancellationToken);
}
///
@@ -115,7 +115,7 @@ public static async Task DiscoverDeviceAsync(this IAdapter adapter, Fun
/// Thrown if the device connection fails.
public static Task ConnectToDeviceAsync(this IAdapter adapter, IDevice device, ConnectParameters connectParameters, CancellationToken cancellationToken)
{
- return adapter.ConnectToDeviceAsync(device, connectParameters:connectParameters, cancellationToken: cancellationToken);
+ return adapter.ConnectToDeviceAsync(device, connectParameters: connectParameters, cancellationToken: cancellationToken);
}
}
}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Shared/KnownCharacteristics.cs b/Source/Plugin.BLE/Shared/KnownCharacteristics.cs
index 86c38c9f..5baf48ec 100644
--- a/Source/Plugin.BLE/Shared/KnownCharacteristics.cs
+++ b/Source/Plugin.BLE/Shared/KnownCharacteristics.cs
@@ -57,6 +57,9 @@ public static KnownCharacteristic Lookup(Guid id)
new KnownCharacteristic("DST Offset", Guid.ParseExact("00002a0d-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Exact Time 256", Guid.ParseExact("00002a0c-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Firmware Revision String", Guid.ParseExact("00002a26-0000-1000-8000-00805f9b34fb", "d")),
+ new KnownCharacteristic("Fitness Machine Control Point", Guid.ParseExact("00002ad9-0000-1000-8000-00805f9b34fb", "d")),
+ new KnownCharacteristic("Fitness Machine Feature", Guid.ParseExact("00002acc-0000-1000-8000-00805f9b34fb", "d")),
+ new KnownCharacteristic("Fitness Machine Status", Guid.ParseExact("00002ada-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Glucose Feature", Guid.ParseExact("00002a51-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Glucose Measurement", Guid.ParseExact("00002a18-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Glucose Measure Context", Guid.ParseExact("00002a34-0000-1000-8000-00805f9b34fb", "d")),
@@ -99,6 +102,7 @@ public static KnownCharacteristic Lookup(Guid id)
new KnownCharacteristic("Service Changed", Guid.ParseExact("00002a05-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Software Revision String", Guid.ParseExact("00002a28-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Supported New Alert Category", Guid.ParseExact("00002a47-0000-1000-8000-00805f9b34fb", "d")),
+ new KnownCharacteristic("Supported Speed Range", Guid.ParseExact("00002ad4-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Supported Unread Alert Category", Guid.ParseExact("00002a48-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("System ID", Guid.ParseExact("00002a23-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Temperature Measurement", Guid.ParseExact("00002a1c-0000-1000-8000-00805f9b34fb", "d")),
@@ -109,6 +113,8 @@ public static KnownCharacteristic Lookup(Guid id)
new KnownCharacteristic("Time Update State", Guid.ParseExact("00002a17-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Time with DST", Guid.ParseExact("00002a11-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Time Zone", Guid.ParseExact("00002a0e-0000-1000-8000-00805f9b34fb", "d")),
+ new KnownCharacteristic("Training Status", Guid.ParseExact("00002ad3-0000-1000-8000-00805f9b34fb", "d")),
+ new KnownCharacteristic("Treadmill Data", Guid.ParseExact("00002acd-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Tx Power Level", Guid.ParseExact("00002a07-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Unread Alert Status", Guid.ParseExact("00002a45-0000-1000-8000-00805f9b34fb", "d")),
new KnownCharacteristic("Aerobic Heart Rate Lower Limit", Guid.ParseExact("00002a7e-0000-1000-8000-00805f9b34fb", "d")),
diff --git a/Source/Plugin.BLE/Shared/KnownServices.cs b/Source/Plugin.BLE/Shared/KnownServices.cs
index 56d9ee95..305443d5 100644
--- a/Source/Plugin.BLE/Shared/KnownServices.cs
+++ b/Source/Plugin.BLE/Shared/KnownServices.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Plugin.BLE.Abstractions.Extensions;
namespace Plugin.BLE.Abstractions
{
@@ -30,39 +31,55 @@ public static KnownService Lookup(Guid id)
///
public static IReadOnlyList Services { get; } = new List
{
- new KnownService("Alert Notification Service", Guid.ParseExact("00001811-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Battery Service", Guid.ParseExact("0000180f-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Blood Pressure", Guid.ParseExact("00001810-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Current Time Service", Guid.ParseExact("00001805-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Cycling Power", Guid.ParseExact("00001818-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Cycling Speed and Cadence", Guid.ParseExact("00001816-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Device Information", Guid.ParseExact("0000180a-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Generic Access", Guid.ParseExact("00001800-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Generic Attribute", Guid.ParseExact("00001801-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Glucose", Guid.ParseExact("00001808-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Health Thermometer", Guid.ParseExact("00001809-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Heart Rate", Guid.ParseExact("0000180d-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Human Interface Device", Guid.ParseExact("00001812-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Immediate Alert", Guid.ParseExact("00001802-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Link Loss", Guid.ParseExact("00001803-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Location and Navigation", Guid.ParseExact("00001819-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Next DST Change Service", Guid.ParseExact("00001807-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Phone Alert Status Service", Guid.ParseExact("0000180e-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Reference Time Update Service", Guid.ParseExact("00001806-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Running Speed and Cadence", Guid.ParseExact("00001814-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("Scan Parameters", Guid.ParseExact("00001813-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("TX Power", Guid.ParseExact("00001804-0000-1000-8000-00805f9b34fb", "d")),
- new KnownService("TI SensorTag Smart Keys", Guid.ParseExact("0000ffe0-0000-1000-8000-00805f9b34fb", "d")),
+ new KnownService("Alert Notification", GuidExtension.UuidFromPartial(0x1811)),
+ new KnownService("Automation IO", GuidExtension.UuidFromPartial(0x1815)),
+ new KnownService("Battery", GuidExtension.UuidFromPartial(0x180f)),
+ new KnownService("Blood Pressure", GuidExtension.UuidFromPartial(0x1810)),
+ new KnownService("Body Composition", GuidExtension.UuidFromPartial(0x181b)),
+ new KnownService("Bond Management", GuidExtension.UuidFromPartial(0x181e)),
+ new KnownService("Continuous Glucose", GuidExtension.UuidFromPartial(0x181f)),
+ new KnownService("Current Time", GuidExtension.UuidFromPartial(0x1805)),
+ new KnownService("Cycling Power", GuidExtension.UuidFromPartial(0x1818)),
+ new KnownService("Cycling Speed and Cadence", GuidExtension.UuidFromPartial(0x1816)),
+ new KnownService("Device Information", GuidExtension.UuidFromPartial(0x180a)),
+ new KnownService("Environmental Sensing", GuidExtension.UuidFromPartial(0x181a)),
+ new KnownService("Fitness Machine", GuidExtension.UuidFromPartial(0x1826)),
+ new KnownService("Generic Access", GuidExtension.UuidFromPartial(0x1800)),
+ new KnownService("Generic Attribute", GuidExtension.UuidFromPartial(0x1801)),
+ new KnownService("Glucose", GuidExtension.UuidFromPartial(0x1808)),
+ new KnownService("Health Thermometer", GuidExtension.UuidFromPartial(0x1809)),
+ new KnownService("Heart Rate", GuidExtension.UuidFromPartial(0x180d)),
+ new KnownService("HTTP Proxy", GuidExtension.UuidFromPartial(0x1823)),
+ new KnownService("Human Interface Device", GuidExtension.UuidFromPartial(0x1812)),
+ new KnownService("Immediate Alert", GuidExtension.UuidFromPartial(0x1802)),
+ new KnownService("Indoor Positioning", GuidExtension.UuidFromPartial(0x1821)),
+ new KnownService("Internet Protocol Support", GuidExtension.UuidFromPartial(0x1820)),
+ new KnownService("Link Loss", GuidExtension.UuidFromPartial(0x1803)),
+ new KnownService("Location and Navigation", GuidExtension.UuidFromPartial(0x1819)),
+ new KnownService("Next DST Change", GuidExtension.UuidFromPartial(0x1807)),
+ new KnownService("Object Transfer", GuidExtension.UuidFromPartial(0x1825)),
+ new KnownService("Phone Alert Status", GuidExtension.UuidFromPartial(0x180e)),
+ new KnownService("Pulse Oximeter", GuidExtension.UuidFromPartial(0x1822)),
+ new KnownService("Reference Time Update", GuidExtension.UuidFromPartial(0x1806)),
+ new KnownService("Running Speed and Cadence", GuidExtension.UuidFromPartial(0x1814)),
+ new KnownService("Scan Parameters", GuidExtension.UuidFromPartial(0x1813)),
+ new KnownService("Transport Discovery", GuidExtension.UuidFromPartial(0x1824)),
+ new KnownService("TX Power", GuidExtension.UuidFromPartial(0x1804)),
+ new KnownService("User Data", GuidExtension.UuidFromPartial(0x181c)),
+ new KnownService("Weight Scale", GuidExtension.UuidFromPartial(0x181d)),
+
+ new KnownService("TI SensorTag Smart Keys", Guid.ParseExact("0000ffe0-0000-1000-8000-00805f9b34fb", "d")),
new KnownService("TI SensorTag Infrared Thermometer", Guid.ParseExact("f000aa00-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag Accelerometer", Guid.ParseExact("f000aa10-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag Humidity", Guid.ParseExact("f000aa20-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag Magnometer", Guid.ParseExact("f000aa30-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag Barometer", Guid.ParseExact("f000aa40-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag Gyroscope", Guid.ParseExact("f000aa50-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag Test", Guid.ParseExact("f000aa60-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag Connection Control", Guid.ParseExact("f000ccc0-0451-4000-b000-000000000000", "d")),
- new KnownService("TI SensorTag OvertheAir Download", Guid.ParseExact("f000ffc0-0451-4000-b000-000000000000", "d")),
- new KnownService("TXRX_SERV_UUID RedBearLabs Biscuit Service", Guid.ParseExact("713d0000-503e-4c75-ba94-3148f18d941e", "d")),
+ new KnownService("TI SensorTag Accelerometer", Guid.ParseExact("f000aa10-0451-4000-b000-000000000000", "d")),
+ new KnownService("TI SensorTag Humidity", Guid.ParseExact("f000aa20-0451-4000-b000-000000000000", "d")),
+ new KnownService("TI SensorTag Magnometer", Guid.ParseExact("f000aa30-0451-4000-b000-000000000000", "d")),
+ new KnownService("TI SensorTag Barometer", Guid.ParseExact("f000aa40-0451-4000-b000-000000000000", "d")),
+ new KnownService("TI SensorTag Gyroscope", Guid.ParseExact("f000aa50-0451-4000-b000-000000000000", "d")),
+ new KnownService("TI SensorTag Test", Guid.ParseExact("f000aa60-0451-4000-b000-000000000000", "d")),
+ new KnownService("TI SensorTag Connection Control", Guid.ParseExact("f000ccc0-0451-4000-b000-000000000000", "d")),
+ new KnownService("TI SensorTag OvertheAir Download", Guid.ParseExact("f000ffc0-0451-4000-b000-000000000000", "d")),
+
+ new KnownService("TXRX_SERV_UUID RedBearLabs Biscuit", Guid.ParseExact("713d0000-503e-4c75-ba94-3148f18d941e", "d")),
}.AsReadOnly();
}
-}
\ No newline at end of file
+}
diff --git a/Source/Plugin.BLE/Shared/Utils/FakeAdapter.cs b/Source/Plugin.BLE/Shared/Utils/FakeAdapter.cs
index 9dd9ea71..5893f896 100644
--- a/Source/Plugin.BLE/Shared/Utils/FakeAdapter.cs
+++ b/Source/Plugin.BLE/Shared/Utils/FakeAdapter.cs
@@ -8,12 +8,17 @@ namespace Plugin.BLE.Abstractions.Utils
{
internal class FakeAdapter : AdapterBase
{
- public override Task ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters, CancellationToken cancellationToken)
+ public override Task ConnectToKnownDeviceNativeAsync(Guid deviceGuid, ConnectParameters connectParameters, CancellationToken cancellationToken)
{
TraceUnavailability();
return Task.FromResult(null);
}
+ public override Task BondAsync(IDevice device)
+ {
+ return Task.FromResult(0);
+ }
+
protected override Task StartScanningForDevicesNativeAsync(ScanFilterOptions scanFilterOptions, bool allowDuplicatesKey, CancellationToken scanCancellationToken)
{
TraceUnavailability();
@@ -47,6 +52,11 @@ public override IReadOnlyList GetSystemConnectedOrPairedDevices(Guid[]
return new List();
}
+ protected override IReadOnlyList GetBondedDevices()
+ {
+ return null; // not supported
+ }
+
public override IReadOnlyList GetKnownDevicesByIds(Guid[] ids)
{
TraceUnavailability();
diff --git a/Source/Plugin.BLE/Shared/Utils/TaskBuilder.cs b/Source/Plugin.BLE/Shared/Utils/TaskBuilder.cs
index d75c87cd..43bdc8db 100644
--- a/Source/Plugin.BLE/Shared/Utils/TaskBuilder.cs
+++ b/Source/Plugin.BLE/Shared/Utils/TaskBuilder.cs
@@ -4,6 +4,9 @@
namespace Plugin.BLE.Abstractions.Utils
{
+ ///
+ /// Builder class to create event driven Tasks that may be marshalled onto the main thread.
+ ///
public static class TaskBuilder
{
///
@@ -15,12 +18,15 @@ public static class TaskBuilder
/// Platform specific main thread invocation. Useful to avoid GATT 133 errors on Android.
/// Set this to NULL in order to disable main thread queued invocations.
/// Android: already implemented and set by default
- /// UWP, iOS, macOS: NULL by default - not needed, turning this on is redundant as it's already handled internaly by the platform
+ /// Windows, iOS, macOS: NULL by default - not needed, turning this on is redundant as it's already handled internaly by the platform
///
public static Action MainThreadInvoker { get; set; }
private static readonly SemaphoreSlim QueueSemaphore = new SemaphoreSlim(1);
+ ///
+ /// Creates an event driven chain of Actions.
+ ///
public static async Task FromEvent(
Action execute,
Func, Action, TEventHandler> getCompleteHandler,
@@ -29,7 +35,8 @@ public static async Task FromEvent, TRejectHandler> getRejectHandler,
Action subscribeReject,
Action unsubscribeReject,
- CancellationToken token = default)
+ CancellationToken token = default,
+ bool mainthread = true)
{
var tcs = new TaskCompletionSource();
void Complete(TReturn args) => tcs.TrySetResult(args);
@@ -45,7 +52,7 @@ public static async Task FromEvent tcs.TrySetCanceled(), false))
{
- return await SafeEnqueueAndExecute(execute, token, tcs);
+ return await SafeEnqueueAndExecute(execute, token, tcs, mainthread);
}
}
finally
@@ -55,13 +62,16 @@ public static async Task FromEvent SafeEnqueueAndExecute(execute, token);
+ ///
+ /// Queues the given onto the main thread and executes it.
+ ///
+ public static Task EnqueueOnMainThreadAsync(Action execute, CancellationToken token = default, bool mainthread = true)
+ => SafeEnqueueAndExecute(execute, token, mainthread: mainthread);
- private static async Task SafeEnqueueAndExecute(Action execute, CancellationToken token, TaskCompletionSource tcs = null)
+ private static async Task SafeEnqueueAndExecute(Action execute, CancellationToken token, TaskCompletionSource tcs = null, bool mainthread = true)
{
- if (MainThreadInvoker != null)
+ if (MainThreadInvoker != null && mainthread)
{
var shouldReleaseSemaphore = false;
var shouldCompleteTask = tcs == null;
@@ -108,4 +118,4 @@ private static async Task SafeEnqueueAndExecute(Action execute
return await (tcs?.Task ?? Task.FromResult(default(TReturn)));
}
}
-}
\ No newline at end of file
+}
diff --git a/Source/Plugin.BLE/Windows/Adapter.cs b/Source/Plugin.BLE/Windows/Adapter.cs
index df2f864a..15afc820 100644
--- a/Source/Plugin.BLE/Windows/Adapter.cs
+++ b/Source/Plugin.BLE/Windows/Adapter.cs
@@ -1,37 +1,56 @@
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading;
using System.Threading.Tasks;
-
-#if WINDOWS_UWP
-using Windows.System;
-using Microsoft.Toolkit.Uwp.Connectivity;
-#else
-using Microsoft.UI.Dispatching;
-using CommunityToolkit.WinUI.Connectivity;
-#endif
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.Advertisement;
-
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Extensions;
+using System.Collections.Concurrent;
+using Windows.Devices.Enumeration;
-namespace Plugin.BLE.UWP
+namespace Plugin.BLE.Windows
{
public class Adapter : AdapterBase
{
- private BluetoothLEHelper _bluetoothHelper;
+ private readonly BluetoothAdapter _bluetoothAdapter;
private BluetoothLEAdvertisementWatcher _bleWatcher;
- private DispatcherQueue _dq;
- public Adapter(BluetoothLEHelper bluetoothHelper)
+ ///
+ /// Registry used to store device instances for pending disconnect operations
+ /// Helps to detect connection lost events.
+ ///
+ private readonly IDictionary disconnectingRegistry = new ConcurrentDictionary();
+
+ public Adapter(BluetoothAdapter adapter)
+ {
+ _bluetoothAdapter = adapter;
+ }
+
+ public override async Task BondAsync(IDevice device)
{
- _bluetoothHelper = bluetoothHelper;
- _dq = DispatcherQueue.GetForCurrentThread();
+ var bleDevice = device.NativeDevice as BluetoothLEDevice;
+ if (bleDevice is null)
+ {
+ Trace.Message($"BondAsync failed since NativeDevice is null with: {device.Name}: {device.Id} ");
+ return;
+ }
+ DeviceInformation deviceInformation = await DeviceInformation.CreateFromIdAsync(bleDevice.DeviceId);
+ if (deviceInformation.Pairing.IsPaired)
+ {
+ Trace.Message($"BondAsync is already paired with: {device.Name}: {device.Id}");
+ return;
+ }
+ if (!deviceInformation.Pairing.CanPair)
+ {
+ Trace.Message($"BondAsync cannot pair with: {device.Name}: {device.Id}");
+ return;
+ }
+ DevicePairingResult result = await deviceInformation.Pairing.PairAsync();
+ Trace.Message($"BondAsync pairing result was {result.Status} with: {device.Name}: {device.Id}");
}
protected override Task StartScanningForDevicesNativeAsync(ScanFilterOptions scanFilterOptions, bool allowDuplicatesKey, CancellationToken scanCancellationToken)
@@ -52,10 +71,8 @@ protected override Task StartScanningForDevicesNativeAsync(ScanFilterOptions sca
Trace.Message($"ScanFilters: {string.Join(", ", serviceUuids)}");
}
-
- _bleWatcher.Received -= DeviceFoundAsync;
- _bleWatcher.Received += DeviceFoundAsync;
-
+ _bleWatcher.Received -= AdvertisementReceived;
+ _bleWatcher.Received += AdvertisementReceived;
_bleWatcher.Start();
return Task.FromResult(true);
}
@@ -72,74 +89,163 @@ protected override void StopScanNative()
protected override async Task ConnectToDeviceNativeAsync(IDevice device, ConnectParameters connectParameters, CancellationToken cancellationToken)
{
- Trace.Message($"Connecting to device with ID: {device.Id.ToString()}");
+ var dev = device as Device;
+ if (dev.NativeDevice == null)
+ {
+ await dev.RecreateNativeDevice();
+ }
+ var nativeDevice = device.NativeDevice as BluetoothLEDevice;
+ Trace.Message("ConnectToDeviceNativeAsync {0} Named: {1} Connected: {2}", device.Id.ToHexBleAddress(), device.Name, nativeDevice.ConnectionStatus);
- if (!(device.NativeDevice is ObservableBluetoothLEDevice nativeDevice))
- return;
- nativeDevice.PropertyChanged -= Device_ConnectionStatusChanged;
- nativeDevice.PropertyChanged += Device_ConnectionStatusChanged;
+ bool success = await dev.ConnectInternal(connectParameters, cancellationToken);
+ if (success)
+ {
+ if (!ConnectedDeviceRegistry.ContainsKey(device.Id.ToString()))
+ {
+ ConnectedDeviceRegistry[device.Id.ToString()] = device;
+ nativeDevice.ConnectionStatusChanged += Device_ConnectionStatusChanged;
+ if (nativeDevice.ConnectionStatus == BluetoothConnectionStatus.Connected)
+ {
+ Device_ConnectionStatusChanged(nativeDevice, null);
+ }
+ }
+ }
+ else
+ {
+ // use DisconnectDeviceNative to clean up resources otherwise windows won't disconnect the device
+ // after a subsequent successful connection (#528, #536, #423)
+ DisconnectDeviceNative(device);
- ConnectedDeviceRegistry[device.Id.ToString()] = device;
+ // fire a connection failed event
+ HandleConnectionFail(device, "Failed connecting to device.");
- await nativeDevice.ConnectAsync();
+ // this is normally done in Device_ConnectionStatusChanged but since nothing actually connected
+ // or disconnect, ConnectionStatusChanged will not fire.
+ ConnectedDeviceRegistry.TryRemove(device.Id.ToString(), out _);
+ }
}
- private void Device_ConnectionStatusChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
+ private void Device_ConnectionStatusChanged(BluetoothLEDevice nativeDevice, object args)
{
- if (!(sender is ObservableBluetoothLEDevice nativeDevice) || nativeDevice.BluetoothLEDevice == null)
- {
- return;
- }
+ Trace.Message("Device_ConnectionStatusChanged {0} {1} {2}",
+ nativeDevice.BluetoothAddress.ToHexBleAddress(),
+ nativeDevice.Name,
+ nativeDevice.ConnectionStatus);
+ var id = nativeDevice.BluetoothAddress.ParseDeviceId().ToString();
- if (propertyChangedEventArgs.PropertyName != nameof(nativeDevice.IsConnected))
- {
- return;
- }
-
- var address = ParseDeviceId(nativeDevice.BluetoothLEDevice.BluetoothAddress).ToString();
- if (nativeDevice.IsConnected && ConnectedDeviceRegistry.TryGetValue(address, out var connectedDevice))
+ if (nativeDevice.ConnectionStatus == BluetoothConnectionStatus.Connected
+ && ConnectedDeviceRegistry.TryGetValue(id, out var connectedDevice))
{
+#if WINDOWS10_0_22000_0_OR_GREATER
+ if (Environment.OSVersion.Version.Build >= 22000)
+ {
+ var conpar = nativeDevice.GetConnectionParameters();
+ Trace.Message(
+ $"Connected with Latency = {conpar.ConnectionLatency}, "
+ + $"Interval = {conpar.ConnectionInterval}, Timeout = {conpar.LinkTimeout}");
+ }
+#endif
HandleConnectedDevice(connectedDevice);
return;
}
- if (!nativeDevice.IsConnected && ConnectedDeviceRegistry.TryRemove(address, out var disconnectedDevice))
+ if (nativeDevice.ConnectionStatus == BluetoothConnectionStatus.Disconnected
+ && ConnectedDeviceRegistry.TryRemove(id, out var disconnectedDevice))
{
- HandleDisconnectedDevice(true, disconnectedDevice);
+ bool disconnectRequested = disconnectingRegistry.Remove(id);
+ if (!disconnectRequested)
+ {
+ // Device was powered off or went out of range. Call DisconnectInternal to cleanup
+ // resources otherwise windows will not disconnect on a subsequent connect-disconnect.
+ ((Device)disconnectedDevice).DisconnectInternal();
+ }
+ ConnectedDeviceRegistry.Remove(id, out _);
+ nativeDevice.ConnectionStatusChanged -= Device_ConnectionStatusChanged;
+ // fire the correct event (DeviceDisconnected or DeviceConnectionLost)
+ HandleDisconnectedDevice(disconnectRequested, disconnectedDevice);
}
}
protected override void DisconnectDeviceNative(IDevice device)
{
// Windows doesn't support disconnecting, so currently just dispose of the device
- Trace.Message($"Disconnected from device with ID: {device.Id}");
-
- if (device.NativeDevice is ObservableBluetoothLEDevice)
- {
- ((Device)device).ClearServices();
- device.Dispose();
- }
+ Trace.Message($"DisconnectDeviceNative from device with ID: {device.Id.ToHexBleAddress()}");
+ disconnectingRegistry[device.Id.ToString()] = device;
+ ((Device)device).DisconnectInternal();
}
- public override async Task ConnectToKnownDeviceAsync(Guid deviceGuid, ConnectParameters connectParameters = default, CancellationToken cancellationToken = default)
+ public override async Task ConnectToKnownDeviceNativeAsync(Guid deviceGuid, ConnectParameters connectParameters = default, CancellationToken cancellationToken = default)
{
- //convert GUID to string and take last 12 characters as MAC address
- var guidString = deviceGuid.ToString("N").Substring(20);
- var bluetoothAddress = Convert.ToUInt64(guidString, 16);
- var nativeDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(bluetoothAddress);
- var knownDevice = new Device(this, nativeDevice, 0, deviceGuid, _dq);
+ var bleAddress = deviceGuid.ToBleAddress();
+ var nativeDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(bleAddress);
+ if (nativeDevice == null)
+ throw new Abstractions.Exceptions.DeviceConnectionException(deviceGuid, "", $"[Adapter] Device {deviceGuid} not found.");
+
+ var knownDevice = new Device(this, nativeDevice, 0, deviceGuid);
- await ConnectToDeviceAsync(knownDevice, cancellationToken: cancellationToken);
+ await ConnectToDeviceAsync(knownDevice, connectParameters, cancellationToken: cancellationToken);
return knownDevice;
}
+ protected override IReadOnlyList GetBondedDevices()
+ {
+ string pairedSelector = BluetoothLEDevice.GetDeviceSelectorFromPairingState(true);
+ DeviceInformationCollection pairedDevices = DeviceInformation.FindAllAsync(pairedSelector).GetAwaiter().GetResult();
+ List devlist = new List();
+ foreach (var dev in pairedDevices)
+ {
+ Guid id = dev.Id.ToBleDeviceGuidFromId();
+ ulong bleaddress = id.ToBleAddress();
+ var bluetoothLeDevice = BluetoothLEDevice.FromBluetoothAddressAsync(bleaddress).AsTask().Result;
+ if (bluetoothLeDevice != null)
+ {
+ var device = new Device(
+ this,
+ bluetoothLeDevice,
+ 0, id);
+ devlist.Add(device);
+ Trace.Message("GetBondedDevices: {0}: {1}", dev.Id, dev.Name);
+ }
+ else
+ {
+ Trace.Message("GetBondedDevices: {0}: {1}, BluetoothLEDevice == null", dev.Id, dev.Name);
+ }
+ }
+ return devlist;
+ }
+
public override IReadOnlyList GetSystemConnectedOrPairedDevices(Guid[] services = null)
{
- //currently no way to retrieve paired and connected devices on windows without using an
- //async method.
- Trace.Message("Returning devices connected by this app only");
- return ConnectedDevices;
+ string pairedSelector = BluetoothLEDevice.GetDeviceSelectorFromPairingState(true);
+ DeviceInformationCollection pairedDevices = DeviceInformation.FindAllAsync(pairedSelector).GetAwaiter().GetResult();
+ List devlist = ConnectedDevices.ToList();
+ List ids = ConnectedDevices.Select(d => d.Id).ToList();
+ foreach (var dev in pairedDevices)
+ {
+ Guid id = dev.Id.ToBleDeviceGuidFromId();
+ ulong bleaddress = id.ToBleAddress();
+ if (!ids.Contains(id))
+ {
+ var bluetoothLeDevice = BluetoothLEDevice.FromBluetoothAddressAsync(bleaddress).AsTask().Result;
+ if (bluetoothLeDevice != null)
+ {
+ var device = new Device(
+ this,
+ bluetoothLeDevice,
+ 0, id);
+ devlist.Add(device);
+ ids.Add(id);
+ Trace.Message("GetSystemConnectedOrPairedDevices: {0}: {1}", dev.Id, dev.Name);
+ }
+ else
+ {
+ Trace.Message("GetSystemConnectedOrPairedDevices: {0}: {1}, BluetoothLEDevice == null", dev.Id, dev.Name);
+ }
+
+ }
+ }
+ return devlist;
}
///
@@ -159,47 +265,35 @@ public static List ParseAdvertisementData(BluetoothLEAdvert
///
/// The bluetooth advertisement watcher currently being used
/// The advertisement recieved by the watcher
- private async void DeviceFoundAsync(BluetoothLEAdvertisementWatcher watcher, BluetoothLEAdvertisementReceivedEventArgs btAdv)
+ private void AdvertisementReceived(BluetoothLEAdvertisementWatcher watcher, BluetoothLEAdvertisementReceivedEventArgs btAdv)
{
- var deviceId = ParseDeviceId(btAdv.BluetoothAddress);
+ var deviceId = btAdv.BluetoothAddress.ParseDeviceId();
if (DiscoveredDevicesRegistry.TryGetValue(deviceId, out var device))
{
- Trace.Message("AdvertisdedPeripheral: {0} Id: {1}, Rssi: {2}", device.Name, device.Id, btAdv.RawSignalStrengthInDBm);
+ // This deviceId has been discovered
+ Trace.Message("AdvReceived - Old: {0}", btAdv.ToDetailedString(device.Name));
(device as Device)?.Update(btAdv.RawSignalStrengthInDBm, ParseAdvertisementData(btAdv.Advertisement));
- this.HandleDiscoveredDevice(device);
+ HandleDiscoveredDevice(device);
}
else
{
- var bluetoothLeDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(btAdv.BluetoothAddress);
+ var bluetoothLeDevice = BluetoothLEDevice.FromBluetoothAddressAsync(btAdv.BluetoothAddress).AsTask().Result;
if (bluetoothLeDevice != null) //make sure advertisement bluetooth address actually returns a device
{
- device = new Device(this, bluetoothLeDevice, btAdv.RawSignalStrengthInDBm, deviceId, _dq, ParseAdvertisementData(btAdv.Advertisement), btAdv.IsConnectable);
- Trace.Message("DiscoveredPeripheral: {0} Id: {1}, Rssi: {2}", device.Name, device.Id, btAdv.RawSignalStrengthInDBm);
- this.HandleDiscoveredDevice(device);
+ device = new Device(
+ this,
+ bluetoothLeDevice,
+ btAdv.RawSignalStrengthInDBm,
+ deviceId,
+ ParseAdvertisementData(btAdv.Advertisement),
+ btAdv.IsConnectable);
+ Trace.Message("AdvReceived - New: {0}", btAdv.ToDetailedString(device.Name));
+ HandleDiscoveredDevice(device);
}
}
}
- ///
- /// Method to parse the bluetooth address as a hex string to a UUID
- ///
- /// BluetoothLEDevice native device address
- /// a GUID that is padded left with 0 and the last 6 bytes are the bluetooth address
- private static Guid ParseDeviceId(ulong bluetoothAddress)
- {
- var macWithoutColons = bluetoothAddress.ToString("x");
- macWithoutColons = macWithoutColons.PadLeft(12, '0'); //ensure valid length
- var deviceGuid = new byte[16];
- Array.Clear(deviceGuid, 0, 16);
- var macBytes = Enumerable.Range(0, macWithoutColons.Length)
- .Where(x => x % 2 == 0)
- .Select(x => Convert.ToByte(macWithoutColons.Substring(x, 2), 16))
- .ToArray();
- macBytes.CopyTo(deviceGuid, 10);
- return new Guid(deviceGuid);
- }
-
public override IReadOnlyList GetKnownDevicesByIds(Guid[] ids)
{
// TODO: implement this
@@ -210,5 +304,10 @@ protected override Task ConnectNativeAsync(Guid uuid, Func TrySetStateAsync(bool on)
+ {
+ if (!isInitialized)
+ {
+ return false;
+ }
+ try
+ {
+ return await radio.SetStateAsync(on ? RadioState.On : RadioState.Off) == RadioAccessStatus.Allowed;
+ }
+ catch (Exception ex)
+ {
+ Trace.Message("TrySetStateAsync exception: {0}", ex.Message);
+ return false;
+ }
}
}
diff --git a/Source/Plugin.BLE/Windows/Characteristic.cs b/Source/Plugin.BLE/Windows/Characteristic.cs
index acac08bf..379d83e5 100644
--- a/Source/Plugin.BLE/Windows/Characteristic.cs
+++ b/Source/Plugin.BLE/Windows/Characteristic.cs
@@ -11,7 +11,7 @@
using Plugin.BLE.Abstractions.EventArgs;
using Plugin.BLE.Extensions;
-namespace Plugin.BLE.UWP
+namespace Plugin.BLE.Windows
{
public class Characteristic : CharacteristicBase
{
@@ -50,7 +50,7 @@ protected override async Task> GetDescriptorsNativeAs
protected override async Task<(byte[] data, int resultCode)> ReadNativeAsync()
{
var readResult = await NativeCharacteristic.ReadValueAsync(BleImplementation.CacheModeCharacteristicRead);
- var _value = readResult.GetValueOrThrowIfError();
+ _value = readResult.GetValueOrThrowIfError();
return (_value, (int)readResult.Status);
}
@@ -59,8 +59,18 @@ protected override async Task StartUpdatesNativeAsync(CancellationToken cancella
NativeCharacteristic.ValueChanged -= OnCharacteristicValueChanged;
NativeCharacteristic.ValueChanged += OnCharacteristicValueChanged;
- var result = await NativeCharacteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
- result.ThrowIfError();
+ if (Properties.HasFlag(CharacteristicPropertyType.Notify))
+ {
+ var result = await NativeCharacteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
+ result.ThrowIfError();
+ } else if (Properties.HasFlag(CharacteristicPropertyType.Indicate))
+ {
+ var result = await NativeCharacteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync(GattClientCharacteristicConfigurationDescriptorValue.Indicate);
+ result.ThrowIfError();
+ } else
+ {
+ throw new Exception($"StartUpdatesNativeAsync for {Uuid} failed since not Notify or Indicate");
+ }
}
protected override async Task StopUpdatesNativeAsync(CancellationToken cancellationToken = default)
diff --git a/Source/Plugin.BLE/Windows/Descriptor.cs b/Source/Plugin.BLE/Windows/Descriptor.cs
index d4220825..f42114d0 100644
--- a/Source/Plugin.BLE/Windows/Descriptor.cs
+++ b/Source/Plugin.BLE/Windows/Descriptor.cs
@@ -6,7 +6,7 @@
using Windows.Security.Cryptography;
using Plugin.BLE.Extensions;
-namespace Plugin.BLE.UWP
+namespace Plugin.BLE.Windows
{
public class Descriptor : DescriptorBase
{
diff --git a/Source/Plugin.BLE/Windows/Device.cs b/Source/Plugin.BLE/Windows/Device.cs
index 904cdc98..5fb0e4ad 100644
--- a/Source/Plugin.BLE/Windows/Device.cs
+++ b/Source/Plugin.BLE/Windows/Device.cs
@@ -2,25 +2,31 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-
-#if WINDOWS_UWP
-using Windows.System;
-using Microsoft.Toolkit.Uwp.Connectivity;
-#else
-using Microsoft.UI.Dispatching;
-using CommunityToolkit.WinUI.Connectivity;
-#endif
using Windows.Devices.Bluetooth;
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Extensions;
+using System.Threading;
+using System.Collections.Concurrent;
+using WBluetooth = global::Windows.Devices.Bluetooth;
+using static System.Net.Mime.MediaTypeNames;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+using Windows.Devices.Enumeration;
-namespace Plugin.BLE.UWP
+namespace Plugin.BLE.Windows
{
- public class Device : DeviceBase
+ public class Device : DeviceBase
{
- public Device(Adapter adapter, BluetoothLEDevice nativeDevice, int rssi, Guid id, DispatcherQueue dq, IReadOnlyList advertisementRecords = null, bool isConnectable = true)
- : base(adapter, new ObservableBluetoothLEDevice(nativeDevice.DeviceInformation, dq))
+ private ConcurrentBag asyncOperations = new();
+ private readonly Mutex opMutex = new Mutex(false);
+ private readonly SemaphoreSlim opSemaphore = new SemaphoreSlim(1);
+ private ConnectParameters connectParameters;
+ private GattSession gattSession = null;
+ private bool isDisposed = false;
+
+ public Device(Adapter adapter, BluetoothLEDevice nativeDevice, int rssi, Guid id,
+ IReadOnlyList advertisementRecords = null, bool isConnectable = true)
+ : base(adapter, nativeDevice)
{
Rssi = rssi;
Id = id;
@@ -40,28 +46,45 @@ public override Task UpdateRssiAsync()
//No current method to update the Rssi of a device
//In future implementations, maybe listen for device's advertisements
- Trace.Message("Request RSSI not supported in UWP");
+ Trace.Message("Request RSSI not supported in Windows");
- return Task.FromResult(true);
+ return Task.FromResult(false);
+ }
+
+ public void DisposeNativeDevice()
+ {
+ if (NativeDevice is not null)
+ {
+ NativeDevice.Dispose();
+ NativeDevice = null;
+ }
+ }
+
+ public async Task RecreateNativeDevice()
+ {
+ DisposeNativeDevice();
+ var bleAddress = Id.ToBleAddress();
+ NativeDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(bleAddress);
}
protected override async Task> GetServicesNativeAsync()
{
- if (NativeDevice?.BluetoothLEDevice == null)
+ if (NativeDevice == null)
return new List();
- var result = await NativeDevice.BluetoothLEDevice.GetGattServicesAsync(BleImplementation.CacheModeGetServices);
+ var result = await NativeDevice.GetGattServicesAsync(BleImplementation.CacheModeGetServices);
result?.ThrowIfError();
return result?.Services?
.Select(nativeService => new Service(nativeService, this))
.Cast()
.ToList() ?? new List();
+
}
protected override async Task GetServiceNativeAsync(Guid id)
{
- var result = await NativeDevice.BluetoothLEDevice.GetGattServicesForUuidAsync(id, BleImplementation.CacheModeGetServices);
+ var result = await NativeDevice.GetGattServicesForUuidAsync(id, BleImplementation.CacheModeGetServices);
result.ThrowIfError();
var nativeService = result.Services?.FirstOrDefault();
@@ -70,40 +93,178 @@ protected override async Task GetServiceNativeAsync(Guid id)
protected override DeviceState GetState()
{
- if (NativeDevice.IsConnected)
+ if (NativeDevice is null)
+ {
+ return DeviceState.Disconnected;
+ }
+ if (gattSession is null)
+ {
+ // This is the case if the OS already is connected, but the ConnectInternal method has not yet been called
+ // Because the gattSession is created in the ConnectInternal method
+ return DeviceState.Limited;
+ }
+ if (NativeDevice.ConnectionStatus == BluetoothConnectionStatus.Connected)
{
return DeviceState.Connected;
}
-
- return NativeDevice.IsPaired ? DeviceState.Limited : DeviceState.Disconnected;
+ return NativeDevice.WasSecureConnectionUsedForPairing ? DeviceState.Limited : DeviceState.Disconnected;
}
- protected override Task RequestMtuNativeAsync(int requestValue)
+ protected override async Task RequestMtuNativeAsync(int requestValue)
{
- Trace.Message("Request MTU not supported in UWP");
- return Task.FromResult(-1);
+ // Ref https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.genericattributeprofile.gattsession.maxpdusize
+ // There are no means in windows to request a change, but we can read the current value
+ if (gattSession is null)
+ {
+ Trace.Message("WARNING RequestMtuNativeAsync failed since gattSession is null");
+ return -1;
+ }
+ return gattSession.MaxPduSize;
}
protected override bool UpdateConnectionIntervalNative(ConnectionInterval interval)
{
- Trace.Message("Update Connection Interval not supported in UWP");
+ Trace.Message("Update Connection Interval not supported in Windows");
return false;
}
+ static bool MaybeRequestPreferredConnectionParameters(BluetoothLEDevice device, ConnectParameters connectParameters)
+ {
+#if WINDOWS10_0_22000_0_OR_GREATER
+ if (Environment.OSVersion.Version.Build < 22000)
+ {
+ return false;
+ }
+ BluetoothLEPreferredConnectionParameters parameters = null;
+ switch(connectParameters.ConnectionParameterSet)
+ {
+ case ConnectionParameterSet.Balanced:
+ parameters = BluetoothLEPreferredConnectionParameters.Balanced;
+ break;
+ case ConnectionParameterSet.PowerOptimized:
+ parameters = BluetoothLEPreferredConnectionParameters.PowerOptimized;
+ break;
+ case ConnectionParameterSet.ThroughputOptimized:
+ parameters = BluetoothLEPreferredConnectionParameters.ThroughputOptimized;
+ break;
+ case ConnectionParameterSet.None:
+ default:
+ break;
+ }
+ if (parameters is not null)
+ {
+ var conreq = device.RequestPreferredConnectionParameters(parameters);
+ Trace.Message($"RequestPreferredConnectionParameters({connectParameters.ConnectionParameterSet}): {conreq.Status}");
+ return conreq.Status == BluetoothLEPreferredConnectionParametersRequestStatus.Success;
+ }
+ return true;
+#else
+ return false;
+#endif
+
+ }
+ public async Task ConnectInternal(ConnectParameters connectParameters, CancellationToken cancellationToken)
+ {
+ // ref https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice.frombluetoothaddressasync
+ // Creating a BluetoothLEDevice object by calling this method alone doesn't (necessarily) initiate a connection.
+ // To initiate a connection, set GattSession.MaintainConnection to true, or call an uncached service discovery
+ // method on BluetoothLEDevice, or perform a read/write operation against the device.
+ // 2024-04-22: Note, that The DeviceInformation.Pairing.Custom.PairAsync also initiates a connection
+ if (NativeDevice is null)
+ {
+ Trace.Message("ConnectInternal says: Cannot connect since NativeDevice is null");
+ return false;
+ }
+ try
+ {
+ MaybeRequestPreferredConnectionParameters(NativeDevice, connectParameters);
+ var devId = BluetoothDeviceId.FromId(NativeDevice.DeviceId);
+ gattSession = await GattSession.FromDeviceIdAsync(devId);
+ gattSession.MaintainConnection = true;
+ gattSession.SessionStatusChanged += GattSession_SessionStatusChanged;
+ gattSession.MaxPduSizeChanged += GattSession_MaxPduSizeChanged;
+ }
+ catch (Exception ex)
+ {
+ Trace.Message("WARNING ConnectInternal failed: {0}", ex.Message);
+ DisposeGattSession();
+ return false;
+ }
+ bool success = gattSession != null;
+ return success;
+ }
+
+ private void DisposeGattSession()
+ {
+ if (gattSession != null)
+ {
+ gattSession.MaintainConnection = false;
+ gattSession.MaxPduSizeChanged -= GattSession_MaxPduSizeChanged;
+ gattSession.SessionStatusChanged -= GattSession_SessionStatusChanged;
+ gattSession.Dispose();
+ gattSession = null;
+ }
+ }
+
+ private void GattSession_SessionStatusChanged(GattSession sender, GattSessionStatusChangedEventArgs args)
+ {
+ Trace.Message("GattSession_SessionStatusChanged: " + args.Status);
+ }
+
+ private void GattSession_MaxPduSizeChanged(GattSession sender, object args)
+ {
+ Trace.Message("GattSession_MaxPduSizeChanged: {0}", sender.MaxPduSize);
+ }
+
+ public void DisconnectInternal()
+ {
+ DisposeGattSession();
+ ClearServices();
+ DisposeNativeDevice();
+ }
+
public override void Dispose()
{
- NativeDevice.Services.ToList().ForEach(s =>
+ if (isDisposed)
{
- s?.Service?.Session?.Dispose();
- s?.Service?.Dispose();
- });
+ return;
+ }
+ isDisposed = true;
+ try
+ {
+ DisposeGattSession();
+ ClearServices();
+ DisposeNativeDevice();
+ }
+ catch { }
+ }
- NativeDevice.BluetoothLEDevice?.Dispose();
- GC.Collect();
+ ~Device()
+ {
+ DisposeGattSession();
}
public override bool IsConnectable { get; protected set; }
public override bool SupportsIsConnectable { get => true; }
+
+ protected override DeviceBondState GetBondState()
+ {
+ try
+ {
+ DeviceInformation deviceInformation = DeviceInformation.CreateFromIdAsync(NativeDevice.DeviceId).AsTask().Result;
+ return deviceInformation.Pairing.IsPaired ? DeviceBondState.Bonded : DeviceBondState.NotBonded;
+ }
+ catch (Exception ex)
+ {
+ Trace.Message($"GetBondState exception for {NativeDevice.DeviceId} : {ex.Message}");
+ return DeviceBondState.NotSupported;
+ }
+ }
+
+ public override bool UpdateConnectionParameters(ConnectParameters connectParameters = default)
+ {
+ return MaybeRequestPreferredConnectionParameters(NativeDevice, connectParameters);
+ }
}
}
diff --git a/Source/Plugin.BLE/Windows/Extensions/GattProtocolErrorParser.cs b/Source/Plugin.BLE/Windows/Extensions/GattProtocolErrorParser.cs
new file mode 100644
index 00000000..d17f9216
--- /dev/null
+++ b/Source/Plugin.BLE/Windows/Extensions/GattProtocolErrorParser.cs
@@ -0,0 +1,124 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+// This file has been copied from CommunityToolkit.WinUI.Connectivity
+// https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp.Connectivity/BluetoothLEHelper/GattProtocolErrorParser.cs
+// Date: 2023-10-28 with these changes applied:
+// 1. These comments are added
+// 2. namespace Microsoft.Toolkit.Uwp.Connectivity -> Plugin.BLE.Extensions
+// 3. public static class GattProtocolErrorParser -> internal static class GattProtocolErrorParser
+
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace Plugin.BLE.Extensions
+{
+ ///
+ /// Helper function when working with
+ ///
+ internal static class GattProtocolErrorParser
+ {
+ ///
+ /// Helper to convert an Gatt error value into a string
+ ///
+ /// the byte error value.
+ /// String representation of the error
+ public static string GetErrorString(this byte? errorValue)
+ {
+ var errorString = "Protocol Error";
+
+ if (errorValue.HasValue == false)
+ {
+ return errorString;
+ }
+
+ if (errorValue == GattProtocolError.AttributeNotFound)
+ {
+ return "Attribute Not Found";
+ }
+
+ if (errorValue == GattProtocolError.AttributeNotLong)
+ {
+ return "Attribute Not Long";
+ }
+
+ if (errorValue == GattProtocolError.InsufficientAuthentication)
+ {
+ return "Insufficient Authentication";
+ }
+
+ if (errorValue == GattProtocolError.InsufficientAuthorization)
+ {
+ return "Insufficient Authorization";
+ }
+
+ if (errorValue == GattProtocolError.InsufficientEncryption)
+ {
+ return "Insufficient Encryption";
+ }
+
+ if (errorValue == GattProtocolError.InsufficientEncryptionKeySize)
+ {
+ return "Insufficient Encryption Key Size";
+ }
+
+ if (errorValue == GattProtocolError.InsufficientResources)
+ {
+ return "Insufficient Resources";
+ }
+
+ if (errorValue == GattProtocolError.InvalidAttributeValueLength)
+ {
+ return "Invalid Attribute Value Length";
+ }
+
+ if (errorValue == GattProtocolError.InvalidHandle)
+ {
+ return "Invalid Handle";
+ }
+
+ if (errorValue == GattProtocolError.InvalidOffset)
+ {
+ return "Invalid Offset";
+ }
+
+ if (errorValue == GattProtocolError.InvalidPdu)
+ {
+ return "Invalid Pdu";
+ }
+
+ if (errorValue == GattProtocolError.PrepareQueueFull)
+ {
+ return "Prepare Queue Full";
+ }
+
+ if (errorValue == GattProtocolError.ReadNotPermitted)
+ {
+ return "Read Not Permitted";
+ }
+
+ if (errorValue == GattProtocolError.RequestNotSupported)
+ {
+ return "Request Not Supported";
+ }
+
+ if (errorValue == GattProtocolError.UnlikelyError)
+ {
+ return "UnlikelyError";
+ }
+
+ if (errorValue == GattProtocolError.UnsupportedGroupType)
+ {
+ return "Unsupported Group Type";
+ }
+
+ if (errorValue == GattProtocolError.WriteNotPermitted)
+ {
+ return "Write Not Permitted";
+ }
+
+ return errorString;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/Plugin.BLE/Windows/Extensions/GattResultExtensions.cs b/Source/Plugin.BLE/Windows/Extensions/GattResultExtensions.cs
index 658577f2..2d27d34b 100644
--- a/Source/Plugin.BLE/Windows/Extensions/GattResultExtensions.cs
+++ b/Source/Plugin.BLE/Windows/Extensions/GattResultExtensions.cs
@@ -2,32 +2,26 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
-#if WINDOWS_UWP
-using Microsoft.Toolkit.Uwp.Connectivity;
-#else
-using CommunityToolkit.WinUI.Connectivity;
-#endif
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Exceptions;
namespace Plugin.BLE.Extensions
{
- public static class GattResultExtensions
+ internal static class GattResultExtensions
{
- public static void ThrowIfError(this GattWriteResult result, [CallerMemberName]string tag = null)
+ public static void ThrowIfError(this GattWriteResult result, [CallerMemberName] string tag = null)
=> result.Status.ThrowIfError(tag, result.ProtocolError);
- public static void ThrowIfError(this GattCharacteristicsResult result, [CallerMemberName]string tag = null)
+ public static void ThrowIfError(this GattCharacteristicsResult result, [CallerMemberName] string tag = null)
=> result.Status.ThrowIfError(tag, result.ProtocolError);
- public static void ThrowIfError(this GattDescriptorsResult result, [CallerMemberName]string tag = null)
+ public static void ThrowIfError(this GattDescriptorsResult result, [CallerMemberName] string tag = null)
=> result.Status.ThrowIfError(tag, result.ProtocolError);
- public static void ThrowIfError(this GattDeviceServicesResult result, [CallerMemberName]string tag = null)
+ public static void ThrowIfError(this GattDeviceServicesResult result, [CallerMemberName] string tag = null)
=> result.Status.ThrowIfError(tag, result.ProtocolError);
-
- public static byte[] GetValueOrThrowIfError(this GattReadResult result, [CallerMemberName]string tag = null)
+ public static byte[] GetValueOrThrowIfError(this GattReadResult result, [CallerMemberName] string tag = null)
{
var errorMessage = result.Status.GetErrorMessage(tag, result.ProtocolError);
if (!string.IsNullOrEmpty(errorMessage))
@@ -38,7 +32,7 @@ public static byte[] GetValueOrThrowIfError(this GattReadResult result, [CallerM
return result.Value?.ToArray() ?? new byte[0];
}
- public static void ThrowIfError(this GattCommunicationStatus status, [CallerMemberName]string tag = null, byte? protocolError = null)
+ public static void ThrowIfError(this GattCommunicationStatus status, [CallerMemberName] string tag = null, byte? protocolError = null)
{
var errorMessage = status.GetErrorMessage(tag, protocolError);
if (!string.IsNullOrEmpty(errorMessage))
@@ -56,7 +50,7 @@ private static string GetErrorMessage(this GattCommunicationStatus status, strin
Trace.Message($"[{tag}] success.");
return null;
case GattCommunicationStatus.ProtocolError when protocolError != null:
- return $"[{tag}] failed with status: {status} and protocol error {protocolError.GetErrorString()}";
+ return $"[{tag}] failed with status: {status} and protocol error {protocolError.GetErrorString()}";
case GattCommunicationStatus.AccessDenied:
case GattCommunicationStatus.ProtocolError:
case GattCommunicationStatus.Unreachable:
diff --git a/Source/Plugin.BLE/Windows/Extensions/TextHelpers.cs b/Source/Plugin.BLE/Windows/Extensions/TextHelpers.cs
new file mode 100644
index 00000000..83cbfda4
--- /dev/null
+++ b/Source/Plugin.BLE/Windows/Extensions/TextHelpers.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Versioning;
+using System.Text;
+using System.Threading.Tasks;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.Advertisement;
+
+namespace Plugin.BLE.Extensions;
+
+public static class TextHelpers
+{
+ public static string ToDetailedString(this BluetoothLEAdvertisementReceivedEventArgs btAdv, string name = "na")
+ {
+ string hexadr = btAdv.BluetoothAddress.ToHexBleAddress();
+ StringBuilder sb = new StringBuilder();
+ sb.Append(hexadr)
+ .Append(", ").Append(name)
+ .Append(", ").Append(btAdv.BluetoothAddressType)
+ .Append(", ").Append(btAdv.RawSignalStrengthInDBm)
+ .Append(", ").Append(btAdv.AdvertisementType);
+
+ if (btAdv.IsConnectable)
+ {
+ sb.Append(", Connectable");
+ }
+ if (btAdv.IsScannable)
+ {
+ sb.Append(", Scannable");
+ }
+ if (btAdv.IsScanResponse)
+ {
+ sb.Append(", ScanResponse");
+ }
+ if (btAdv.IsDirected)
+ {
+ sb.Append(", Directed");
+ }
+ return sb.ToString();
+ }
+
+ ///
+ /// Get a string of the BLE address: 48 bit = 6 bytes = 12 Hex chars
+ ///
+ ///
+ ///
+ public static string ToHexBleAddress(this Guid id)
+ {
+ return id.ToString("N").Substring(20).ToUpperInvariant();
+ //return id.ToString()[^12..].ToUpperInvariant(); //Not for netstandard2.0
+ }
+
+ ///
+ /// Get a string of the BLE address: 48 bit = 6 bytes = 12 Hex chars
+ ///
+ ///
+ ///
+ public static string ToHexBleAddress(this ulong bluetoothAddress)
+ {
+ return bluetoothAddress.ToString("X12");
+ }
+
+ ///
+ /// Method to parse the bluetooth address as a hex string to a UUID
+ ///
+ /// BluetoothLEDevice native device address
+ /// a GUID that is padded left with 0 and the last 6 bytes are the bluetooth address
+ public static Guid ParseDeviceId(this ulong bluetoothAddress)
+ {
+ var macWithoutColons = bluetoothAddress.ToString("x");
+ macWithoutColons = macWithoutColons.PadLeft(12, '0'); //ensure valid length
+ var deviceGuid = new byte[16];
+ Array.Clear(deviceGuid, 0, 16);
+ var macBytes = Enumerable.Range(0, macWithoutColons.Length)
+ .Where(x => x % 2 == 0)
+ .Select(x => Convert.ToByte(macWithoutColons.Substring(x, 2), 16))
+ .ToArray();
+ macBytes.CopyTo(deviceGuid, 10);
+ return new Guid(deviceGuid);
+ }
+
+ ///
+ /// Convert 12 chars hex string = 6 bytes = 48 bits to Guid used in this plugin
+ ///
+ ///
+ ///
+ public static Guid ToBleDeviceGuid(this string macWithoutColons)
+ {
+ macWithoutColons = macWithoutColons.PadLeft(12, '0'); //ensure valid length
+ var deviceGuid = new byte[16];
+ Array.Clear(deviceGuid, 0, 16);
+ var macBytes = Enumerable.Range(0, macWithoutColons.Length)
+ .Where(x => x % 2 == 0)
+ .Select(x => Convert.ToByte(macWithoutColons.Substring(x, 2), 16))
+ .ToArray();
+ macBytes.CopyTo(deviceGuid, 10);
+ return new Guid(deviceGuid);
+ }
+
+ public static Guid ToBleDeviceGuidFromId(this string idWithColons)
+ {
+ //example: Bluetooth#Bluetoothe4:aa:ea:cd:28:00-70:bf:92:06:e1:9e
+ var nocolons = idWithColons.Replace(":", "");
+ return ToBleDeviceGuid(nocolons.Substring(nocolons.Length-12, 12));
+ }
+
+
+ public static ulong ToBleAddress(this Guid deviceGuid)
+ {
+ //convert GUID to string and take last 12 characters as MAC address
+ var guidString = deviceGuid.ToString("N").Substring(20);
+ var bluetoothAddress = Convert.ToUInt64(guidString, 16);
+ return bluetoothAddress;
+ }
+}
diff --git a/Source/Plugin.BLE/Windows/Service.cs b/Source/Plugin.BLE/Windows/Service.cs
index b8bb8a03..29eca911 100644
--- a/Source/Plugin.BLE/Windows/Service.cs
+++ b/Source/Plugin.BLE/Windows/Service.cs
@@ -8,7 +8,7 @@
using Plugin.BLE.Extensions;
using System.Threading;
-namespace Plugin.BLE.UWP
+namespace Plugin.BLE.Windows
{
public class Service : ServiceBase
{
diff --git a/cleanup.sh b/cleanup.sh
index a6284fc1..9579519a 100644
--- a/cleanup.sh
+++ b/cleanup.sh
@@ -5,3 +5,6 @@ find . -type d \( -name 'bin' -o -name 'obj' \) -prune -exec rm -rf {} \;
# remove Resource.designer.cs files
find . -name 'Resource.designer.cs' -exec rm -rf {} \;
+
+# remove VS cache folder
+rm -rf Source/.vs/
diff --git a/doc/changelog.md b/doc/changelog.md
index e80d09a7..16d29585 100644
--- a/doc/changelog.md
+++ b/doc/changelog.md
@@ -1,7 +1,63 @@
# Changelog
+## 3.1 .NET 8 & Gemeric Windows Implementation
+
+### 3.1.0
+- #844 Fix Device.UpdateRssiAsync on Windows (fixes #810)
+- #845 Implement Adapter.SupportsExtendedAdvertising on Windows (fixes #815)
+- #848 Windows: Fixed null entries in DiscoveredDevices (fixes #828)
+- #849 Windows: Connect -> ConnectionLost -> Reconnect (fixes #826)
+- #853 Windows: Check for OS build number version Runtime (fixes #852)
+- #855 Android: Bluetooth stack breaks when trying connect on phone boot (fixes #854)
+- #858 Windows: Fixed StartUpdatesNativeAsync for indicate (fixes #847)
+
+#### 3.1.0-rc.1
+- #820 Windows Implementation for BondAsync and BondedDevices
+- #839 Windows: Handle that a device is already connected
+
+#### 3.1.0-beta.3
+- #823 Implement BleImplementation.TrySetStateAsync on Android (fixes #821)
+- #827 Fix a NullReferenceException in Characteristic.NSErrorToGattStatus on iOS (fixes #825)
+
+#### 3.1.0-beta.2
+- #784 Turn Bluetooth Radio On/Off
+- #801 Connection parameters for Windows
+- #805 Added services for Citysports treadmill (fixes #804)
+- #807 Fix GetSystemConnectedOrPairedDevices in Windows
+- #809 Fix ConnectionLost -> Connect for Windows
+
+#### 3.1.0-beta.1
+- #746 Windows.Devices.Bluetooth Only
+- #764 ReadAsync updates characteristic value on Windows
+- #770 Remove .NET 6
+- #773 Added BroadcastName to AdvertisementRecordType (fixes #772)
+- #774 disable MainThreadInvoker in Adapter.ConnectToDeviceAsync (fixes #757)
+- #776 Improve Connect In Windows
+- #777 StateChanged for Windows
+- #778 Add support for .NET 8
+
+
## 3.0 MAUI
+#### 3.0.0
+- Add support for MAUI (.NET 6 and 7), while keeping compatibility with Xamarin
+- Add support for Windows (UWP & WinUI, experimental)
+- Various improvements and bugfixes (see beta- and pre-releases)
+
+#### 3.0.0-rc.1
+- #730 Bonding on Android (fixes #690)
+- #740 Target Android 13 everywhere
+
+#### 3.0.0-beta.6
+- #728 Add Windows support for RequestMtuAsync (fixes #727)
+- #735 Fix DisconnectAsync hangs on Windows and Android after scanning then connecting with ConnectToKnownDeviceAsync (fixes #734)
+- #736 Add MAUI sample client
+
+#### 3.0.0-beta.5
+- #721 Fix Windows connect/disconnect issues (fixes #423, #528, #536, #620)
+- #719 Fixed crash with incorrect data in advertisement data on Android (fixes #567, #713)
+- #708 Coding style fixes (fixes #693)
+
#### 3.0.0-beta.4
- #669 Return error codes to application
- #679 Querying adapter capabilities: extended advertisements & coded PHY
@@ -21,6 +77,7 @@
- #614 Upgrade to .NET 6
- #638 GitHub Actions: update to .NET 7 (fixes #622, #626, #630)
+
## 2.2 UWP
#### 2.2.0-pre5
@@ -46,6 +103,7 @@
#### 2.2.0-pre.1 UWP
- UWP support pre-release
+
## 2.1 MacOS
### 2.1.3
diff --git a/doc/characteristics.md b/doc/characteristics.md
index a420b28f..3630c463 100644
--- a/doc/characteristics.md
+++ b/doc/characteristics.md
@@ -7,7 +7,7 @@
Plugin
iOS
Android
-
UWP
+
Windows
0
@@ -66,10 +66,10 @@
-Specification: [Core 4.2 Vol.3 3.3.1.1](https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=286439)
-UWP: [GattCharacteristicProperties](https://msdn.microsoft.com/en-in/library/windows/apps/windows.devices.bluetooth.genericattributeprofile.gattcharacteristicproperties)
-Android: [GattProperty](https://developer.xamarin.com/api/type/Android.Bluetooth.GattProperty/)
-iOS: [CBCharacteristicProperties](https://developer.apple.com/library/ios/documentation/CoreBluetooth/Reference/CBCharacteristic_Class/#//apple_ref/c/tdef/CBCharacteristicProperties)
+- Specification: [Core 4.2 Vol.3 3.3.1.1](https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=286439)
+- Windows: [GattCharacteristicProperties](https://msdn.microsoft.com/en-in/library/windows/apps/windows.devices.bluetooth.genericattributeprofile.gattcharacteristicproperties)
+- Android: [GattProperty](https://developer.xamarin.com/api/type/Android.Bluetooth.GattProperty/)
+- iOS: [CBCharacteristicProperties](https://developer.apple.com/library/ios/documentation/CoreBluetooth/Reference/CBCharacteristic_Class/#//apple_ref/c/tdef/CBCharacteristicProperties)
From 1 to 128 all platforms are using the values from the specification.
-iOS and UWP are using the values 256, and 512. On UWP they are mapped to extended properties (Core 4.2 §3.3.3.1). iOS is using it for non standard (4.2) values.
+iOS and Windows are using the values 256, and 512. On Windows they are mapped to extended properties (Core 4.2 §3.3.3.1). iOS is using it for non standard (4.2) values.
diff --git a/doc/scanmode_mapping.md b/doc/scanmode_mapping.md
index 2d1dfa89..1017277b 100644
--- a/doc/scanmode_mapping.md
+++ b/doc/scanmode_mapping.md
@@ -5,7 +5,7 @@
Value
iOS/macOS
Android
-
UWP
+
Windows
Passive
@@ -33,4 +33,4 @@
- iOS: Not supported. [Passive Mode will be used automatically in background mode](https://lists.apple.com/archives/bluetooth-dev/2012/May/msg00041.html) (caution: very old information!))
- Android: [ScanSettings](https://developer.android.com/reference/android/bluetooth/le/ScanSettings.html). Falls back to system default for all scan modes, if Android < Lollipop.
-- UWP: [BluetoothLEScanningMode](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothlescanningmode)
+- Windows: [BluetoothLEScanningMode](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothlescanningmode)