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.xml Resources Assets - v12.0 + v13.0 false @@ -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 @@ + + + + + + + + + + + +