├── BlazorBluetooth.png ├── BlazorBluetoothUA.png ├── SampleClientSide ├── Pages │ └── BleTester.razor ├── wwwroot │ ├── favicon.png │ ├── BlazorBluetoothUA.png │ ├── service-worker.js │ ├── manifest.webmanifest │ ├── index.html │ ├── service-worker.published.js │ └── css │ │ └── app.css ├── Layout │ ├── MainLayout.razor │ └── MainLayout.razor.css ├── _Imports.razor ├── App.razor ├── Program.cs ├── SampleClientSide.csproj └── Properties │ └── launchSettings.json ├── SampleServerSide ├── Pages │ ├── BleTester.razor │ ├── _Host.cshtml │ └── _Layout.cshtml ├── wwwroot │ ├── favicon.ico │ └── css │ │ ├── open-iconic │ │ ├── font │ │ │ ├── fonts │ │ │ │ ├── open-iconic.eot │ │ │ │ ├── open-iconic.otf │ │ │ │ ├── open-iconic.ttf │ │ │ │ └── open-iconic.woff │ │ │ └── css │ │ │ │ └── open-iconic-bootstrap.min.css │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── FONT-LICENSE │ │ └── site.css ├── appsettings.json ├── appsettings.Development.json ├── Layout │ ├── MainLayout.razor │ └── MainLayout.razor.css ├── SampleServerSide.csproj ├── _Imports.razor ├── App.razor ├── Program.cs └── Properties │ └── launchSettings.json ├── Blazor.Bluetooth ├── BlazorBluetoothUA.png ├── BluetoothServiceDataMap.cs ├── BluetoothManufacturerDataMap.cs ├── IBluetoothServiceDataMap.cs ├── IBluetoothManufacturerDataMap.cs ├── BluetoothNotSupportedException.cs ├── ScriptNotFoundException.cs ├── DeviceDisconnectHandler.cs ├── RequestDeviceCancelledException.cs ├── BluetoothAvailabilityHandler.cs ├── AdvertisementsUnavailableException.cs ├── ServiceExtensions.cs ├── AdvertisementReceivedHandler.cs ├── CharacteristicEventArgs.cs ├── CharacteristicValueHandler.cs ├── IBluetoothAdvertisingEvent.cs ├── ServiceData.cs ├── Filter.cs ├── BluetoothAdvertisingEvent.cs ├── BluetoothCharacteristicProperties.cs ├── ManufacturerData.cs ├── IDevice.cs ├── IBluetoothRemoteGATTDescriptor.cs ├── BluetoothRemoteGATTDescriptor.cs ├── IBluetoothCharacteristicProperties.cs ├── IBluetoothRemoteGATTService.cs ├── IBluetoothRemoteGATTServer.cs ├── BluetoothRemoteGATTService.cs ├── RequestDeviceOptions.cs ├── Blazor.Bluetooth.csproj ├── BluetoothRemoteGATTServer.cs ├── BluetoothUUID.cs ├── IBluetoothNavigator.cs ├── Device.cs ├── IBluetoothRemoteGATTCharacteristic.cs ├── BluetoothNavigator.cs ├── BluetoothRemoteGATTCharacteristic.cs └── wwwroot │ └── JSInterop.js ├── SampleShared ├── Components │ ├── InputTextToByteArrayComponent.razor │ ├── AddServicesComponent.razor │ ├── ShowConnectedDeviceComponent.razor │ ├── ShowDescriptorComponent.razor │ ├── BluetoothUuidsComponent.razor │ ├── AddFilterComponent.razor.cs │ ├── ShowServiceComponent.razor │ ├── AddManufacturerDataComponent.razor.cs │ ├── DeviceComponent.razor │ ├── AddServicesComponent.razor.cs │ ├── AddManufacturerDataComponent.razor │ ├── DeviceRequestComponent.razor.cs │ ├── ShowDescriptorComponent.razor.cs │ ├── AddFilterComponent.razor │ ├── DeviceRequestComponent.razor │ ├── BluetoothUuidsComponent.razor.cs │ ├── DeviceComponent.razor.cs │ ├── InputTextToByteArrayComponent.razor.cs │ ├── AdvComponent.razor │ ├── AdvComponent.razor.cs │ ├── ShowConnectedDeviceComponent.razor.cs │ ├── BleTesterPageComponent.razor │ ├── ShowServiceComponent.razor.cs │ ├── BleTesterPageComponent.razor.cs │ ├── ShowCharacteristicComponent.razor │ └── ShowCharacteristicComponent.razor.cs ├── SampleShared.csproj └── BindableBase.cs ├── .gitattributes ├── Blazor.Bluetooth.sln ├── README.md └── .gitignore /BlazorBluetooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/BlazorBluetooth.png -------------------------------------------------------------------------------- /BlazorBluetoothUA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/BlazorBluetoothUA.png -------------------------------------------------------------------------------- /SampleClientSide/Pages/BleTester.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Blazor Web Bluetooth API tester

4 | 5 | -------------------------------------------------------------------------------- /SampleServerSide/Pages/BleTester.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Blazor Web Bluetooth API tester

4 | 5 | -------------------------------------------------------------------------------- /SampleClientSide/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/SampleClientSide/wwwroot/favicon.png -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/SampleServerSide/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Blazor.Bluetooth/BlazorBluetoothUA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/Blazor.Bluetooth/BlazorBluetoothUA.png -------------------------------------------------------------------------------- /SampleClientSide/wwwroot/BlazorBluetoothUA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/SampleClientSide/wwwroot/BlazorBluetoothUA.png -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valerii-sovytskyi/Blazor.Bluetooth/HEAD/SampleServerSide/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /SampleClientSide/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 |
3 |
4 |
5 | @Body 6 |
7 |
8 |
-------------------------------------------------------------------------------- /SampleServerSide/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /SampleServerSide/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothServiceDataMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | internal class BluetoothServiceDataMap : Dictionary, IBluetoothServiceDataMap 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SampleServerSide/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace SampleServerSide.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = "_Layout"; 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothManufacturerDataMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | internal class BluetoothManufacturerDataMap : Dictionary, IBluetoothManufacturerDataMap 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SampleServerSide/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | SampleServerSide 4 | 5 |
6 |
7 |
8 | @Body 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /SampleClientSide/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); 5 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothServiceDataMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | /// 6 | /// Bluetooth service data map interface. 7 | /// 8 | public interface IBluetoothServiceDataMap : IDictionary 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothManufacturerDataMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | /// 6 | /// Bluetooth manufacturer map interface. 7 | /// 8 | public interface IBluetoothManufacturerDataMap : IDictionary 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SampleShared/Components/InputTextToByteArrayComponent.razor: -------------------------------------------------------------------------------- 1 |
2 | 3 | @if (!IsTextValid) 4 | { 5 |

Byte array is invalid format, input bytes with a space separation, with values 0-255, for ex "12 53 124 255 0"

6 | } 7 |
8 | -------------------------------------------------------------------------------- /SampleServerSide/SampleServerSide.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /SampleClientSide/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 7 | @using Microsoft.JSInterop 8 | @using SampleClientSide 9 | @using SampleClientSide.Layout 10 | @using SampleShared 11 | @using SampleShared.Components 12 | -------------------------------------------------------------------------------- /SampleShared/SampleShared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SampleClientSide/wwwroot/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SampleClientSide", 3 | "short_name": "SampleClientSide", 4 | "id": "./", 5 | "start_url": "./", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#03173d", 9 | "prefer_related_applications": false, 10 | "icons": [ 11 | { 12 | "src": "BlazorBluetoothUA.png", 13 | "type": "image/png", 14 | "sizes": "300x300" 15 | } 16 | ] 17 | } 18 | 19 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothNotSupportedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | public class BluetoothNotSupportedException : Exception 6 | { 7 | public BluetoothNotSupportedException(Exception innerException) 8 | : base("Bluetooth probably is not supported on your browser. Please check browser compatibility https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility", innerException) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/ScriptNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | public class ScriptNotFoundException : Exception 6 | { 7 | public ScriptNotFoundException(Exception ex) 8 | : base("JSInteropt is not found, check if you added in your wwwrooot/index.html for Client App, or in _Host.cshtml (.net5) / _Layout.cshtml (.net6) for Server App.", ex) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SampleServerSide/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using SampleServerSide 10 | @using SampleServerSide.Layout 11 | @using SampleShared 12 | @using SampleShared.Components 13 | -------------------------------------------------------------------------------- /SampleClientSide/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
-------------------------------------------------------------------------------- /Blazor.Bluetooth/DeviceDisconnectHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | internal class DeviceDisconnectHandler 6 | { 7 | private readonly Device _device; 8 | 9 | internal DeviceDisconnectHandler(Device device) 10 | { 11 | _device = device; 12 | } 13 | 14 | [JSInvokable] 15 | public void HandleDeviceDisconnected() 16 | { 17 | _device.RaiseOnGattServerDisconnected(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/RequestDeviceCancelledException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | public class RequestDeviceCancelledException : Exception 6 | { 7 | public RequestDeviceCancelledException() 8 | { 9 | } 10 | 11 | public RequestDeviceCancelledException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public RequestDeviceCancelledException(string message, Exception inner) 17 | : base(message, inner) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SampleClientSide/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 2 | using Blazor.Bluetooth; 3 | using SampleClientSide; 4 | using Microsoft.AspNetCore.Components.Web; 5 | 6 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 7 | builder.RootComponents.Add("#app"); 8 | builder.RootComponents.Add("head::after"); 9 | 10 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 11 | 12 | builder.Services.AddBluetoothNavigator(); 13 | 14 | await builder.Build().RunAsync(); -------------------------------------------------------------------------------- /SampleServerSide/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothAvailabilityHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | internal class BluetoothAvailabilityHandler 6 | { 7 | private readonly BluetoothNavigator _bluetoothNavigator; 8 | 9 | internal BluetoothAvailabilityHandler(BluetoothNavigator bluetoothNavigator) 10 | { 11 | _bluetoothNavigator = bluetoothNavigator; 12 | } 13 | 14 | [JSInvokable] 15 | public void HandleAvailabilityChanged() 16 | { 17 | _bluetoothNavigator.RaiseOnAvailabilityChanged(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/AdvertisementsUnavailableException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | public class AdvertisementsUnavailableException : Exception 6 | { 7 | private const string ExceptionMessage = 8 | "Advertisements unavailable.\n" + 9 | "Because of this feature in experimental mode, please make sure you enable it in your browser!\n" + 10 | "For chrome/edge about:flags/#enable-web-bluetooth-new-permissions-backend\n"; 11 | 12 | public AdvertisementsUnavailableException(Exception inner) 13 | : base(ExceptionMessage, inner) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/ServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | public static class ServiceExtensions 6 | { 7 | /// 8 | /// Add to . 9 | /// 10 | /// Service collection. 11 | /// Service collection. 12 | public static IServiceCollection AddBluetoothNavigator(this IServiceCollection services) 13 | { 14 | return services.AddTransient(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/AdvertisementReceivedHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System.Text.Json; 3 | 4 | namespace Blazor.Bluetooth 5 | { 6 | internal class AdvertisementReceivedHandler 7 | { 8 | private readonly Device _device; 9 | 10 | internal AdvertisementReceivedHandler(Device device) 11 | { 12 | _device = device; 13 | } 14 | 15 | [JSInvokable] 16 | public void HandleAdvertisementReceived(JsonElement jsonElement) 17 | { 18 | var json = jsonElement.GetRawText(); 19 | var bluetoothAdvertisingEvent = JsonSerializer.Deserialize(json); 20 | _device.RaiseAdvertisementReceived(bluetoothAdvertisingEvent); 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /SampleShared/Components/AddServicesComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 | 4 |
5 | 9 | 10 | 11 | @if (Services != null) 12 | { 13 | @for (var i = 0; i < Services.Count; i++) 14 | { 15 | var localI = i; 16 |
17 | @Services[localI] 18 | 19 |
20 | } 21 | } 22 |
23 | -------------------------------------------------------------------------------- /SampleServerSide/Program.cs: -------------------------------------------------------------------------------- 1 | using Blazor.Bluetooth; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | builder.Services.AddRazorPages(); 7 | builder.Services.AddServerSideBlazor(); 8 | 9 | builder.Services.AddBluetoothNavigator(); 10 | builder.Services.AddRazorComponents().AddInteractiveServerComponents(); 11 | 12 | var app = builder.Build(); 13 | 14 | // Configure the HTTP request pipeline. 15 | if (!app.Environment.IsDevelopment()) 16 | { 17 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 18 | app.UseHsts(); 19 | } 20 | 21 | app.UseHttpsRedirection(); 22 | 23 | app.UseStaticFiles(); 24 | 25 | app.UseRouting(); 26 | 27 | app.MapBlazorHub(); 28 | app.MapFallbackToPage("/_Host"); 29 | 30 | app.Run(); 31 | -------------------------------------------------------------------------------- /SampleServerSide/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:35360", 7 | "sslPort": 44352 8 | } 9 | }, 10 | "profiles": { 11 | "SampleServerSide": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7159;http://localhost:5159", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowConnectedDeviceComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 |
4 | 8 | 9 | 10 | 11 | @if (Services.Any()) 12 | { 13 | @foreach (var service in Services) 14 | { 15 | 16 | } 17 | } 18 | else 19 | { 20 |

No services here.

21 | } 22 |
-------------------------------------------------------------------------------- /Blazor.Bluetooth/CharacteristicEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | /// 6 | /// Characteristic event args for . 7 | /// 8 | public class CharacteristicEventArgs : EventArgs 9 | { 10 | /// 11 | /// Gets a string representing the UUID of this service. 12 | /// 13 | public Guid ServiceId { get; set; } 14 | 15 | /// 16 | /// Gets a string containing the UUID of the characteristic, for example '00002a37-0000-1000-8000-00805f9b34fb' for the Heart Rate Measurement characteristic. 17 | /// 18 | public Guid CharacteristicId { get; set; } 19 | 20 | /// 21 | /// Gets a value bytes. 22 | /// 23 | public byte[] Value { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowDescriptorComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 |
4 |

Properties:

5 |

Value: @Descriptor.Value

6 |

CharacteristicUuid: @Descriptor.CharacteristicUuid

7 |

DeviceUuid: @Descriptor.DeviceUuid

8 |

ServiceUuid: @Descriptor.ServiceUuid

9 |

Uuid: @Descriptor.Uuid

10 |
11 | 12 |

Methods:

13 |

Value: @ValueRead

14 | 15 |
16 | 20 |
21 | 22 |
23 | 24 |
-------------------------------------------------------------------------------- /SampleShared/Components/BluetoothUuidsComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 |
4 |
5 | 9 | 10 |

UUID is: @UUID

11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
-------------------------------------------------------------------------------- /SampleShared/BindableBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace SampleShared 5 | { 6 | /// 7 | /// Bindable base. 8 | /// 9 | public abstract class BindableBase : ComponentBase 10 | { 11 | /// 12 | /// Set property with StateHasChanged call. 13 | /// 14 | /// Any type of property. 15 | /// Property ref. 16 | /// Value. 17 | /// Property name, by default CallerMemberName 18 | /// Indicate if property changed. 19 | protected bool SetProperty(ref T property, T value, [CallerMemberName] string propertyName = "") 20 | { 21 | if (!EqualityComparer.Default.Equals(property, value)) 22 | { 23 | property = value; 24 | StateHasChanged(); 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SampleClientSide/SampleClientSide.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | service-worker-assets.js 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/CharacteristicValueHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace Blazor.Bluetooth 6 | { 7 | internal class CharacteristicValueHandler 8 | { 9 | private readonly BluetoothRemoteGATTCharacteristic _characteristic; 10 | 11 | internal CharacteristicValueHandler(BluetoothRemoteGATTCharacteristic characteristic) 12 | { 13 | _characteristic = characteristic; 14 | } 15 | 16 | [JSInvokable] 17 | public void HandleCharacteristicValueChanged(Guid serviceGuid, Guid characteristicGuid, uint[] value) 18 | { 19 | byte[] byteArray = value.Select(u => (byte)(u & 0xff)).ToArray(); 20 | 21 | var args = new CharacteristicEventArgs 22 | { 23 | ServiceId = serviceGuid, 24 | CharacteristicId = characteristicGuid, 25 | Value = byteArray 26 | }; 27 | 28 | _characteristic.RaiseCharacteristicValueChanged(args); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /SampleShared/Components/AddFilterComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using Blazor.Bluetooth; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace SampleShared.Components; 6 | public partial class AddFilterComponent : BindableBase 7 | { 8 | private List _filters; 9 | public List Filters 10 | { 11 | get => _filters; 12 | set 13 | { 14 | if (_filters != value) 15 | { 16 | _filters = value; 17 | SetRef(_filters); 18 | } 19 | } 20 | } 21 | 22 | [Parameter] 23 | public Action> SetRef { get; set; } 24 | 25 | private void AddNewFilter() 26 | { 27 | if (Filters is null) 28 | { 29 | Filters = new List(); 30 | } 31 | 32 | Filters.Add(new Filter()); 33 | StateHasChanged(); 34 | } 35 | 36 | private void RemoveFilter(Filter filter) 37 | { 38 | Filters.Remove(filter); 39 | 40 | if (Filters.Count == 0) 41 | { 42 | Filters = null; 43 | } 44 | StateHasChanged(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothAdvertisingEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Blazor.Bluetooth 2 | { 3 | public interface IBluetoothAdvertisingEvent 4 | { 5 | /// 6 | /// Gets appearance. 7 | /// 8 | public ushort Appearance { get; } 9 | 10 | /// 11 | /// Gets device. 12 | /// 13 | public IDevice Device { get; } 14 | 15 | /// 16 | /// Gets manufacturer data. 17 | /// 18 | public IBluetoothManufacturerDataMap ManufacturerData { get; } 19 | 20 | /// 21 | /// Gets name. 22 | /// 23 | public string Name { get; } 24 | 25 | /// 26 | /// Gets RSSI. 27 | /// 28 | public sbyte Rssi { get; } 29 | 30 | /// 31 | /// Gets service data. 32 | /// 33 | public IBluetoothServiceDataMap ServiceData { get; } 34 | 35 | /// 36 | /// Gets TX power. 37 | /// 38 | public sbyte TxPower { get; } 39 | 40 | /// 41 | /// Gets UUIDs. 42 | /// 43 | public string[] Uuids { get; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowServiceComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 |
4 |

Properties:

5 |

DeviceUuid: @Service.DeviceUuid

6 |

IsPrimary: @Service.IsPrimary

7 |

Uuid: @Service.Uuid

8 |

Methods:

9 | 13 | 14 | 15 | 16 | 17 | @if (Characteristics.Any()) 18 | { 19 | @foreach (var characteristic in Characteristics) 20 | { 21 | 22 | } 23 | } 24 | else 25 | { 26 |

No characteristics here.

27 | } 28 |
-------------------------------------------------------------------------------- /SampleServerSide/Pages/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | @namespace SampleServerSide.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @RenderBody() 18 | 19 |
20 | 21 | An error has occurred. This application may no longer respond until reloaded. 22 | 23 | 24 | An unhandled exception has occurred. See browser dev tools for details. 25 | 26 | Reload 27 | 🗙 28 |
29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /SampleShared/Components/AddManufacturerDataComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using Blazor.Bluetooth; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace SampleShared.Components; 5 | public partial class AddManufacturerDataComponent : BindableBase 6 | { 7 | private List _manufacturerData; 8 | public List ManufacturerData 9 | { 10 | get => _manufacturerData; 11 | set 12 | { 13 | if (_manufacturerData != value) 14 | { 15 | _manufacturerData = value; 16 | SetRef(_manufacturerData); 17 | } 18 | } 19 | } 20 | 21 | [Parameter] 22 | public Action> SetRef { get; set; } 23 | 24 | private void AddData() 25 | { 26 | if (ManufacturerData is null) 27 | { 28 | ManufacturerData = new List(); 29 | } 30 | 31 | ManufacturerData.Add(new ManufacturerData()); 32 | StateHasChanged(); 33 | } 34 | 35 | private void RemoveData(ManufacturerData manufacturerData) 36 | { 37 | ManufacturerData.Remove(manufacturerData); 38 | if (ManufacturerData.Count == 0) 39 | { 40 | ManufacturerData = null; 41 | } 42 | 43 | StateHasChanged(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/ServiceData.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | public class ServiceData 6 | { 7 | /// 8 | /// Gets or sets a service identifier. 9 | /// 10 | /// The GATT service name, the service UUID, or the UUID 16-bit or 32-bit form. 11 | /// This takes the same values as the elements of the services array. 12 | /// 13 | [JsonPropertyName("service")] 14 | public string Service { get; set; } = null; 15 | 16 | /// 17 | /// Gets or sets a data prefix. 18 | /// Optional. 19 | /// 20 | /// The data prefix. 21 | /// A buffer containing values to match against the values at the start of the advertising service data. 22 | /// 23 | [JsonPropertyName("dataPrefix")] 24 | public byte[]? DataPrefix { get; set; } = null; 25 | 26 | /// 27 | /// Gets or sets a mask. 28 | /// Optional. 29 | /// 30 | /// This allows you to match against bytes within the service data, 31 | /// by masking some bytes of the service data dataPrefix. 32 | /// 33 | [JsonPropertyName("mask")] 34 | public byte[]? Mask { get; set; } = null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/Filter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Blazor.Bluetooth 6 | { 7 | /// 8 | /// Filter for . 9 | /// 10 | [Serializable] 11 | public class Filter 12 | { 13 | /// 14 | /// Gets or sets Name of a device. 15 | /// 16 | [JsonPropertyName("name")] 17 | public string? Name { get; set; } 18 | 19 | /// 20 | /// Gets or sets Prefix name. 21 | /// 22 | [JsonPropertyName("namePrefix")] 23 | public string? NamePrefix { get; set; } 24 | 25 | /// 26 | /// Gets or sets service objects. For example those types possible: 'heart_rate', 0x1802, 0x1803, 'c48e6067-5295-48d3-8d5c-0395f61792b1'. 27 | /// 28 | [JsonPropertyName("services")] 29 | public List? Services { get; set; } = null; 30 | 31 | /// 32 | /// Gets or sets a Manufacturer data. 33 | /// An array of objects matching against manufacturer data in the Bluetooth Low Energy (BLE) advertising packets. 34 | /// 35 | [JsonPropertyName("manufacturerData")] 36 | public List? ManufacturerData { get; set; } = null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SampleShared/Components/DeviceComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 | @if (Device != null) 4 | { 5 |
6 | 7 | 8 | 9 |
10 |

Device info:

11 |

Name: @Device.Name

12 |

Id: @Device.Id

13 |

Gatt.DeviceUuid: @Device.Gatt.DeviceUuid

14 |

Gatt.Connected: @Device.Gatt.Connected

15 | 16 |
17 | 18 | @if (Device.Gatt.Connected) 19 | { 20 | 21 | } 22 | 23 |
24 |

25 | 26 | @if (!Device.Gatt.Connected) 27 | { 28 | 29 | } 30 | else 31 | { 32 | 33 | } 34 | 35 | 36 |

37 |
38 |
39 | } 40 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothAdvertisingEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Blazor.Bluetooth 2 | { 3 | internal class BluetoothAdvertisingEvent : IBluetoothAdvertisingEvent 4 | { 5 | #region Internal fields 6 | 7 | public ushort InternalAppearance { get; set; } 8 | public Device InternalDevice { get; set; } 9 | public BluetoothManufacturerDataMap InternalManufacturerData { get; set; } 10 | public string InternalName { get; set; } 11 | public sbyte InternalRssi { get; set; } 12 | public BluetoothServiceDataMap InternalServiceData { get; set; } 13 | public sbyte InternalTxPower { get; set; } 14 | 15 | // TODO: Originaly this is Array type, but I don't know if this is array of strings. Find out the type. 16 | public string[] InternalUuids { get; set; } 17 | 18 | #endregion 19 | 20 | #region Public fields 21 | 22 | public ushort Appearance => InternalAppearance; 23 | 24 | public IDevice Device => InternalDevice; 25 | 26 | public IBluetoothManufacturerDataMap ManufacturerData => InternalManufacturerData; 27 | 28 | public string Name => InternalName; 29 | 30 | public sbyte Rssi => InternalRssi; 31 | 32 | public IBluetoothServiceDataMap ServiceData => InternalServiceData; 33 | 34 | public sbyte TxPower => InternalTxPower; 35 | 36 | public string[] Uuids => InternalUuids; 37 | 38 | #endregion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SampleClientSide/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SampleClientSide 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 | An unhandled error has occurred. 28 | Reload 29 | 🗙 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /SampleShared/Components/AddServicesComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace SampleShared.Components; 4 | public partial class AddServicesComponent : BindableBase 5 | { 6 | 7 | private List _services; 8 | public List Services 9 | { 10 | get => _services; 11 | set 12 | { 13 | if (_services != value) 14 | { 15 | _services = value; 16 | SetRef(_services); 17 | } 18 | } 19 | } 20 | 21 | private string _serviceUUID; 22 | public string ServiceUUID 23 | { 24 | get => _serviceUUID; 25 | set => SetProperty(ref _serviceUUID, value); 26 | } 27 | 28 | [Parameter] 29 | public Action> SetRef { get; set; } 30 | 31 | private void AddService() 32 | { 33 | if (Services is null) 34 | { 35 | Services = new List(); 36 | } 37 | 38 | Services.Add(ServiceUUID); 39 | ServiceUUID = string.Empty; 40 | StateHasChanged(); 41 | } 42 | 43 | private void RemoveService(int index) 44 | { 45 | Services.RemoveAt(index); 46 | if (Services.Count == 0) 47 | { 48 | Services = null; 49 | } 50 | 51 | StateHasChanged(); 52 | } 53 | 54 | private void OnServiceTextChanged(int serviceIndex, object arg) 55 | { 56 | Services[serviceIndex] = arg.ToString(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothCharacteristicProperties.cs: -------------------------------------------------------------------------------- 1 | namespace Blazor.Bluetooth 2 | { 3 | internal class BluetoothCharacteristicProperties : IBluetoothCharacteristicProperties 4 | { 5 | #region Internal fields 6 | 7 | public bool InternalAuthenticatedSignedWrites { get; set; } 8 | 9 | public bool InternalBroadcast { get; set; } 10 | 11 | public bool InternalIndicate { get; set; } 12 | 13 | public bool InternalNotify { get; set; } 14 | 15 | public bool InternalRead { get; set; } 16 | 17 | public bool InternalReliableWrite { get; set; } 18 | 19 | public bool InternalWritableAuxiliaries { get; set; } 20 | 21 | public bool InternalWrite { get; set; } 22 | 23 | public bool InternalWriteWithoutResponse { get; set; } 24 | 25 | #endregion 26 | 27 | #region Public fields 28 | 29 | public bool AuthenticatedSignedWrites => InternalAuthenticatedSignedWrites; 30 | 31 | public bool Broadcast => InternalBroadcast; 32 | 33 | public bool Indicate => InternalIndicate; 34 | 35 | public bool Notify => InternalNotify; 36 | 37 | public bool Read => InternalRead; 38 | 39 | public bool ReliableWrite => InternalReliableWrite; 40 | 41 | public bool WritableAuxiliaries => InternalWritableAuxiliaries; 42 | 43 | public bool Write => InternalWrite; 44 | 45 | public bool WriteWithoutResponse => InternalWriteWithoutResponse; 46 | 47 | #endregion 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SampleServerSide/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | } 28 | 29 | .top-row a:first-child { 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | } 33 | 34 | @media (max-width: 640.98px) { 35 | .top-row:not(.auth) { 36 | display: none; 37 | } 38 | 39 | .top-row.auth { 40 | justify-content: space-between; 41 | } 42 | 43 | .top-row a, .top-row .btn-link { 44 | margin-left: 0; 45 | } 46 | } 47 | 48 | @media (min-width: 641px) { 49 | .page { 50 | flex-direction: row; 51 | } 52 | 53 | .sidebar { 54 | width: 250px; 55 | height: 100vh; 56 | position: sticky; 57 | top: 0; 58 | } 59 | 60 | .top-row { 61 | position: sticky; 62 | top: 0; 63 | z-index: 1; 64 | } 65 | 66 | .top-row, article { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SampleClientSide/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:34898", 8 | "sslPort": 44358 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5243", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7199;http://localhost:5243", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SampleShared/Components/AddManufacturerDataComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 |
4 | 5 | 6 | @if (ManufacturerData != null) 7 | { 8 | @foreach (var manufacturerData in ManufacturerData) 9 | { 10 | var thisManData = manufacturerData; 11 |
12 | 13 | 14 |
15 | 19 |
20 | 21 |
22 | 26 |
27 | 28 |
29 | 33 |
34 |
35 | } 36 | } 37 |
-------------------------------------------------------------------------------- /Blazor.Bluetooth/ManufacturerData.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | public class ManufacturerData 6 | { 7 | /// 8 | /// Gets or sets a company identifier. 9 | /// 10 | /// A mandatory number identifying the manufacturer of the device. 11 | /// Company identifiers are listed in the Bluetooth specification , Section 7. 12 | /// For example, to match against devices manufacturered by "Digianswer A/S", 13 | /// with assigned hex number 0x000C, you would specify 12. 14 | /// 15 | [JsonPropertyName("companyIdentifier")] 16 | public int CompanyIdentifier { get; set; } 17 | 18 | /// 19 | /// Gets or sets a data prefix. 20 | /// Optional. 21 | /// 22 | /// The data prefix. 23 | /// A buffer containing values to match against the values at the start 24 | /// of the advertising manufacturer data. 25 | /// 26 | [JsonPropertyName("dataPrefix")] 27 | public byte[]? DataPrefix { get; set; } = null; 28 | 29 | /// 30 | /// Gets or sets a mask. 31 | /// Optional. 32 | /// 33 | /// This allows you to match against bytes within the manufacturer data, 34 | /// by masking some bytes of the service data dataPrefix. 35 | /// 36 | [JsonPropertyName("mask")] 37 | public byte[]? Mask { get; set; } = null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SampleShared/Components/DeviceRequestComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Blazor.Bluetooth; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace SampleShared.Components; 6 | public partial class DeviceRequestComponent : BindableBase 7 | { 8 | [Inject] 9 | public IBluetoothNavigator BluetoothNavigator { get; set; } 10 | 11 | private bool _addFilter; 12 | public bool AddFilter 13 | { 14 | get => _addFilter; 15 | set => SetProperty(ref _addFilter, value); 16 | } 17 | 18 | [Parameter] 19 | public ObservableCollection Logs { get; set; } 20 | 21 | [Parameter] 22 | public EventHandler OnDeviceReceived { get; set; } 23 | 24 | private bool _isBusy; 25 | public bool IsBusy 26 | { 27 | get => _isBusy; 28 | set => SetProperty(ref _isBusy, value); 29 | } 30 | 31 | public RequestDeviceOptions Options = new RequestDeviceOptions(); 32 | 33 | public async Task RequestDevice() 34 | { 35 | IsBusy = true; 36 | 37 | try 38 | { 39 | var isBleAvailable = await BluetoothNavigator.GetAvailability(); 40 | if (!isBleAvailable) 41 | { 42 | Logs.Add("The BLE is not available on your browser"); 43 | } 44 | 45 | var device = await BluetoothNavigator.RequestDevice(Options); 46 | 47 | OnDeviceReceived.Invoke(this, device); 48 | } 49 | catch (Exception ex) 50 | { 51 | Logs.Add($"Exception: {ex.Message}"); 52 | } 53 | 54 | IsBusy = false; 55 | } 56 | } -------------------------------------------------------------------------------- /Blazor.Bluetooth/IDevice.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Blazor.Bluetooth 5 | { 6 | public interface IDevice 7 | { 8 | /// 9 | /// Gets a string that uniquely identifies a device. 10 | /// 11 | string Id { get; } 12 | 13 | /// 14 | /// Gets a string that provices a human-readable name for the device. 15 | /// 16 | string Name { get; } 17 | 18 | /// 19 | /// Gets a reference to the device's BluetoothRemoteGATTServer. 20 | /// 21 | IBluetoothRemoteGATTServer Gatt { get; } 22 | 23 | /// 24 | /// On GATT server disconnected event represent disconnection from the device. 25 | /// 26 | event Action OnGattServerDisconnected; 27 | 28 | /// 29 | /// On advertisement received event. 30 | /// 31 | event Action OnAdvertisementReceived; 32 | 33 | /// 34 | /// Activate receiving advertisements from the device. 35 | /// Advertisements will stop receiving once you connect to the device. 36 | /// 37 | /// Will throw in case of Experimental mode inactive. 38 | Task WatchAdvertisements(); 39 | 40 | /// 41 | /// Provides a way for the page to revoke access to a device the user has granted access to. 42 | /// 43 | Task Forget(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowDescriptorComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Blazor.Bluetooth; 3 | using System.Collections.ObjectModel; 4 | using System.Reflection.PortableExecutable; 5 | 6 | namespace SampleShared.Components; 7 | public partial class ShowDescriptorComponent : BindableBase 8 | { 9 | [Parameter] 10 | public IBluetoothRemoteGATTDescriptor Descriptor { get; set; } 11 | 12 | [Parameter] 13 | public ObservableCollection Logs { get; set; } 14 | 15 | private byte[] _readWrite; 16 | public byte[] ValueWrite 17 | { 18 | get => _readWrite; 19 | set => SetProperty(ref _readWrite, value); 20 | } 21 | 22 | private bool _isBusy; 23 | public bool IsBusy 24 | { 25 | get => _isBusy; 26 | set => SetProperty(ref _isBusy, value); 27 | } 28 | 29 | private string _readRead; 30 | public string ValueRead 31 | { 32 | get => _readRead; 33 | set => SetProperty(ref _readRead, value); 34 | } 35 | 36 | private async Task ReadValue() 37 | { 38 | try 39 | { 40 | var value = await Descriptor.ReadValue(); 41 | ValueRead = string.Join(" ", value); 42 | 43 | } 44 | catch (Exception e) 45 | { 46 | Logs.Add(e.Message); 47 | } 48 | } 49 | 50 | private async Task WriteValue() 51 | { 52 | try 53 | { 54 | await Descriptor.WriteValue(ValueWrite); 55 | 56 | } 57 | catch (Exception e) 58 | { 59 | Logs.Add(e.Message); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /SampleShared/Components/AddFilterComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 | 4 |
5 | 6 | @if (Filters != null) 7 | { 8 | @foreach (var filter in Filters) 9 | { 10 | var thisFilter = filter; 11 |
12 | 13 | 14 |
15 | 19 |
20 | 21 |
22 | 26 |
27 | 28 | 29 | 30 |
31 |

Services

32 | 33 |
34 | 35 | 36 | 37 |
38 |

ManufacturerData

39 | 40 |
41 | 42 |
43 | } 44 | } 45 |
-------------------------------------------------------------------------------- /SampleClientSide/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /SampleShared/Components/DeviceRequestComponent.razor: -------------------------------------------------------------------------------- 1 | @using Blazor.Bluetooth 2 | @inherits BindableBase 3 | 4 |
5 | 6 |
7 | 10 | 11 |
12 | 13 | @if (AddFilter) 14 | { 15 |
16 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 |
27 |

Filters

28 | 29 |
30 | 31 |
32 | 33 | 34 | 35 |
36 |

Exclusive Filters

37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 |
45 |

Optional Services

46 | 47 |
48 | 49 |
50 | 51 | 52 | 53 |
54 |

Optional Manufacturer data

55 | 56 |
57 | } 58 | 59 | 60 |
-------------------------------------------------------------------------------- /SampleShared/Components/BluetoothUuidsComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Blazor.Bluetooth; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace SampleShared.Components; 6 | public partial class BluetoothUuidsComponent : BindableBase 7 | { 8 | [Inject] 9 | public IBluetoothNavigator BluetoothNavigator { get; set; } 10 | 11 | private string _name; 12 | public string Name 13 | { 14 | get => _name; 15 | set => SetProperty(ref _name, value); 16 | } 17 | 18 | private string _UUID; 19 | public string UUID 20 | { 21 | get => _UUID; 22 | set => SetProperty(ref _UUID, value); 23 | } 24 | 25 | [Parameter] 26 | public ObservableCollection Logs { get; set; } 27 | 28 | public async Task SetGetService() 29 | { 30 | try 31 | { 32 | UUID = await BluetoothUUID.GetService(Name); 33 | } 34 | catch (Exception ex) 35 | { 36 | Logs.Add(ex.Message); 37 | } 38 | } 39 | 40 | public async Task SetGetCharacteristic() 41 | { 42 | try 43 | { 44 | UUID = await BluetoothUUID.GetCharacteristic(Name); 45 | } 46 | catch (Exception ex) 47 | { 48 | Logs.Add(ex.Message); 49 | } 50 | } 51 | 52 | public async Task SetGetDescriptor() 53 | { 54 | try 55 | { 56 | UUID = await BluetoothUUID.GetDescriptor(Name); 57 | } 58 | catch (Exception ex) 59 | { 60 | Logs.Add(ex.Message); 61 | } 62 | } 63 | 64 | public async Task SetGetCanonical() 65 | { 66 | try 67 | { 68 | UUID = await BluetoothUUID.GetCanonicalUUID(Name); 69 | } 70 | catch (Exception ex) 71 | { 72 | Logs.Add(ex.Message); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /SampleShared/Components/DeviceComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Blazor.Bluetooth; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace SampleShared.Components; 6 | public partial class DeviceComponent : BindableBase 7 | { 8 | private bool _isBusy; 9 | public bool IsBusy 10 | { 11 | get => _isBusy; 12 | set => SetProperty(ref _isBusy, value); 13 | } 14 | 15 | [Parameter] 16 | public IDevice Device { get; set; } 17 | 18 | [Parameter] 19 | public ObservableCollection Logs { get; set; } 20 | 21 | public async Task ConnectDevice() 22 | { 23 | IsBusy = true; 24 | 25 | try 26 | { 27 | await Device.Gatt.Connect(); 28 | 29 | StateHasChanged(); 30 | } 31 | catch (System.Exception ex) 32 | { 33 | Logs.Add($"Exception: {ex.Message}"); 34 | } 35 | 36 | IsBusy = false; 37 | } 38 | 39 | public async Task DisconnectDevice() 40 | { 41 | IsBusy = true; 42 | 43 | try 44 | { 45 | await Device.Gatt.Disconnect(); 46 | StateHasChanged(); 47 | } 48 | catch (System.Exception ex) 49 | { 50 | Logs.Add($"Exception: {ex.Message}"); 51 | } 52 | 53 | if (!await Device.Gatt.GetConnected()) 54 | { 55 | Logs.Add("Info: " + Device.Name + " disconnected" ); 56 | } 57 | 58 | IsBusy = false; 59 | } 60 | 61 | public async Task UpdateIsConnected() 62 | { 63 | IsBusy = true; 64 | 65 | try 66 | { 67 | await Device.Gatt.GetConnected(); 68 | StateHasChanged(); 69 | } 70 | catch (System.Exception ex) 71 | { 72 | Logs.Add($"Exception: {ex.Message}"); 73 | } 74 | 75 | IsBusy = false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothRemoteGATTDescriptor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Blazor.Bluetooth 4 | { 5 | /// 6 | /// Represents a GATT Descriptor, which provides further information about a characteristic’s value. 7 | /// 8 | public interface IBluetoothRemoteGATTDescriptor 9 | { 10 | /// 11 | /// Gets a string that uniquely identifies a device. 12 | /// 13 | string DeviceUuid { get; } 14 | 15 | /// 16 | /// Gets a string representing the UUID of this service. 17 | /// 18 | string ServiceUuid { get; } 19 | 20 | /// 21 | /// Gets a string containing the UUID of the characteristic, for example '00002a37-0000-1000-8000-00805f9b34fb' for the Heart Rate Measurement characteristic. 22 | /// 23 | string CharacteristicUuid { get; } 24 | 25 | /// 26 | /// Gets the UUID of the characteristic descriptor, for example '00002902-0000-1000-8000-00805f9b34fb' for theClient Characteristic Configuration descriptor. 27 | /// 28 | string Uuid { get; } 29 | 30 | /// 31 | /// Gets the currently cached descriptor value. This value gets updated when the value of the descriptor is read. 32 | /// 33 | byte[] Value { get; } 34 | 35 | /// 36 | /// Read the value property if it is available and supported. Otherwise it throws an error. 37 | /// 38 | /// Task with bytes array. 39 | Task ReadValue(); 40 | 41 | /// 42 | /// Sets the value into descriptor. 43 | /// 44 | /// Value. 45 | /// Task. 46 | Task WriteValue(byte[] value); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SampleShared/Components/InputTextToByteArrayComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace SampleShared.Components; 4 | public partial class InputTextToByteArrayComponent 5 | { 6 | public bool IsTextValid { get; set; } = true; 7 | 8 | private string _stringValue; 9 | 10 | public string StringValue 11 | { 12 | get => _stringValue; 13 | set 14 | { 15 | if (_stringValue != value) 16 | { 17 | _stringValue = value; 18 | Value = TryParseStringValue(); 19 | ValueChanged.InvokeAsync(Value); 20 | StateHasChanged(); 21 | } 22 | } 23 | } 24 | 25 | [Parameter] 26 | public byte[] Value { get; set; } 27 | 28 | [Parameter] 29 | public EventCallback ValueChanged { get; set; } 30 | 31 | private byte[] TryParseStringValue() 32 | { 33 | if (string.IsNullOrEmpty(StringValue)) 34 | { 35 | IsTextValid = true; 36 | return Array.Empty(); 37 | } 38 | 39 | try 40 | { 41 | var bytes = StringValue.Trim().Split(" "); 42 | var value = new byte[bytes.Length]; 43 | 44 | for (int i = 0; i < bytes.Length; i++) 45 | { 46 | 47 | if (byte.TryParse(bytes[i], out var valueByte)) 48 | { 49 | value[i] = valueByte; 50 | } 51 | else 52 | { 53 | IsTextValid = false; 54 | return Array.Empty(); 55 | } 56 | } 57 | 58 | IsTextValid = true; 59 | return value; 60 | } 61 | catch (Exception e) 62 | { 63 | IsTextValid = false; 64 | } 65 | 66 | IsTextValid = false; 67 | return Array.Empty(); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /SampleShared/Components/AdvComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 |
4 | @if (AdvertisementsFeatureDissabled) 5 | { 6 |

Please make sure to enable "Web BLE new permissions backend" feature for bluetooth to have advertisements!

7 |

(Copy ref to search field)

8 |

about:flags/#enable-web-bluetooth-new-permissions-backend

9 | } 10 | 11 |
12 | @if (!AdvertisementsReceiveActivated) 13 | { 14 | 15 | } 16 | else 17 | { 18 |

Advertisements scanning....

19 | } 20 |
21 | 22 | @if (BluetoothAdvertisingEvent != null) 23 | { 24 |

Advertisement:

25 |

Apperience: @BluetoothAdvertisingEvent.Appearance

26 |

Name: @BluetoothAdvertisingEvent.Name

27 |

Rssi: @BluetoothAdvertisingEvent.Rssi

28 |

Tx_power: @BluetoothAdvertisingEvent.TxPower

29 |

ManufacturerData:

30 | @if (BluetoothAdvertisingEvent.ManufacturerData != null) 31 | { 32 | @foreach (var item in BluetoothAdvertisingEvent.ManufacturerData) 33 | { 34 |

- Key: @item.Key, Value: @item.Value

35 | } 36 | } 37 |

ServiceData:

38 | @if (BluetoothAdvertisingEvent.ServiceData != null) 39 | { 40 | @foreach (var item in BluetoothAdvertisingEvent.ServiceData) 41 | { 42 |

- Key: @item.Key, Value: @item.Value

43 | } 44 | } 45 |

UUIDs:

46 | @if (BluetoothAdvertisingEvent.Uuids != null) 47 | { 48 | @foreach (var item in BluetoothAdvertisingEvent.Uuids) 49 | { 50 |

- item

51 | } 52 | } 53 | } 54 |
-------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothRemoteGATTDescriptor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Blazor.Bluetooth 7 | { 8 | internal class BluetoothRemoteGATTDescriptor : IBluetoothRemoteGATTDescriptor 9 | { 10 | #region Internal fields 11 | 12 | public string InternalDeviceUuid { get; set; } 13 | public string InternalServiceUuid { get; set; } 14 | public string InternalCharacteristicUuid { get; set; } 15 | public string InternalUuid { get; set; } 16 | public byte[] InternalValue { get; set; } 17 | 18 | #endregion 19 | 20 | #region Public fields 21 | 22 | public string DeviceUuid => InternalDeviceUuid; 23 | public string ServiceUuid => InternalServiceUuid; 24 | public string CharacteristicUuid => InternalCharacteristicUuid; 25 | public string Uuid => InternalUuid; 26 | public byte[] Value => InternalValue; 27 | 28 | #endregion 29 | 30 | #region Public methods 31 | 32 | public async Task ReadValue() 33 | { 34 | try 35 | { 36 | var value = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.descriptorReadValue", DeviceUuid, ServiceUuid, CharacteristicUuid, Uuid); 37 | return value.Select(v => (byte)(v & 0xFF)).ToArray(); 38 | } 39 | catch (JSException ex) 40 | { 41 | throw new Exception(ex.Message); 42 | } 43 | } 44 | 45 | public async Task WriteValue(byte[] value) 46 | { 47 | var bytes = value.Select(v => (uint)v).ToArray(); 48 | 49 | try 50 | { 51 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.descriptorWriteValue", DeviceUuid, ServiceUuid, CharacteristicUuid, Uuid, bytes); 52 | } 53 | catch (JSException ex) 54 | { 55 | throw new Exception(ex.Message); 56 | } 57 | } 58 | 59 | #endregion 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothCharacteristicProperties.cs: -------------------------------------------------------------------------------- 1 | namespace Blazor.Bluetooth 2 | { 3 | /// 4 | /// Provides the operations that are valid on the given BluetoothRemoteGATTCharacteristic. 5 | /// 6 | public interface IBluetoothCharacteristicProperties 7 | { 8 | /// 9 | /// Gets a boolean that is true if signed writing to the characteristic value is permitted. 10 | /// 11 | bool AuthenticatedSignedWrites { get; } 12 | 13 | /// 14 | /// Return a boolean that is true if the broadcast of the characteristic value is permitted using the Server Characteristic Configuration Descriptor. 15 | /// 16 | bool Broadcast { get; } 17 | 18 | /// 19 | /// Return a boolean that is true if indications of the characteristic value with acknowledgement is permitted. 20 | /// 21 | bool Indicate { get; } 22 | 23 | /// 24 | /// Return a boolean that is true if notifications of the characteristic value without acknowledgement is permitted. 25 | /// 26 | bool Notify { get; } 27 | 28 | /// 29 | /// Return a boolean that is true if the reading of the characteristic value is permitted. 30 | /// 31 | bool Read { get; } 32 | 33 | /// 34 | /// Return a boolean that is true if reliable writes to the characteristic is permitted. 35 | /// 36 | bool ReliableWrite { get; } 37 | 38 | /// 39 | /// Return a boolean that is true if reliable writes to the characteristic descriptor is permitted. 40 | /// 41 | bool WritableAuxiliaries { get; } 42 | 43 | /// 44 | /// Return a boolean that is true if the writing to the characteristic with response is permitted. 45 | /// 46 | bool Write { get; } 47 | 48 | /// 49 | /// Return a boolean that is true if the writing to the characteristic without response is permitted. 50 | /// 51 | bool WriteWithoutResponse { get; } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SampleShared/Components/AdvComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Blazor.Bluetooth; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace SampleShared.Components; 6 | public partial class AdvComponent : BindableBase 7 | { 8 | private bool _advertisementsReceiveActivated; 9 | public bool AdvertisementsReceiveActivated 10 | { 11 | get => _advertisementsReceiveActivated; 12 | set => SetProperty(ref _advertisementsReceiveActivated, value); 13 | } 14 | 15 | private bool _advertisementsFeatureDissabled; 16 | public bool AdvertisementsFeatureDissabled 17 | { 18 | get => _advertisementsFeatureDissabled; 19 | set => SetProperty(ref _advertisementsFeatureDissabled, value); 20 | } 21 | 22 | private IBluetoothAdvertisingEvent _bluetoothAdvertisingEvent; 23 | public IBluetoothAdvertisingEvent BluetoothAdvertisingEvent 24 | { 25 | get => _bluetoothAdvertisingEvent; 26 | set => SetProperty(ref _bluetoothAdvertisingEvent, value); 27 | } 28 | 29 | private IDevice _device; 30 | [Parameter] 31 | public IDevice Device 32 | { 33 | get => _device; 34 | set 35 | { 36 | if (_device != value) 37 | { 38 | _advertisementsReceiveActivated = false; 39 | _advertisementsFeatureDissabled = false; 40 | _device = value; 41 | } 42 | } 43 | } 44 | 45 | [Parameter] 46 | public ObservableCollection Logs { get; set; } 47 | 48 | public async Task StartReceivingAdvertisements() 49 | { 50 | try 51 | { 52 | Device.OnAdvertisementReceived += Device_OnAdvertisementReceived; 53 | await Device.WatchAdvertisements(); 54 | AdvertisementsReceiveActivated = true; 55 | } 56 | catch (AdvertisementsUnavailableException ex) 57 | { 58 | AdvertisementsFeatureDissabled = true; 59 | Logs.Add($"AdvertisementsUnavailableException: {ex.Message}"); 60 | } 61 | } 62 | 63 | private void Device_OnAdvertisementReceived(IBluetoothAdvertisingEvent bluetoothAdvertisingEvent) 64 | { 65 | Logs.Add("Add received at " + DateTime.Now.ToString("hh:mm:ss")); 66 | BluetoothAdvertisingEvent = bluetoothAdvertisingEvent; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowConnectedDeviceComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using Blazor.Bluetooth; 2 | using Microsoft.AspNetCore.Components; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace SampleShared.Components; 6 | public partial class ShowConnectedDeviceComponent : BindableBase 7 | { 8 | private string _serviceUUID; 9 | public string ServiceUUID 10 | { 11 | get => _serviceUUID; 12 | set => SetProperty(ref _serviceUUID, value); 13 | } 14 | 15 | private bool _isBusy; 16 | public bool IsBusy 17 | { 18 | get => _isBusy; 19 | set => SetProperty(ref _isBusy, value); 20 | } 21 | 22 | public ObservableCollection Services { get; set; } = new(); 23 | 24 | [Parameter] 25 | public IDevice Device { get; set; } 26 | 27 | [Parameter] 28 | public ObservableCollection Logs { get; set; } 29 | 30 | public async Task OnGetServiceByUUIDClicked() 31 | { 32 | if (string.IsNullOrEmpty(ServiceUUID)) 33 | { 34 | return; 35 | } 36 | 37 | try 38 | { 39 | var service = await Device.Gatt.GetPrimaryService(ServiceUUID); 40 | var existing = Services.FirstOrDefault(x => x.Uuid == ServiceUUID); 41 | if (existing != null) 42 | { 43 | Services.Remove(existing); 44 | } 45 | 46 | Services.Add(service); 47 | } 48 | catch (Exception e) 49 | { 50 | Logs.Add(e.Message); 51 | } 52 | } 53 | 54 | public async Task OnGetServicesByUUIDClicked() 55 | { 56 | if (string.IsNullOrEmpty(ServiceUUID)) 57 | { 58 | return; 59 | } 60 | 61 | try 62 | { 63 | var services = await Device.Gatt.GetPrimaryServices(ServiceUUID); 64 | var existing = Services.Where(x => x.Uuid == ServiceUUID); 65 | if (existing != null) 66 | { 67 | foreach (var item in existing) 68 | { 69 | Services.Remove(item); 70 | } 71 | } 72 | 73 | foreach (var service in services) 74 | { 75 | Services.Add(service); 76 | } 77 | } 78 | catch (Exception e) 79 | { 80 | Logs.Add(e.Message); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothRemoteGATTService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace Blazor.Bluetooth 5 | { 6 | /// 7 | /// Represents a service provided by a GATT server, including a device, a list of referenced services, and a list of the characteristics of this service. 8 | /// 9 | public interface IBluetoothRemoteGATTService 10 | { 11 | /// 12 | /// Gets a string that uniquely identifies a device. 13 | /// 14 | string DeviceUuid { get; } 15 | 16 | /// 17 | /// Gets a string representing the UUID of this service. 18 | /// 19 | string Uuid { get; } 20 | 21 | /// 22 | /// Gets a boolean value indicating whether this is a primary or secondary service. 23 | /// 24 | bool IsPrimary { get; } 25 | 26 | /// 27 | /// Returns an instance of for a given universally unique identifier (UUID). 28 | /// 29 | /// The UUID of a characteristic, for example '00002a37-0000-1000-8000-00805f9b34fb' for the Heart Rate Measurement characteristic. 30 | /// Task with result. 31 | Task GetCharacteristic(string uuid); 32 | 33 | /// 34 | /// Returns a list of BluetoothRemoteGATTCharacteristic instances for a given universally unique identifier (UUID). 35 | /// 36 | /// The UUID of a characteristic, for example '00002a37-0000-1000-8000-00805f9b34fb' for the Heart Rate Measurement characteristic. 37 | /// Task with list of result. 38 | Task> GetCharacteristics(string uuid); 39 | 40 | /// 41 | /// Returns a list of BluetoothRemoteGATTCharacteristic instances for a given universally unique identifier (UUID). 42 | /// 43 | /// Task with list of result. 44 | Task> GetCharacteristics(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothRemoteGATTServer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace Blazor.Bluetooth 5 | { 6 | /// 7 | /// Represents a GATT Server on a remote device. 8 | /// 9 | public interface IBluetoothRemoteGATTServer 10 | { 11 | /// 12 | /// Gets a string that uniquely identifies a device. 13 | /// 14 | string DeviceUuid { get; } 15 | 16 | /// 17 | /// Gets a boolean value that returns true while this script execution environment is connected to this.device. It can be false while the user agent is physically connected. 18 | /// 19 | bool Connected { get; } 20 | 21 | /// 22 | /// Causes the script execution environment to connect to device. 23 | /// 24 | /// Task. 25 | Task Connect(); 26 | 27 | /// 28 | /// Causes the script execution environment to disconnect from device. 29 | /// 30 | /// Task. 31 | Task Disconnect(); 32 | 33 | /// 34 | /// Returns the primary offered by the bluetooth device for a specified BluetoothServiceUUID. 35 | /// 36 | /// A Bluetooth service universally unique identifier for a specified device. 37 | /// Task with result. 38 | Task GetPrimaryService(string uuid); 39 | 40 | /// 41 | /// Returns a list of primary objects offered by the bluetooth device for a specified BluetoothServiceUUID. 42 | /// 43 | /// A Bluetooth service universally unique identifier for a specified device. 44 | /// Task with list of result. 45 | Task> GetPrimaryServices(string uuid); 46 | 47 | /// 48 | /// Get current device and check Connected property, made to check the state in runtime, because Connected property is actually setting by Connect/Disconnect/GetConnected only. 49 | /// 50 | /// Task with is connected result. 51 | Task GetConnected(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /SampleShared/Components/BleTesterPageComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |

15 | 16 | 17 |

18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 | @if (Devices.Any()) 30 | { 31 | @foreach (var device in Devices) 32 | { 33 |
34 | 35 | 36 |
37 | } 38 | } 39 | else 40 | { 41 |

No devices got yet

42 | } 43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 |

Logs:

51 | @if (Logs != null && Logs.Any()) 52 | { 53 |
54 | @for (var i = Logs.Count; i > 0; i--) 55 | { 56 | @if (i == Logs.Count) 57 | { 58 |
59 |

@Logs[i - 1]

60 |
61 | 62 | } 63 | else 64 | { 65 |

@Logs[i - 1]

66 | } 67 | } 68 |
69 | } 70 |
71 |
72 |
-------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothRemoteGATTService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blazor.Bluetooth 8 | { 9 | internal class BluetoothRemoteGATTService : IBluetoothRemoteGATTService 10 | { 11 | #region Internal fields 12 | 13 | public string InternalDeviceUuid { get; set; } 14 | public string InternalUuid { get; set; } 15 | public bool InternalIsPrimary { get; set; } 16 | 17 | #endregion 18 | 19 | #region Public fields 20 | 21 | public string DeviceUuid => InternalDeviceUuid; 22 | public string Uuid => InternalUuid; 23 | public bool IsPrimary => InternalIsPrimary; 24 | 25 | #endregion 26 | 27 | #region Public methods 28 | 29 | public async Task GetCharacteristic(string uuid) 30 | { 31 | try 32 | { 33 | var characteristic = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.getCharacteristic", Uuid, uuid, DeviceUuid); 34 | return characteristic; 35 | } 36 | catch (JSException ex) 37 | { 38 | throw new Exception(ex.Message); 39 | } 40 | } 41 | 42 | public async Task> GetCharacteristics(string uuid) 43 | { 44 | try 45 | { 46 | var characteristics = await BluetoothNavigator.JsRuntime.InvokeAsync>("ble.getCharacteristics", Uuid, uuid, DeviceUuid); 47 | return characteristics.Select(x => (IBluetoothRemoteGATTCharacteristic)x).ToList(); 48 | } 49 | catch (JSException ex) 50 | { 51 | throw new Exception(ex.Message); 52 | } 53 | } 54 | 55 | public async Task> GetCharacteristics() 56 | { 57 | try 58 | { 59 | var characteristics = await BluetoothNavigator.JsRuntime.InvokeAsync>("ble.getCharacteristicsWithoutUUID", Uuid, DeviceUuid); 60 | return characteristics.Select(x => (IBluetoothRemoteGATTCharacteristic)x).ToList(); 61 | } 62 | catch (JSException ex) 63 | { 64 | throw new Exception(ex.Message); 65 | } 66 | } 67 | 68 | #endregion 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/RequestDeviceOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using System.Collections.Generic; 4 | 5 | namespace Blazor.Bluetooth 6 | { 7 | /// 8 | /// Request device options for . 9 | /// 10 | [Serializable] 11 | public class RequestDeviceOptions 12 | { 13 | /// 14 | /// Gets or sets a filters. 15 | /// Optional. 16 | /// 17 | [JsonPropertyName("filters")] 18 | public List? Filters { get; set; } = null; 19 | 20 | /// 21 | /// An array of filter objects indicating the characteristics of devices that will be excluded from matching. 22 | /// The properties of the array elements are the same as for filters. 23 | /// Optional. 24 | /// 25 | [JsonPropertyName("exclusionFilters")] 26 | public List? ExclusionFilters { get; set; } = null; 27 | 28 | /// 29 | /// Gets or sets a list of BluetoothServiceUUIDs 30 | /// An array of optional service identifiers. 31 | /// Optional. 32 | /// 33 | /// The identifiers take the same values as the elements of the services array (a GATT service name, service UUID, or UUID short 16-bit or 32-bit form). 34 | /// 35 | [JsonPropertyName("optionalServices")] 36 | public List? OptionalServices { get; set; } = null; 37 | 38 | /// 39 | /// Gets or sets a Manufacturer data. 40 | /// An array of objects matching against manufacturer data in the Bluetooth Low Energy (BLE) advertising packets. 41 | /// Optional. 42 | /// 43 | [JsonPropertyName("optionalManufacturerData")] 44 | public List? OptionalManufacturerData { get; set; } = null; 45 | 46 | /// 47 | /// Gets or sets a value indicates a boolean value indicating that the requesting script can accept all Bluetooth devices. The default is null. 48 | /// Optional. 49 | /// 50 | /// This option is appropriate when devices have not advertised enough information for filtering to be useful. 51 | /// When acceptAllDevices is set to true you should omit all filters and exclusionFilters, 52 | /// and you must set optionalServices to be able to use the returned device. 53 | /// 54 | [JsonPropertyName("acceptAllDevices")] 55 | public bool? AcceptAllDevices { get; set; } = null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/Blazor.Bluetooth.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0;net6.0;net8.0 5 | Valerii Sovytskyi 6 | Blazor Web Bluetooth API for server and client. 7 | MIT 8 | https://github.com/valerii-sovytskyi/Blazor.Bluetooth 9 | Blazor.Bluetooth, Blazor, Bluetooth, Web, API, BluetoothWeb, BluetoothAPI, BluetoothBlazor, BlazorBluetooth 10 | 11 | 1. Added Forgot function for Bluetooth Device. 12 | 2. Fixed Advertisements manufacturer data, service data, tx power properties. 13 | 3. Upgraded advertisenets handlers to handle advertisent per device, not we can handle multiple advertisements for multiple device. 14 | 4. Added BluetoothUUID to get UUID for service/characteristic/descriptor/canonical. 15 | 5. Add GetCharacteristics without UUID parameter for Service. 16 | 6. Add StartNotifications to handle for many characteristics. 17 | 18 | 1.0.6.2 19 | BlazorBluetoothUA.png 20 | 21 | README.md 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | True 48 | 49 | 50 | 51 | True 52 | \ 53 | 54 | 55 | True 56 | \ 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /SampleClientSide/wwwroot/service-worker.published.js: -------------------------------------------------------------------------------- 1 | // Caution! Be sure you understand the caveats before publishing an application with 2 | // offline support. See https://aka.ms/blazor-offline-considerations 3 | 4 | self.importScripts('./service-worker-assets.js'); 5 | self.addEventListener('install', event => event.waitUntil(onInstall(event))); 6 | self.addEventListener('activate', event => event.waitUntil(onActivate(event))); 7 | self.addEventListener('fetch', event => event.respondWith(onFetch(event))); 8 | 9 | const cacheNamePrefix = 'offline-cache-'; 10 | const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; 11 | const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; 12 | const offlineAssetsExclude = [ /^service-worker\.js$/ ]; 13 | 14 | // Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'. 15 | const base = "/"; 16 | const baseUrl = new URL(base, self.origin); 17 | const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href); 18 | 19 | async function onInstall(event) { 20 | console.info('Service worker: Install'); 21 | 22 | // Fetch and cache all matching items from the assets manifest 23 | const assetsRequests = self.assetsManifest.assets 24 | .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) 25 | .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) 26 | .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); 27 | await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); 28 | } 29 | 30 | async function onActivate(event) { 31 | console.info('Service worker: Activate'); 32 | 33 | // Delete unused caches 34 | const cacheKeys = await caches.keys(); 35 | await Promise.all(cacheKeys 36 | .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) 37 | .map(key => caches.delete(key))); 38 | } 39 | 40 | async function onFetch(event) { 41 | let cachedResponse = null; 42 | if (event.request.method === 'GET') { 43 | // For all navigation requests, try to serve index.html from cache, 44 | // unless that request is for an offline resource. 45 | // If you need some URLs to be server-rendered, edit the following check to exclude those URLs 46 | const shouldServeIndexHtml = event.request.mode === 'navigate' 47 | && !manifestUrlList.some(url => url === event.request.url); 48 | 49 | const request = shouldServeIndexHtml ? 'index.html' : event.request; 50 | const cache = await caches.open(cacheName); 51 | cachedResponse = await cache.match(request); 52 | } 53 | 54 | return cachedResponse || fetch(event.request); 55 | } 56 | -------------------------------------------------------------------------------- /Blazor.Bluetooth.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32825.248 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blazor.Bluetooth", "Blazor.Bluetooth\Blazor.Bluetooth.csproj", "{8F3C849C-B968-4707-9386-18C7C47821C6}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleClientSide", "SampleClientSide\SampleClientSide.csproj", "{56850B1A-420E-4C27-91AB-927E6B963B6C}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9186B306-C357-4957-B55C-2AA0E26D2F7D}" 11 | ProjectSection(SolutionItems) = preProject 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleServerSide", "SampleServerSide\SampleServerSide.csproj", "{61DBC4A6-4E33-49D1-8DCA-D03445485C03}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleShared", "SampleShared\SampleShared.csproj", "{0A78AF10-D006-4E05-A59A-421F39695C2F}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {8F3C849C-B968-4707-9386-18C7C47821C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {8F3C849C-B968-4707-9386-18C7C47821C6}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {8F3C849C-B968-4707-9386-18C7C47821C6}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {8F3C849C-B968-4707-9386-18C7C47821C6}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {56850B1A-420E-4C27-91AB-927E6B963B6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {56850B1A-420E-4C27-91AB-927E6B963B6C}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {56850B1A-420E-4C27-91AB-927E6B963B6C}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {56850B1A-420E-4C27-91AB-927E6B963B6C}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {61DBC4A6-4E33-49D1-8DCA-D03445485C03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {61DBC4A6-4E33-49D1-8DCA-D03445485C03}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {61DBC4A6-4E33-49D1-8DCA-D03445485C03}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {61DBC4A6-4E33-49D1-8DCA-D03445485C03}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {0A78AF10-D006-4E05-A59A-421F39695C2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {0A78AF10-D006-4E05-A59A-421F39695C2F}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {0A78AF10-D006-4E05-A59A-421F39695C2F}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {0A78AF10-D006-4E05-A59A-421F39695C2F}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {EF4A2350-F375-4EAD-8FBF-AF9EC65EBBEE} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | h1:focus { 8 | outline: none; 9 | } 10 | 11 | a, .btn-link { 12 | color: #0071c1; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .content { 22 | padding-top: 1.1rem; 23 | } 24 | 25 | .valid.modified:not([type=checkbox]) { 26 | outline: 1px solid #26b050; 27 | } 28 | 29 | .invalid { 30 | outline: 1px solid red; 31 | } 32 | 33 | .validation-message { 34 | color: red; 35 | } 36 | 37 | #blazor-error-ui { 38 | background: lightyellow; 39 | bottom: 0; 40 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 41 | display: none; 42 | left: 0; 43 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 44 | position: fixed; 45 | width: 100%; 46 | z-index: 1000; 47 | } 48 | 49 | #blazor-error-ui .dismiss { 50 | cursor: pointer; 51 | position: absolute; 52 | right: 0.75rem; 53 | top: 0.5rem; 54 | } 55 | 56 | .blazor-error-boundary { 57 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 58 | padding: 1rem 1rem 1rem 3.7rem; 59 | color: white; 60 | } 61 | 62 | .blazor-error-boundary::after { 63 | content: "An error has occurred." 64 | } 65 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothRemoteGATTServer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blazor.Bluetooth 8 | { 9 | internal class BluetoothRemoteGATTServer : IBluetoothRemoteGATTServer 10 | { 11 | #region Internal fields 12 | 13 | public string InternalDeviceUuid { get; set; } 14 | public bool InternalConnected { get; set; } 15 | 16 | #endregion 17 | 18 | #region Public fields 19 | 20 | public string DeviceUuid => InternalDeviceUuid; 21 | public bool Connected => InternalConnected; 22 | 23 | #endregion 24 | 25 | #region Public methods 26 | 27 | public async Task Connect() 28 | { 29 | try 30 | { 31 | var device = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.connectDevice", InternalDeviceUuid); 32 | InternalConnected = device.Gatt.Connected; 33 | } 34 | catch (JSException ex) 35 | { 36 | throw new Exception(ex.Message); 37 | } 38 | } 39 | 40 | public async Task Disconnect() 41 | { 42 | try 43 | { 44 | var device = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.disconnectDevice", InternalDeviceUuid); 45 | InternalConnected = device.Gatt.Connected; 46 | } 47 | catch (JSException ex) 48 | { 49 | throw new Exception(ex.Message); 50 | } 51 | } 52 | 53 | public async Task GetPrimaryService(string uuid) 54 | { 55 | try 56 | { 57 | var primaryService = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.getPrimaryService", uuid, DeviceUuid); 58 | return primaryService; 59 | } 60 | catch (JSException ex) 61 | { 62 | throw new Exception(ex.Message); 63 | } 64 | } 65 | 66 | public async Task> GetPrimaryServices(string uuid) 67 | { 68 | try 69 | { 70 | var primaryServices = await BluetoothNavigator.JsRuntime.InvokeAsync>("ble.getPrimaryServices", uuid, DeviceUuid); 71 | return primaryServices.Select(x => (IBluetoothRemoteGATTService)x).ToList(); 72 | } 73 | catch (JSException ex) 74 | { 75 | throw new Exception(ex.Message); 76 | } 77 | } 78 | 79 | public async Task GetConnected() 80 | { 81 | try 82 | { 83 | var device = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.getDeviceById", DeviceUuid); 84 | InternalConnected = device.Gatt.Connected; 85 | return InternalConnected; 86 | } 87 | catch (JSException ex) 88 | { 89 | throw new Exception(ex.Message); 90 | } 91 | } 92 | 93 | #endregion 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowServiceComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Blazor.Bluetooth; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace SampleShared.Components; 6 | public partial class ShowServiceComponent : BindableBase 7 | { 8 | private bool _isBusy; 9 | public bool IsBusy 10 | { 11 | get => _isBusy; 12 | set => SetProperty(ref _isBusy, value); 13 | } 14 | 15 | private string _characteristicUUID; 16 | public string CharacteristicUUID 17 | { 18 | get => _characteristicUUID; 19 | set => SetProperty(ref _characteristicUUID, value); 20 | } 21 | 22 | public ObservableCollection Characteristics { get; set; } = new(); 23 | 24 | [Parameter] 25 | public IBluetoothRemoteGATTService Service { get; set; } 26 | 27 | [Parameter] 28 | public ObservableCollection Logs { get; set; } 29 | 30 | public async Task OnGetCharacteristicByUUIDClicked() 31 | { 32 | Characteristics.Clear(); 33 | 34 | try 35 | { 36 | var characteristic = await Service.GetCharacteristic(CharacteristicUUID); 37 | 38 | var existing = Characteristics.FirstOrDefault(x => x.Uuid == CharacteristicUUID); 39 | if (existing != null) 40 | { 41 | Characteristics.Remove(existing); 42 | } 43 | 44 | Characteristics.Add(characteristic); 45 | } 46 | catch (Exception e) 47 | { 48 | Logs.Add(e.Message); 49 | } 50 | } 51 | 52 | public async Task OnGetCharacteristicsByUUIDClicked() 53 | { 54 | Characteristics.Clear(); 55 | 56 | try 57 | { 58 | var characteristics = await Service.GetCharacteristics(CharacteristicUUID); 59 | 60 | var existing = Characteristics.Where(x => x.Uuid == CharacteristicUUID); 61 | if (existing != null) 62 | { 63 | foreach (var item in existing) 64 | { 65 | Characteristics.Remove(item); 66 | } 67 | } 68 | 69 | foreach (var characteristic in characteristics) 70 | { 71 | Characteristics.Add(characteristic); 72 | } 73 | } 74 | catch (Exception e) 75 | { 76 | Logs.Add(e.Message); 77 | } 78 | } 79 | 80 | public async Task OnGetCharacteristicsClicked() 81 | { 82 | 83 | Characteristics.Clear(); 84 | 85 | try 86 | { 87 | var characteristics = await Service.GetCharacteristics(); 88 | 89 | var existing = Characteristics.Where(x => x.Uuid == CharacteristicUUID); 90 | if (existing != null) 91 | { 92 | foreach (var item in existing) 93 | { 94 | Characteristics.Remove(item); 95 | } 96 | } 97 | 98 | foreach (var characteristic in characteristics) 99 | { 100 | Characteristics.Add(characteristic); 101 | } 102 | } 103 | catch (Exception e) 104 | { 105 | Logs.Add(e.Message); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /SampleShared/Components/BleTesterPageComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Blazor.Bluetooth; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace SampleShared.Components; 6 | public partial class BleTesterPageComponent : BindableBase 7 | { 8 | [Inject] 9 | public IBluetoothNavigator BluetoothNavigator { get; set; } 10 | 11 | private bool _isBusy; 12 | public bool IsBusy 13 | { 14 | get => _isBusy; 15 | set => SetProperty(ref _isBusy, value); 16 | } 17 | 18 | public ObservableCollection Logs { get; set; } = new ObservableCollection(); 19 | 20 | public ObservableCollection Devices { get; set; } = 21 | new ObservableCollection(); 22 | 23 | public BleTesterPageComponent() 24 | { 25 | Logs.CollectionChanged += (o, e) => StateHasChanged(); 26 | } 27 | 28 | public async Task RemoveDevice(IDevice device) 29 | { 30 | if (device.Gatt.Connected) 31 | { 32 | await device.Gatt.Disconnect(); 33 | } 34 | 35 | try 36 | { 37 | await device.Forget(); 38 | 39 | Devices.Remove(device); 40 | StateHasChanged(); 41 | } 42 | catch (Exception e) 43 | { 44 | Logs.Add(e.ToString()); 45 | } 46 | } 47 | 48 | public async Task OnGetDeviceClicked() 49 | { 50 | try 51 | { 52 | var devices = await BluetoothNavigator.GetDevices(); 53 | if (devices != null) 54 | { 55 | Logs.Add($"Just got {devices.Count} devices"); 56 | 57 | foreach (var device in devices) 58 | { 59 | var existingDevice = Devices.FirstOrDefault(x => x.Id == device.Id); 60 | if (existingDevice != null) 61 | { 62 | Devices.Remove(existingDevice); 63 | } 64 | 65 | Devices.Add(device); 66 | StateHasChanged(); 67 | } 68 | } 69 | } 70 | catch (System.Exception ex) 71 | { 72 | Logs.Add($"Exception: {ex.Message}"); 73 | } 74 | } 75 | 76 | private void OnDeviceReceived(object sender, IDevice? device) 77 | { 78 | if (device is null) 79 | { 80 | return; 81 | } 82 | 83 | var existingDevice = Devices.FirstOrDefault(x => x.Id == device.Id); 84 | if (existingDevice != null) 85 | { 86 | Devices.Remove(existingDevice); 87 | } 88 | 89 | Devices.Add(device); 90 | StateHasChanged(); 91 | } 92 | 93 | public async Task OnGetAvailabilityClicked() 94 | { 95 | try 96 | { 97 | BluetoothNavigator.OnAvailabilityChanged -= BluetoothNavigator_OnAvailabilityChanged; 98 | BluetoothNavigator.OnAvailabilityChanged += BluetoothNavigator_OnAvailabilityChanged; 99 | var isAvailable = await BluetoothNavigator.GetAvailability(); 100 | var txt = isAvailable ? "Bluetooth adapter available" : "Bluetooth adapter is not available"; 101 | Logs.Add(txt); 102 | } 103 | catch (System.Exception ex) 104 | { 105 | Logs.Add($"Exception: {ex.Message}"); 106 | } 107 | } 108 | 109 | private void BluetoothNavigator_OnAvailabilityChanged() 110 | { 111 | Logs.Add($"BluetoothNavigator_OnAvailabilityChanged called"); 112 | } 113 | } -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothUUID.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.JSInterop; 4 | 5 | namespace Blazor.Bluetooth 6 | { 7 | /// 8 | /// The BluetoothUUID interface of the Web Bluetooth API provides a way to look up Universally Unique Identifier (UUID) values by name in the registry maintained by the Bluetooth SIG. 9 | /// 10 | /// 11 | public static class BluetoothUUID 12 | { 13 | /// 14 | /// Get UUID representing a registered service when passed a name or the 16- or 32-bit UUID alias. 15 | /// 16 | /// A string containing the name of the service. 17 | /// A 128-bit UUID. 18 | /// Thrown if name does not appear in the registry. 19 | public static async Task GetService(string name) 20 | { 21 | try 22 | { 23 | var service = 24 | await BluetoothNavigator.JsRuntime.InvokeAsync("ble.bluetoothUUIDGetService", name); 25 | return service; 26 | } 27 | catch (JSException ex) 28 | { 29 | throw new Exception(ex.Message); 30 | } 31 | } 32 | 33 | /// 34 | /// Get UUID representing a registered characteristic when passed a name or the 16- or 32-bit UUID alias. 35 | /// 36 | /// A string containing the name of the characteristic. 37 | /// A 128-bit UUID. 38 | /// Thrown if name does not appear in the registry. 39 | public static async Task GetCharacteristic(string name) 40 | { 41 | try 42 | { 43 | var characteristic = 44 | await BluetoothNavigator.JsRuntime.InvokeAsync("ble.bluetoothUUIDGetCharacteristic", name); 45 | return characteristic; 46 | } 47 | catch (JSException ex) 48 | { 49 | throw new Exception(ex.Message); 50 | } 51 | } 52 | 53 | /// 54 | /// Get UUID representing a registered descriptor when passed a name or the 16- or 32-bit UUID alias. 55 | /// 56 | /// A string containing the name of the descriptor. 57 | /// A 128-bit UUID. 58 | /// Thrown if name does not appear in the registry. 59 | public static async Task GetDescriptor(string name) 60 | { 61 | try 62 | { 63 | var descriptor = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.bluetoothUUIDGetDescriptor", name); 64 | return descriptor; 65 | } 66 | catch (JSException ex) 67 | { 68 | throw new Exception(ex.Message); 69 | } 70 | } 71 | 72 | /// 73 | /// Get UUID representing a registered canonical when passed a name or the 16- or 32-bit UUID alias. 74 | /// 75 | /// A string containing a 16-bit or 32-bit UUID alias. 76 | /// A 128-bit UUID. 77 | /// Thrown if name does not appear in the registry. 78 | public static async Task GetCanonicalUUID(string alias) 79 | { 80 | try 81 | { 82 | var canonical = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.bluetoothUUIDCanonicalUUID", alias); 83 | return canonical; 84 | } 85 | catch (JSException ex) 86 | { 87 | throw new Exception(ex.Message); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /SampleClientSide/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | h1:focus { 6 | outline: none; 7 | } 8 | 9 | a, .btn-link { 10 | color: #0071c1; 11 | } 12 | 13 | .btn-primary { 14 | color: #fff; 15 | background-color: #1b6ec2; 16 | border-color: #1861ac; 17 | } 18 | 19 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 20 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 21 | } 22 | 23 | .content { 24 | padding-top: 1.1rem; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid red; 33 | } 34 | 35 | .validation-message { 36 | color: red; 37 | } 38 | 39 | #blazor-error-ui { 40 | background: lightyellow; 41 | bottom: 0; 42 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 43 | display: none; 44 | left: 0; 45 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 46 | position: fixed; 47 | width: 100%; 48 | z-index: 1000; 49 | } 50 | 51 | #blazor-error-ui .dismiss { 52 | cursor: pointer; 53 | position: absolute; 54 | right: 0.75rem; 55 | top: 0.5rem; 56 | } 57 | 58 | .blazor-error-boundary { 59 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 60 | padding: 1rem 1rem 1rem 3.7rem; 61 | color: white; 62 | } 63 | 64 | .blazor-error-boundary::after { 65 | content: "An error has occurred." 66 | } 67 | 68 | .loading-progress { 69 | position: relative; 70 | display: block; 71 | width: 8rem; 72 | height: 8rem; 73 | margin: 20vh auto 1rem auto; 74 | } 75 | 76 | .loading-progress circle { 77 | fill: none; 78 | stroke: #e0e0e0; 79 | stroke-width: 0.6rem; 80 | transform-origin: 50% 50%; 81 | transform: rotate(-90deg); 82 | } 83 | 84 | .loading-progress circle:last-child { 85 | stroke: #1b6ec2; 86 | stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; 87 | transition: stroke-dasharray 0.05s ease-in-out; 88 | } 89 | 90 | .loading-progress-text { 91 | position: absolute; 92 | text-align: center; 93 | font-weight: bold; 94 | inset: calc(20vh + 3.25rem) 0 auto 0.2rem; 95 | } 96 | 97 | .loading-progress-text:after { 98 | content: var(--blazor-load-percentage-text, "Loading"); 99 | } 100 | 101 | code { 102 | color: #c02d76; 103 | } 104 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowCharacteristicComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits BindableBase 2 | 3 | 4 |
5 |
6 |
7 | 8 |
9 |

Properties:

10 |

Value: @Characteristic.Value

11 |

DeviceUuid: @Characteristic.DeviceUuid

12 |

ServiceUuid: @Characteristic.ServiceUuid

13 |

Uuid: @Characteristic.Uuid

14 |

AuthenticatedSignedWrites: @Characteristic.Properties.AuthenticatedSignedWrites

15 |

Broadcast: @Characteristic.Properties.Broadcast

16 |

Indicate: @Characteristic.Properties.Indicate

17 |

Read: @Characteristic.Properties.Read

18 |

ReliableWrite: @Characteristic.Properties.ReliableWrite

19 |

WritableAuxiliaries: @Characteristic.Properties.WritableAuxiliaries

20 |

Write: @Characteristic.Properties.Write

21 |

WriteWithoutResponse: @Characteristic.Properties.WriteWithoutResponse

22 |

Notify: @Characteristic.Properties.Notify

23 |
24 |
25 |
26 |

Methods:

27 | 28 | @if (Characteristic.Properties.Read) 29 | { 30 |

Value: @ValueRead

31 | 32 | } 33 | 34 | @if (Characteristic.Properties.Write) 35 | { 36 | 40 |
41 | 42 |
43 | 44 | @if (Characteristic.Properties.WriteWithoutResponse) 45 | { 46 | 47 |
48 | } 49 | 50 | 51 |

(p.s. Do not use it as it's obsolete, use WriteWithoutResponse instead)

52 |
53 | } 54 | 55 | @if (Characteristic.Properties.Notify) 56 | { 57 | @if (IsNotificationStarted) 58 | { 59 |

Notification value: @NotificationValue

60 | 61 | } 62 | else 63 | { 64 | 65 | } 66 | } 67 |
68 | 69 | 73 | 74 | 75 | @if (Descriptors.Any()) 76 | { 77 | @foreach (var descriptor in Descriptors) 78 | { 79 | 80 | } 81 | } 82 | else 83 | { 84 |

No desciptors here.

85 | } 86 |
87 |
88 |
89 |
-------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothNavigator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Blazor.Bluetooth 6 | { 7 | /// 8 | /// The Web Bluetooth API provides the ability to connect and interact with Bluetooth Low Energy peripherals. 9 | /// 10 | public interface IBluetoothNavigator 11 | { 12 | /// 13 | /// An event handler that runs when an event of type has fired. 14 | /// 15 | event Action OnAvailabilityChanged; 16 | 17 | /// 18 | /// Returns a reference to the device, if any, from which the user opened the current page. For example, an Eddystone beacon might advertise a URL, which the user agent allows the user to open. A representing the beacon would be available through . 19 | /// 20 | /// Task with a device. 21 | /// JSInteropt is not found, check if you added in your wwwrooot/index.html for Client App, or in _Host.cshtml (.net5) / _Layout.cshtml (.net6) for Server App. 22 | /// Bluetooth probably is not supported on your browser. Please check browser compatibility https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility. 23 | Task ReferringDevice(); 24 | 25 | /// 26 | /// Returns a boolean value indicating whether the user-agent has the ability to support Bluetooth. Some user-agents let the user configure an option that affects what is returned by this value. If this option is set, that is the value returned by this method. 27 | /// 28 | /// Task with availability result. 29 | /// JSInteropt is not found, check if you added in your wwwrooot/index.html for Client App, or in _Host.cshtml (.net5) / _Layout.cshtml (.net6) for Server App. 30 | /// Bluetooth probably is not supported on your browser. Please check browser compatibility https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility. 31 | Task GetAvailability(); 32 | 33 | /// 34 | /// Returns a list of which the origin already obtained permission for via a call to . 35 | /// 36 | /// Task with a list of devices result. 37 | /// JSInteropt is not found, check if you added in your wwwrooot/index.html for Client App, or in _Host.cshtml (.net5) / _Layout.cshtml (.net6) for Server App. 38 | /// Bluetooth probably is not supported on your browser. Please check browser compatibility https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility. 39 | /// Exception, other not handled exceptions. 40 | Task> GetDevices(); 41 | 42 | /// 43 | /// Request from the user a device. If there is no chooser UI, this method returns the first device matching the criteria. 44 | /// 45 | /// Options. 46 | /// Task with a device result. 47 | /// JSInteropt is not found, check if you added in your wwwrooot/index.html for Client App, or in _Host.cshtml (.net5) / _Layout.cshtml (.net6) for Server App. 48 | /// Bluetooth probably is not supported on your browser. Please check browser compatibility https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth#browser_compatibility. 49 | /// Exception thrown in case user cancel connecting to the device. 50 | /// Exception, other not handled exceptions. 51 | Task RequestDevice(RequestDeviceOptions options); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/Device.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Blazor.Bluetooth 6 | { 7 | /// 8 | /// Represents a Bluetooth device inside a particular script execution environment. 9 | /// 10 | internal class Device : IDevice 11 | { 12 | #region Private fields 13 | 14 | private DotNetObjectReference DeviceDisconnectHandler; 15 | private DotNetObjectReference AdvertisementReceivedHandler; 16 | 17 | #endregion 18 | 19 | #region Internal fields 20 | 21 | public string InternalId { get; set; } 22 | 23 | public string InternalName { get; set; } 24 | 25 | public BluetoothRemoteGATTServer InternalGatt { get; set; } 26 | 27 | #endregion 28 | 29 | #region Public fields 30 | 31 | public string Id => InternalId; 32 | 33 | public string Name => InternalName; 34 | 35 | public IBluetoothRemoteGATTServer Gatt => InternalGatt; 36 | 37 | private event Action _onGattServerDisconnected; 38 | public event Action OnGattServerDisconnected 39 | { 40 | add 41 | { 42 | if (DeviceDisconnectHandler is null) 43 | { 44 | DeviceDisconnectHandler = DotNetObjectReference.Create(new DeviceDisconnectHandler(this)); 45 | } 46 | 47 | BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.addDeviceDisconnectionHandler", DeviceDisconnectHandler, Id); 48 | 49 | _onGattServerDisconnected += value; 50 | } 51 | remove 52 | { 53 | BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.addDeviceDisconnectionHandler", null, Id); 54 | 55 | DeviceDisconnectHandler?.Dispose(); 56 | DeviceDisconnectHandler = null; 57 | _onGattServerDisconnected -= value; 58 | } 59 | } 60 | 61 | private event Action _onAdvertisementReceived; 62 | public event Action OnAdvertisementReceived 63 | { 64 | add 65 | { 66 | if (AdvertisementReceivedHandler is null) 67 | { 68 | AdvertisementReceivedHandler = DotNetObjectReference.Create(new AdvertisementReceivedHandler(this)); 69 | } 70 | 71 | BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.setAdvertisementReceivedHandler", AdvertisementReceivedHandler, Id); 72 | 73 | _onAdvertisementReceived += value; 74 | } 75 | remove 76 | { 77 | BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.setAdvertisementReceivedHandler", null, Id); 78 | 79 | AdvertisementReceivedHandler?.Dispose(); 80 | AdvertisementReceivedHandler = null; 81 | _onAdvertisementReceived -= value; 82 | } 83 | } 84 | 85 | #endregion 86 | 87 | #region Public methods 88 | 89 | public async Task WatchAdvertisements() 90 | { 91 | try 92 | { 93 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.watchAdvertisements", InternalId); 94 | } 95 | catch (JSException ex) 96 | { 97 | if (ex.Message.Contains("is not a function")) 98 | { 99 | throw new AdvertisementsUnavailableException(ex); 100 | } 101 | 102 | throw new Exception(ex.Message); 103 | } 104 | } 105 | 106 | public async Task Forget() 107 | { 108 | try 109 | { 110 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.forget", InternalId); 111 | } 112 | catch (JSException ex) 113 | { 114 | if (ex.Message.Contains("is not a function")) 115 | { 116 | throw new AdvertisementsUnavailableException(ex); 117 | } 118 | 119 | throw new Exception(ex.Message); 120 | } 121 | } 122 | 123 | #endregion 124 | 125 | #region Internal methods 126 | 127 | internal void RaiseOnGattServerDisconnected() 128 | { 129 | _onGattServerDisconnected?.Invoke(); 130 | } 131 | 132 | internal void RaiseAdvertisementReceived(BluetoothAdvertisingEvent bluetoothAdvertisingEvent) 133 | { 134 | _onAdvertisementReceived?.Invoke(bluetoothAdvertisingEvent); 135 | } 136 | 137 | #endregion 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/IBluetoothRemoteGATTCharacteristic.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Blazor.Bluetooth 6 | { 7 | /// 8 | /// Represents a GATT Characteristic, which is a basic data element that provides further information about a peripheral’s service. 9 | /// 10 | public interface IBluetoothRemoteGATTCharacteristic 11 | { 12 | /// 13 | /// Gets a string containing the UUID of the characteristic, for example '00002a37-0000-1000-8000-00805f9b34fb' for the Heart Rate Measurement characteristic. 14 | /// 15 | string Uuid { get; } 16 | 17 | /// 18 | /// Gets a string that uniquely identifies a device. 19 | /// 20 | string DeviceUuid { get; } 21 | 22 | /// 23 | /// Gets a string representing the UUID of this service. 24 | /// 25 | string ServiceUuid { get; } 26 | 27 | /// 28 | /// Gets the properties of this characteristic. 29 | /// 30 | IBluetoothCharacteristicProperties Properties { get; } 31 | 32 | /// 33 | /// Gets the currently cached characteristic value. This value gets updated when the value of the characteristic is read or updated via a notification or indication. 34 | /// 35 | byte[] Value { get; } 36 | 37 | /// 38 | /// On raise characteristic value changed event. 39 | /// 40 | event EventHandler OnRaiseCharacteristicValueChanged; 41 | 42 | /// 43 | /// Returns the first for a given descriptor UUID. 44 | /// 45 | /// A Bluetooth descriptor universally unique identifier for a specified characteristic. 46 | /// Task with a result. 47 | Task GetDescriptor(string uuid); 48 | 49 | /// 50 | /// Returns a list of all objects for a given descriptor UUID. 51 | /// 52 | /// A Bluetooth descriptor universally unique identifier for a specified characteristic. 53 | /// Task with a list of result. 54 | Task> GetDescriptors(string uuid); 55 | 56 | /// 57 | /// Returns a bytes holding a duplicate of the value property if it is available and supported. Otherwise it throws an error. 58 | /// 59 | /// Task with a bytes result. 60 | Task ReadValue(); 61 | 62 | /// 63 | /// Write a bytes value to the characteristic. 64 | /// 65 | /// Value. 66 | /// Task. 67 | [Obsolete("This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time. Use IBluetoothRemoteGATTCharacteristic.WriteValueWithResponse and IBluetoothRemoteGATTCharacteristic.WriteValueWithoutResponse instead.")] 68 | Task WriteValue(byte[] value); 69 | 70 | /// 71 | /// Write a bytes value to the characteristic with response. 72 | /// 73 | /// Value. 74 | /// Task. 75 | Task WriteValueWithResponse(byte[] value); 76 | 77 | /// 78 | /// Write a bytes value to the characteristic without response. 79 | /// 80 | /// Value. 81 | /// Task. 82 | Task WriteValueWithoutResponse(byte[] value); 83 | 84 | /// 85 | /// Start notifications with subscribe user to get notifications raised in . 86 | /// 87 | /// Task. 88 | Task StartNotifications(); 89 | 90 | /// 91 | /// Stop notifications with subscribe user to get notifications raised in . 92 | /// 93 | /// Task. 94 | Task StopNotifications(); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /SampleShared/Components/ShowCharacteristicComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using Blazor.Bluetooth; 2 | using Microsoft.AspNetCore.Components; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace SampleShared.Components; 6 | public partial class ShowCharacteristicComponent : BindableBase 7 | { 8 | public ObservableCollection Descriptors { get; set; } = new(); 9 | 10 | private string _descriptorUUID; 11 | public string DescriptorUUID 12 | { 13 | get => _descriptorUUID; 14 | set => SetProperty(ref _descriptorUUID, value); 15 | } 16 | 17 | private string _readRead; 18 | public string ValueRead 19 | { 20 | get => _readRead; 21 | set => SetProperty(ref _readRead, value); 22 | } 23 | 24 | private byte[] _readWrite; 25 | public byte[] ValueWrite 26 | { 27 | get => _readWrite; 28 | set => SetProperty(ref _readWrite, value); 29 | } 30 | 31 | private bool _isBusy; 32 | public bool IsBusy 33 | { 34 | get => _isBusy; 35 | set => SetProperty(ref _isBusy, value); 36 | } 37 | 38 | private bool _isNotificationStarted; 39 | public bool IsNotificationStarted 40 | { 41 | get => _isNotificationStarted; 42 | set => SetProperty(ref _isNotificationStarted, value); 43 | } 44 | 45 | private string _notificationValue; 46 | public string NotificationValue 47 | { 48 | get => _notificationValue; 49 | set => SetProperty(ref _notificationValue, value); 50 | } 51 | 52 | [Parameter] 53 | public IBluetoothRemoteGATTCharacteristic Characteristic { get; set; } 54 | 55 | [Parameter] 56 | public ObservableCollection Logs { get; set; } 57 | 58 | public async Task OnGetDescriptorByUUIDClicked() 59 | { 60 | if (string.IsNullOrEmpty(DescriptorUUID)) 61 | { 62 | return; 63 | } 64 | 65 | Descriptors.Clear(); 66 | 67 | try 68 | { 69 | var descriptor = await Characteristic.GetDescriptor(DescriptorUUID); 70 | var existing = Descriptors.FirstOrDefault(x => x.Uuid == DescriptorUUID); 71 | if (existing != null) 72 | { 73 | Descriptors.Remove(existing); 74 | } 75 | 76 | Descriptors.Add(descriptor); 77 | } 78 | catch (Exception e) 79 | { 80 | Logs.Add(e.Message); 81 | } 82 | } 83 | 84 | public async Task OnGetDescriptorsByUUIDClicked() 85 | { 86 | if (string.IsNullOrEmpty(DescriptorUUID)) 87 | { 88 | return; 89 | } 90 | 91 | Descriptors.Clear(); 92 | 93 | try 94 | { 95 | var descriptors = await Characteristic.GetDescriptors(DescriptorUUID); 96 | var existing = Descriptors.Where(x => x.Uuid == DescriptorUUID); 97 | if (existing != null) 98 | { 99 | foreach (var item in existing) 100 | { 101 | Descriptors.Remove(item); 102 | } 103 | } 104 | 105 | foreach (var descriptor in descriptors) 106 | { 107 | Descriptors.Add(descriptor); 108 | } 109 | } 110 | catch (Exception e) 111 | { 112 | Logs.Add(e.Message); 113 | } 114 | } 115 | 116 | public async Task StartNotification() 117 | { 118 | try 119 | { 120 | await Characteristic.StartNotifications(); 121 | Characteristic.OnRaiseCharacteristicValueChanged += CharacteristicOnOnRaiseCharacteristicValueChanged; 122 | IsNotificationStarted = true; 123 | } 124 | catch (Exception e) 125 | { 126 | Logs.Add(e.Message); 127 | } 128 | } 129 | 130 | public async Task StopNotification() 131 | { 132 | try 133 | { 134 | await Characteristic.StopNotifications(); 135 | Characteristic.OnRaiseCharacteristicValueChanged -= CharacteristicOnOnRaiseCharacteristicValueChanged; 136 | IsNotificationStarted = false; 137 | } 138 | catch (Exception e) 139 | { 140 | Logs.Add(e.Message); 141 | } 142 | } 143 | 144 | private void CharacteristicOnOnRaiseCharacteristicValueChanged(object? sender, CharacteristicEventArgs e) 145 | { 146 | var value = string.Join(" ", e.Value); 147 | NotificationValue = value; 148 | Console.WriteLine(value); 149 | } 150 | 151 | private async Task ReadValue() 152 | { 153 | try 154 | { 155 | var value = await Characteristic.ReadValue(); 156 | ValueRead = string.Join(" ", value); 157 | 158 | } 159 | catch (Exception e) 160 | { 161 | Logs.Add(e.Message); 162 | } 163 | } 164 | 165 | private async Task WriteValue() 166 | { 167 | try 168 | { 169 | await Characteristic.WriteValue(ValueWrite); 170 | 171 | } 172 | catch (Exception e) 173 | { 174 | Logs.Add(e.Message); 175 | } 176 | } 177 | 178 | private async Task WriteValueWithoutResponse() 179 | { 180 | try 181 | { 182 | await Characteristic.WriteValueWithoutResponse(ValueWrite); 183 | } 184 | catch (Exception e) 185 | { 186 | Logs.Add(e.Message); 187 | } 188 | } 189 | 190 | private async Task WriteValueWithResponse() 191 | { 192 | try 193 | { 194 | await Characteristic.WriteValueWithResponse(ValueWrite); 195 | } 196 | catch (Exception e) 197 | { 198 | Logs.Add(e.Message); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothNavigator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Text.Json; 7 | 8 | namespace Blazor.Bluetooth 9 | { 10 | internal class BluetoothNavigator : IBluetoothNavigator 11 | { 12 | #region Private fields 13 | 14 | private DotNetObjectReference BluetoothAvailabilityHandler; 15 | 16 | #endregion 17 | 18 | #region Internal fields 19 | 20 | /// 21 | /// TODO: find out the way to not use static. 22 | /// 23 | internal static IJSRuntime JsRuntime { get; private set; } 24 | 25 | #endregion 26 | 27 | #region Public fields 28 | 29 | private event Action _onAvailabilityChanged; 30 | 31 | public event Action OnAvailabilityChanged 32 | { 33 | add 34 | { 35 | if (BluetoothAvailabilityHandler is null) 36 | { 37 | BluetoothAvailabilityHandler = DotNetObjectReference.Create(new BluetoothAvailabilityHandler(this)); 38 | } 39 | 40 | JsRuntime.InvokeVoidAsync("ble.addBluetoothAvailabilityHandler", BluetoothAvailabilityHandler); 41 | 42 | _onAvailabilityChanged += value; 43 | } 44 | remove 45 | { 46 | JsRuntime.InvokeVoidAsync("ble.addBluetoothAvailabilityHandler", null); 47 | 48 | BluetoothAvailabilityHandler?.Dispose(); 49 | BluetoothAvailabilityHandler = null; 50 | _onAvailabilityChanged -= value; 51 | } 52 | } 53 | 54 | #endregion 55 | 56 | #region Constructor 57 | 58 | public BluetoothNavigator(IJSRuntime jsRuntime) 59 | { 60 | JsRuntime = jsRuntime; 61 | } 62 | 63 | #endregion 64 | 65 | #region Public methods 66 | 67 | public async Task ReferringDevice() 68 | { 69 | try 70 | { 71 | var device = await JsRuntime.InvokeAsync("ble.referringDevice"); 72 | return device; 73 | } 74 | catch (JSException ex) 75 | { 76 | if (ex.Message.Contains("'ble' was undefined")) 77 | { 78 | throw new ScriptNotFoundException(ex); 79 | } 80 | 81 | if (ex.Message.Contains("navigator.bluetooth is undefined")) 82 | { 83 | throw new BluetoothNotSupportedException(ex); 84 | } 85 | 86 | throw new Exception(ex.Message); 87 | } 88 | } 89 | 90 | public async Task GetAvailability() 91 | { 92 | try 93 | { 94 | var isBleAvailable = await JsRuntime.InvokeAsync("ble.getAvailability"); 95 | return isBleAvailable; 96 | } 97 | catch (JSException ex) 98 | { 99 | if (ex.Message.Contains("'ble' was undefined")) 100 | { 101 | throw new ScriptNotFoundException(ex); 102 | } 103 | 104 | if (ex.Message.Contains("navigator.bluetooth is undefined")) 105 | { 106 | throw new BluetoothNotSupportedException(ex); 107 | } 108 | 109 | throw new Exception(ex.Message); 110 | } 111 | } 112 | 113 | public async Task> GetDevices() 114 | { 115 | try 116 | { 117 | var devices = await JsRuntime.InvokeAsync("ble.getDevices"); 118 | return devices.Select(x => (IDevice)x).ToList(); 119 | } 120 | catch (JSException ex) 121 | { 122 | if (ex.Message.Contains("'ble' was undefined")) 123 | { 124 | throw new ScriptNotFoundException(ex); 125 | } 126 | 127 | if (ex.Message.Contains("navigator.bluetooth is undefined")) 128 | { 129 | throw new BluetoothNotSupportedException(ex); 130 | } 131 | 132 | throw new Exception(ex.Message); 133 | } 134 | } 135 | 136 | public async Task RequestDevice(RequestDeviceOptions options) 137 | { 138 | var jsonOptions = new JsonSerializerOptions 139 | { 140 | DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull 141 | }; 142 | 143 | var json = JsonSerializer.Serialize(options, jsonOptions); 144 | 145 | try 146 | { 147 | var device = await JsRuntime.InvokeAsync("ble.requestDevice", json); 148 | return device; 149 | } 150 | catch (JSException ex) 151 | { 152 | if (ex.Message.Contains("'ble' was undefined")) 153 | { 154 | throw new ScriptNotFoundException(ex); 155 | } 156 | 157 | if (ex.Message.Contains("navigator.bluetooth is undefined")) 158 | { 159 | throw new BluetoothNotSupportedException(ex); 160 | } 161 | 162 | if (ex.Message.Contains("User cancelled the requestDevice() chooser.")) 163 | { 164 | throw new RequestDeviceCancelledException(ex.Message, ex); 165 | } 166 | 167 | throw new Exception(ex.Message); 168 | } 169 | } 170 | 171 | #endregion 172 | 173 | #region Internal methods 174 | 175 | internal void RaiseOnAvailabilityChanged() 176 | { 177 | _onAvailabilityChanged?.Invoke(); 178 | } 179 | 180 | #endregion 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Blazor.Bluetooth/BluetoothRemoteGATTCharacteristic.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Blazor.Bluetooth 8 | { 9 | internal class BluetoothRemoteGATTCharacteristic : IBluetoothRemoteGATTCharacteristic 10 | { 11 | #region Private fields 12 | 13 | private DotNetObjectReference _characteristicValueHandler; 14 | private event EventHandler _onRaiseCharacteristicValueChanged; 15 | 16 | #endregion 17 | 18 | #region Internal fields 19 | 20 | public string InternalDeviceUuid { get; set; } 21 | public string InternalServiceUuid { get; set; } 22 | public string InternalUuid { get; set; } 23 | public BluetoothCharacteristicProperties InternalProperties { get; set; } 24 | public byte[] InternalValue { get; set; } 25 | 26 | #endregion 27 | 28 | #region Public fields 29 | 30 | public string DeviceUuid => InternalDeviceUuid; 31 | public string ServiceUuid => InternalServiceUuid; 32 | public string Uuid => InternalUuid; 33 | public IBluetoothCharacteristicProperties Properties => InternalProperties; 34 | public byte[] Value => InternalValue; 35 | 36 | public event EventHandler OnRaiseCharacteristicValueChanged 37 | { 38 | add 39 | { 40 | if (_characteristicValueHandler == null) 41 | { 42 | _characteristicValueHandler = DotNetObjectReference.Create(new CharacteristicValueHandler(this)); 43 | } 44 | 45 | BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.setCharacteristicValueChangedHandler", _characteristicValueHandler, DeviceUuid, ServiceUuid, Uuid); 46 | 47 | _onRaiseCharacteristicValueChanged += value; 48 | } 49 | remove 50 | { 51 | BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.setCharacteristicValueChangedHandler", null, DeviceUuid, ServiceUuid, Uuid); 52 | 53 | _characteristicValueHandler?.Dispose(); 54 | _characteristicValueHandler = null; 55 | _onRaiseCharacteristicValueChanged -= value; 56 | } 57 | } 58 | 59 | #endregion 60 | 61 | #region Public methods 62 | 63 | public async Task GetDescriptor(string uuid) 64 | { 65 | try 66 | { 67 | var descriptor = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.getDescriptor", uuid, ServiceUuid, Uuid, DeviceUuid); 68 | return descriptor; 69 | } 70 | catch (JSException ex) 71 | { 72 | throw new Exception(ex.Message); 73 | } 74 | } 75 | 76 | public async Task> GetDescriptors(string uuid) 77 | { 78 | try 79 | { 80 | var descriptors = await BluetoothNavigator.JsRuntime.InvokeAsync>("ble.getDescriptors", uuid, ServiceUuid, Uuid, DeviceUuid); 81 | return descriptors.Select(x => (IBluetoothRemoteGATTDescriptor)x).ToList(); 82 | } 83 | catch (JSException ex) 84 | { 85 | throw new Exception(ex.Message); 86 | } 87 | } 88 | 89 | public async Task ReadValue() 90 | { 91 | try 92 | { 93 | var value = await BluetoothNavigator.JsRuntime.InvokeAsync("ble.characteristicReadValue", DeviceUuid, ServiceUuid, Uuid); 94 | return value.Select(v => (byte)(v & 0xFF)).ToArray(); 95 | } 96 | catch (JSException ex) 97 | { 98 | throw new Exception(ex.Message); 99 | } 100 | } 101 | 102 | [Obsolete("This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.")] 103 | public async Task WriteValue(byte[] value) 104 | { 105 | var bytes = value.Select(v => (uint)v).ToArray(); 106 | 107 | try 108 | { 109 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.characteristicWriteValue", DeviceUuid, ServiceUuid, Uuid, bytes); 110 | 111 | } 112 | catch (JSException ex) 113 | { 114 | throw new Exception(ex.Message); 115 | } 116 | } 117 | 118 | public async Task WriteValueWithoutResponse(byte[] value) 119 | { 120 | var bytes = value.Select(v => (uint)v).ToArray(); 121 | 122 | try 123 | { 124 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.characteristicWriteValueWithoutResponse", DeviceUuid, ServiceUuid, Uuid, bytes); 125 | 126 | } 127 | catch (JSException ex) 128 | { 129 | throw new Exception(ex.Message); 130 | } 131 | } 132 | 133 | public async Task WriteValueWithResponse(byte[] value) 134 | { 135 | var bytes = value.Select(v => (uint)v).ToArray(); 136 | 137 | try 138 | { 139 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.characteristicWriteValueWithResponse", DeviceUuid, ServiceUuid, Uuid, bytes); 140 | } 141 | catch (JSException ex) 142 | { 143 | throw new Exception(ex.Message); 144 | } 145 | } 146 | 147 | public async Task StartNotifications() 148 | { 149 | try 150 | { 151 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.startNotification", DeviceUuid, ServiceUuid, Uuid); 152 | } 153 | catch (JSException ex) 154 | { 155 | throw new Exception(ex.Message); 156 | } 157 | } 158 | 159 | public async Task StopNotifications() 160 | { 161 | try 162 | { 163 | await BluetoothNavigator.JsRuntime.InvokeVoidAsync("ble.stopNotification", DeviceUuid, ServiceUuid, Uuid); 164 | } 165 | catch (JSException ex) 166 | { 167 | throw new Exception(ex.Message); 168 | } 169 | } 170 | 171 | #endregion 172 | 173 | #region Internal methods 174 | 175 | internal void RaiseCharacteristicValueChanged(CharacteristicEventArgs args) 176 | { 177 | _onRaiseCharacteristicValueChanged?.Invoke(this, args); 178 | } 179 | 180 | #endregion 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to use Blazor.Bluetooth 2 | 3 | Blazor.Bluetooth makes it easy to connect Blazor to your Bluetooth devices using Web Bluetooth. 4 | 5 | Works both **Client-side** and **Server-side**. 6 | 7 | ## Getting Started 8 | 9 | 1. Add Nuget package [Blazor.Bluetooth](https://www.nuget.org/packages/Blazor.Bluetooth) 10 | 2. In Program.cs add ```builder.Services.AddBluetoothNavigator();``` 11 | 3. Add JSInterop.js to the project 12 | 13 | - For the **Client**: In your **wwwrooot/index.html** add `````` 14 | - For the **Server**: In your _Host.cshtml for .**net5** and _Layout.cshtml for **.net6** add `````` 15 | 16 | 4. In the component you want to connect to a device add the Blazor.Bluetooth Namespace ```@using Blazor.Bluetooth``` or add it to your ```_Imports.razor``` 17 | 5. Inject the **IBluetoothNavigator** (the instance that will communicate with your device) ```@inject IBluetoothNavigator navigator``` 18 | 19 | You can check and run [SampleClientSide](https://github.com/valerii-sovytskyi/Blazor.Bluetooth/tree/master/SampleClientSide) to test device connection. 20 | But right now let's see what we can do here 21 | 22 | ## How to use 23 | 24 | ### Bluetooth 25 | 26 | First of all you can check if user's browser support Bluetooth connection by calling **IBluetoothNavigator.GetAvailability** 27 | 28 | ### Request a device 29 | 30 | Create a **RequestDeviceOptions** with all the options you would like to get a device. But note that it's important if **AcceptAllDevices** is **true**, then do not set **Filters**, other ways you have to fill **Filters**. 31 | 32 | ``` 33 | var options = new RequestDeviceOptions { AcceptAllDevices = true }; 34 | var device = await BluetoothNavigator.RequestDevice(options); 35 | ``` 36 | 37 | _Note: see full info about requests here [Request Device Options](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#options)_ 38 | 39 | ### Connect/Disconnect to the device 40 | 41 | Connect to the device. 42 | 43 | ```await device.Gatt.Connect();``` 44 | 45 | _Note: that for my case I want to able to connect to the device all the time, sometimes it took like 5 times to connect, so it would be nice you to implement some service to run connection for few times untill you will be connected or time out. It you failed to connect, you will get an **Exception**_ 46 | 47 | Disconnect from the device will raise **IDevice.OnGattServerDisconnected** event. 48 | 49 | ```await Device.Gatt.Disonnect();``` 50 | 51 | _Note: Sometimes to reconnect to the device you will have to wait, it dependec on a device but for my case I needed to wait up to 3 sec. ```await Task.Delay(3000);```_ 52 | 53 | ### Get characteristic to read/write and get notifications 54 | 55 | To read or write a byte array you have to get characteristic, (or descriptor it dependes). Also you can check if your characteristic support write or read. 56 | 57 | ``` 58 | var service = await device.Gatt.GetPrimaryService("Your service UUID"); 59 | var characteristic = await service.GetCharacteristic("Your characteristic UUID"); 60 | if (characteristic.Properties.Write) 61 | { 62 | characteristic.OnRaiseCharacteristicValueChanged += (sender, e) => { }; 63 | await characteristic.StartNotifications(); 64 | await characteristic.WriteValueWithResponse(/* Your byte array */); 65 | } 66 | ``` 67 | 68 | _Note: do not forget to unsubscribe from the event and call StopNotifications._ 69 | 70 | ### Others 71 | 72 | I didn't describe all the functionality, some of them I didn't have possibility to test so there could be some issues and I would like to hear from you a feedback to make this nuget fully work. 73 | 74 | ## Release notes 75 | 76 | - [1.0.2](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.2) 77 | 1. IBluetoothRemoteGATTServer has a new implementation to GetConnected 78 | 79 | _Why? It means you will check if device connected on runtime. Because the property Connect will be updated only by your actions Connect/Disconnect/GetConnected, so we need to actually check if Connected still connected to device, this method can help. Consider of using this method instead. One more think, we have a bug related to after Connect we not always connected successfully, but Connect is true, but after some time, we receive exceptions as device is disconnected. Consider to connect to device, wait some time, up to 1 sec I guess, then check if you are connected by calling Connect method. I didn't implement this functionality inside Blazor.Bluetooth, because this is only a mirror for real web bluetooth, so this is not a part of real lib._ 80 | 81 | - [1.0.3](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.3) 82 | 1. Added RequestDeviceCancelledException - by handling this exception user can just inore it, as it indicate, user clicked on cancel button. 83 | 2. Removed Newtonsoft.Json as we can use System.Text.Json 84 | 3. Filter.Services / RequestDeviceQuery.Filters / RequestDeviceQuery.OptionalServices are null by default, and removed useless methods ShouldSerialize_. 85 | 86 | - [1.0.4](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.4) 87 | 1. Fixed issue if user try to connect to one device, then disconnect, then try to connect to another device. 88 | 2. Fixed issue with Paired bluetooth devices list, it cause issue if you trying to talk to device if you reconnected to the same device. 89 | 90 | - [1.0.5](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.5) 91 | 1. Added Watch advertisements to the IDevice 92 | 2. Updated tests, also uploaded tester so you can test your device without prepearing IDE. 93 | 3. Fixed all event subscribtions to run synchronously, because it was run method faster then event was subscribet inside JSInteropt.js. 94 | 95 | - [1.0.5.5](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.5.5) 96 | 1. Fixed critical issue #1 to this library on Blazor Server App. 97 | 98 | - [1.0.5.6](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.5.6) 99 | 1. Fixed issue with connecting only for Current device instead of connecting directly by the device ID 100 | 2. Updated sample for testing bluetooth.getAvailability and bluetooth.getDevices. 101 | 102 | - [1.0.6.0](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.6.0) 103 | 104 | 1. Fixed reading Characteristic properties. 105 | 2. Add support for net8. 106 | 3. Changed RequestDeviceQuery to RequestDeviceOptions 107 | 4. Added all the options due to official docs, but not tested as have no possibility, also the question for services and identifiers, full info is here https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#options. 108 | 109 | - [1.0.6.1](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.6.1) 110 | 111 | 1. Fixed critical issue with JSInterop. 112 | 113 | - [1.0.6.2](https://www.nuget.org/packages/Blazor.Bluetooth/1.0.6.2) 114 | 115 | 1. Added Forgot function for Bluetooth Device. 116 | 2. Fixed Advertisements manufacturer data, service data, tx power properties. 117 | 3. Upgraded advertisenets handlers to handle advertisent per device, not we can handle multiple advertisements for multiple device. 118 | 4. Added BluetoothUUID to get UUID for service/characteristic/descriptor/canonical. 119 | 5. Add GetCharacteristics without UUID parameter for Service. 120 | 6. Add StartNotifications to handle for many characteristics. 121 | 122 | ## Additionaly 123 | 124 | - Some features are working only with Experimental feature activated, for example input this reference into your browser (chrome/edge) **about:flags/#enable-web-bluetooth-new-permissions-backend** and enable it. Or more general option **about:flags/#enable-experimental-web-platform-features**. 125 | - Try [Blazor.Bluetooth Web tester](https://blazorbluetooth.azurewebsites.net/), so you do not have to run the code -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | [Ss]tart/ 34 | [Ee]nd/ 35 | [Ss]nippets/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | /Reset demos.ps1 355 | /Copy Snippets.ps1 356 | -------------------------------------------------------------------------------- /SampleServerSide/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /Blazor.Bluetooth/wwwroot/JSInterop.js: -------------------------------------------------------------------------------- 1 | window.ble = {}; 2 | 3 | // BluetoothUUID 4 | 5 | window.ble.bluetoothUUIDGetService = (name) => { 6 | let result = BluetoothUUID.getService(name); 7 | return result; 8 | } 9 | 10 | window.ble.bluetoothUUIDGetCharacteristic = (name) => { 11 | let result = BluetoothUUID.getCharacteristic(name); 12 | return result; 13 | } 14 | 15 | window.ble.bluetoothUUIDGetDescriptor = (name) => { 16 | let result = BluetoothUUID.getDescriptor(name); 17 | return result; 18 | } 19 | 20 | window.ble.bluetoothUUIDCanonicalUUID = (alias) => { 21 | let result = BluetoothUUID.canonicalUUID(alias); 22 | return result; 23 | } 24 | 25 | // End BluetoothUUID 26 | 27 | // Helpers 28 | 29 | var PairedBluetoothDevices = []; 30 | 31 | function getPairedBluetoothDeviceById(deviceId) { 32 | var device = PairedBluetoothDevices.filter(function (item) { 33 | return item.id == deviceId; 34 | }); 35 | 36 | return device[0]; 37 | } 38 | 39 | async function getCharacteristic(deviceId, serviceId, characteristicId) { 40 | var device = getPairedBluetoothDeviceById(deviceId); 41 | 42 | var service = await device.gatt.getPrimaryService(serviceId); 43 | var characteristic = await service.getCharacteristic(characteristicId); 44 | return characteristic; 45 | } 46 | 47 | // End Helpers 48 | 49 | 50 | // Device 51 | 52 | function convertBluetoothAdvertisingEventToInternal(event) { 53 | return { 54 | "InternalAppearance": event.appearance, 55 | "InternalDevice": event.device, 56 | "InternalManufacturerData": event.manufacturerData, 57 | "InternalName": event.name, 58 | "InternalRssi": event.rssi, 59 | "InternalServiceData": event.serviceData, 60 | "InternalTxPower": event.txPower, 61 | "InternalUuids": event.uuids, 62 | } 63 | } 64 | 65 | var advertisementsHandlers = []; 66 | 67 | window.ble.setAdvertisementReceivedHandler = (handler, deviceId) => { 68 | 69 | var addedHandler = advertisementsHandlers.find(x => x.deviceId == deviceId); 70 | if (addedHandler != null) { 71 | 72 | // Remove previous handler for specific device. 73 | advertisementsHandlers = 74 | advertisementsHandlers.filter(item => item.deviceId != addedHandler.deviceId); 75 | } 76 | 77 | if (handler != null) { 78 | 79 | // Add new handler for specific device. 80 | advertisementsHandlers.push({ 81 | handler: handler, 82 | deviceId: deviceId 83 | }); 84 | } 85 | } 86 | 87 | window.ble.watchAdvertisements = async (deviceId) => { 88 | var device = getPairedBluetoothDeviceById(deviceId); 89 | if (!device.watchingAdvertisements) { 90 | device.addEventListener('advertisementreceived', handleAdvertisementReceived); 91 | device.watchAdvertisements(); 92 | } 93 | } 94 | 95 | async function handleAdvertisementReceived(event) { 96 | 97 | // get handler for specific device. 98 | var handler = advertisementsHandlers.find(x => x.deviceId == event.device.id); 99 | if (handler != null) { 100 | var convertedEvent = convertBluetoothAdvertisingEventToInternal(event); 101 | await handler.handler.invokeMethodAsync('HandleAdvertisementReceived', convertedEvent); 102 | } 103 | } 104 | 105 | window.ble.forget = async (deviceId) => { 106 | var device = getPairedBluetoothDeviceById(deviceId); 107 | device.forget(); 108 | } 109 | 110 | // End Device 111 | 112 | 113 | // Service 114 | 115 | function convertPrimaryServiceToInternal(service, deviceId) { 116 | return { 117 | "InternalIsPrimary": service.isPrimary, 118 | "InternalUuid": service.uuid, 119 | "InternalDeviceUuid": deviceId, 120 | } 121 | } 122 | 123 | window.ble.getPrimaryService = async (serviceId, deviceId) => { 124 | var device = getPairedBluetoothDeviceById(deviceId); 125 | var primaryService = await device.gatt.getPrimaryService(serviceId); 126 | return convertPrimaryServiceToInternal(primaryService, deviceId) 127 | } 128 | 129 | window.ble.getPrimaryServices = async (serviceId, deviceId) => { 130 | var device = getPairedBluetoothDeviceById(deviceId); 131 | var primaryServices = await device.gatt.getPrimaryServices(serviceId); 132 | return primaryServices.map(x => convertPrimaryServiceToInternal(x, deviceId)); 133 | } 134 | 135 | // End Service 136 | 137 | 138 | // Characteristic 139 | 140 | function convertCharacteristicToInternal(characteristic, deviceId, serviceId) { 141 | return { 142 | "InternalProperties": { 143 | "InternalAuthenticatedSignedWrites": characteristic.properties.authenticatedSignedWrites, 144 | "InternalBroadcast": characteristic.properties.broadcast, 145 | "InternalIndicate": characteristic.properties.indicate, 146 | "InternalNotify": characteristic.properties.notify, 147 | "InternalRead": characteristic.properties.read, 148 | "InternalReliableWrite": characteristic.properties.reliableWrite, 149 | "InternalWritableAuxiliaries": characteristic.properties.writableAuxiliaries, 150 | "InternalWrite": characteristic.properties.write, 151 | "InternalWriteWithoutResponse": characteristic.properties.writeWithoutResponse, 152 | }, 153 | "InternalUuid": characteristic.uuid, 154 | "InternalValue": characteristic.value, 155 | "InternalDeviceUuid": deviceId, 156 | "InternalServiceUuid": serviceId, 157 | } 158 | } 159 | 160 | window.ble.getCharacteristic = async (serviceId, characteristicId, deviceId) => { 161 | 162 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 163 | return convertCharacteristicToInternal(characteristic, deviceId, serviceId) 164 | } 165 | 166 | window.ble.getCharacteristics = async (serviceId, characteristicId, deviceId) => { 167 | var device = getPairedBluetoothDeviceById(deviceId); 168 | var service = await device.gatt.getPrimaryService(serviceId); 169 | var characteristics = await service.getCharacteristics(characteristicId); 170 | return characteristics.map(x => convertCharacteristicToInternal(x, deviceId, serviceId)); 171 | } 172 | 173 | window.ble.getCharacteristicsWithoutUUID = async (serviceId, deviceId) => { 174 | var device = getPairedBluetoothDeviceById(deviceId); 175 | var service = await device.gatt.getPrimaryService(serviceId); 176 | var characteristics = await service.getCharacteristics(); 177 | return characteristics.map(x => convertCharacteristicToInternal(x, deviceId, serviceId)); 178 | } 179 | 180 | // End Characteristic 181 | 182 | 183 | // Descriptors 184 | 185 | function convertDescriptorToInternal(descriptor, characteristicId, deviceId, serviceId) { 186 | return { 187 | "InternalUuid": descriptor.uuid, 188 | "InternalValue": descriptor.value, 189 | "InternalDeviceUuid": deviceId, 190 | "InternalCharacteristicUuid": characteristicId, 191 | "InternalServiceUuid": serviceId, 192 | } 193 | } 194 | 195 | window.ble.getDescriptor = async (descriptorId, serviceId, characteristicId, deviceId) => { 196 | 197 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 198 | var descriptor = await characteristic.getDescriptor(descriptorId); 199 | return convertDescriptorToInternal(descriptor, characteristicId, deviceId, serviceId); 200 | } 201 | 202 | window.ble.getDescriptors = async (descriptorId, serviceId, characteristicId, deviceId) => { 203 | 204 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 205 | var descriptors = await characteristic.getDescriptors(descriptorId); 206 | return descriptors.map(x => convertDescriptorToInternal(x, characteristicId, deviceId, serviceId)); 207 | } 208 | 209 | // End Descriptors 210 | 211 | 212 | // Characteristic Start/Stop notifications 213 | 214 | window.ble.startNotification = async (deviceId, serviceId, characteristicId) => { 215 | 216 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 217 | await characteristic.startNotifications(); 218 | characteristic.addEventListener('characteristicvaluechanged', handleCharacteristicValueChanged); 219 | } 220 | 221 | window.ble.stopNotification = async (deviceId, serviceId, characteristicId) => { 222 | 223 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 224 | await characteristic.stopNotifications(); 225 | characteristic.removeEventListener('characteristicvaluechanged', handleCharacteristicValueChanged); 226 | } 227 | 228 | // End Characteristic Start/Stop notifications 229 | 230 | 231 | // Characteristic value changed 232 | 233 | var characteristicValueHandlers = []; 234 | 235 | window.ble.setCharacteristicValueChangedHandler = (handler, deviceId, serviceId, characteristicId) => { 236 | 237 | var addedHandler = characteristicValueHandlers.find( 238 | x => x.deviceId == deviceId 239 | && x.serviceId == serviceId 240 | && x.characteristicId == characteristicId); 241 | 242 | if (addedHandler != null) { 243 | 244 | // Remove previous handler for specific device. 245 | characteristicValueHandlers = 246 | characteristicValueHandlers.filter( 247 | x => x.deviceId != addedHandler.deviceId 248 | || x.serviceId != addedHandler.serviceId 249 | || x.characteristicId != addedHandler.characteristicId); 250 | } 251 | 252 | if (handler != null) { 253 | 254 | // Add new handler for specific device. 255 | characteristicValueHandlers.push({ 256 | handler: handler, 257 | deviceId: deviceId, 258 | serviceId: serviceId, 259 | characteristicId: characteristicId 260 | }); 261 | } 262 | } 263 | 264 | async function handleCharacteristicValueChanged(event) { 265 | 266 | var target = event.target; 267 | // get handler for specific device. 268 | var handler = characteristicValueHandlers.find( 269 | x => x.deviceId == target.service.device.id 270 | && x.serviceId == target.service.uuid 271 | && x.characteristicId == target.uuid); 272 | if (handler != null) { 273 | 274 | var value = target.value; 275 | 276 | var uint8Array = new Uint8Array(value.buffer); 277 | 278 | var array = Array.from(uint8Array) 279 | await handler.handler.invokeMethodAsync('HandleCharacteristicValueChanged', event.target.service.uuid, event.target.uuid, array); 280 | } 281 | } 282 | 283 | // End Characteristic value changed 284 | 285 | 286 | // Characteristic read/write value 287 | 288 | window.ble.characteristicWriteValue = async (deviceId, serviceId, characteristicId, value) => { 289 | 290 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 291 | var b = Uint8Array.from(value); 292 | await characteristic.writeValue(b); 293 | } 294 | 295 | window.ble.characteristicWriteValueWithoutResponse = async (deviceId, serviceId, characteristicId, value) => { 296 | 297 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 298 | var b = Uint8Array.from(value); 299 | await characteristic.writeValueWithoutResponse(b); 300 | } 301 | 302 | window.ble.characteristicWriteValueWithResponse = async (deviceId, serviceId, characteristicId, value) => { 303 | 304 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 305 | var b = Uint8Array.from(value); 306 | await characteristic.writeValueWithResponse(b); 307 | } 308 | 309 | window.ble.characteristicReadValue = async (deviceId, serviceId, characteristicId) => { 310 | 311 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 312 | 313 | var value = await characteristic.readValue(); 314 | var uint8Array = new Uint8Array(value.buffer); 315 | var array = Array.from(uint8Array); 316 | return array; 317 | } 318 | 319 | // End Characteristic write value 320 | 321 | 322 | // Descriptor read/write 323 | 324 | window.ble.descriptorReadValue = async (deviceId, serviceId, characteristicId, descriptorId) => { 325 | 326 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 327 | var descriptor = await characteristic.getDescriptor(descriptorId); 328 | 329 | var value = await descriptor.readValue(); 330 | var uint8Array = new Uint8Array(value.buffer); 331 | var array = Array.from(uint8Array); 332 | return array; 333 | } 334 | 335 | window.ble.descriptorWriteValue = async (deviceId, serviceId, characteristicId, descriptorId, value) => { 336 | 337 | var characteristic = await getCharacteristic(deviceId, serviceId, characteristicId); 338 | var descriptor = await characteristic.getDescriptor(descriptorId); 339 | 340 | var b = Uint8Array.from(value); 341 | await descriptor.writeValue(b); 342 | } 343 | 344 | // End Descriptor read/write 345 | 346 | 347 | // Bluetooth 348 | 349 | function convertDeviceToInternal(device) { 350 | return { 351 | "InternalName": device.name, 352 | "InternalId": device.id, 353 | "InternalGatt": { 354 | "InternalDeviceUuid": device.id, 355 | "InternalConnected": device.gatt.connected 356 | } 357 | }; 358 | } 359 | 360 | window.ble.getDeviceById = (deviceId) => { 361 | return convertDeviceToInternal(getPairedBluetoothDeviceById(deviceId)); 362 | } 363 | 364 | window.ble.connectDevice = async (deviceId) => { 365 | var device = getPairedBluetoothDeviceById(deviceId); 366 | 367 | if (device !== null) { 368 | await device.gatt.connect(); 369 | return convertDeviceToInternal(device); 370 | } 371 | else { 372 | return null; 373 | } 374 | } 375 | 376 | window.ble.disconnectDevice = async (deviceId) => { 377 | var device = getPairedBluetoothDeviceById(deviceId); 378 | 379 | if (device !== null) { 380 | await device.gatt.disconnect(); 381 | return convertDeviceToInternal(device); 382 | } 383 | else { 384 | return null; 385 | } 386 | } 387 | 388 | window.ble.referringDevice = () => { 389 | 390 | var device = navigator.bluetooth.referringDevice; 391 | if (device === undefined) { 392 | throw 'Referring device is not supporting'; 393 | } 394 | else { 395 | return convertDeviceToInternal(device); 396 | } 397 | } 398 | 399 | window.ble.requestDevice = async (options) => { 400 | var objOptions = JSON.parse(options); 401 | var device = await navigator.bluetooth.requestDevice(objOptions); 402 | 403 | var alreadyPariedDevice = getPairedBluetoothDeviceById(device.id); 404 | if (alreadyPariedDevice != null) { 405 | var indexToRemove = PairedBluetoothDevices.findIndex(x => x.id == device.id); 406 | PairedBluetoothDevices.splice(indexToRemove, 1); 407 | } 408 | 409 | PairedBluetoothDevices.push(device); 410 | 411 | if (device !== null) { 412 | console.log('> Bluetooth Device selected.'); 413 | } 414 | 415 | return window.ble.getDeviceById(device.id); 416 | } 417 | 418 | window.ble.getAvailability = async () => { 419 | return await navigator.bluetooth.getAvailability(); 420 | } 421 | 422 | window.ble.getDevices = async () => { 423 | if (navigator.bluetooth.getDevices == undefined) { 424 | throw 'Get devices is not supporting'; 425 | } else { 426 | var devices = await navigator.bluetooth.getDevices(); 427 | 428 | devices.forEach((device) => { 429 | var alreadyPariedDevice = getPairedBluetoothDeviceById(device.id); 430 | if (alreadyPariedDevice != null) { 431 | var indexToRemove = PairedBluetoothDevices.findIndex(x => x.id == device.id); 432 | PairedBluetoothDevices.splice(indexToRemove, 1); 433 | } 434 | 435 | PairedBluetoothDevices.push(device); 436 | }); 437 | 438 | return devices.map(x => convertDeviceToInternal(x)); 439 | } 440 | } 441 | 442 | // End Bluetooth 443 | 444 | 445 | // On disconnected from device 446 | 447 | var DeviceDisconnectionHandler = []; 448 | 449 | async function onDisconnected() { 450 | 451 | console.log('> Bluetooth Device disconnected.'); 452 | 453 | await DeviceDisconnectionHandler.invokeMethodAsync('HandleDeviceDisconnected'); 454 | } 455 | 456 | window.ble.addDeviceDisconnectionHandler = (deviceDisconnectHandler, deviceUuid) => { 457 | var device = getPairedBluetoothDeviceById(deviceUuid); 458 | 459 | if (deviceDisconnectHandler !== null) { 460 | 461 | DeviceDisconnectionHandler = deviceDisconnectHandler; 462 | device.addEventListener('gattserverdisconnected', onDisconnected); 463 | } 464 | else if (DeviceDisconnectionHandler !== null && DeviceDisconnectionHandler.length > 0) { 465 | 466 | device.removeEventListener('gattserverdisconnected', onDisconnected); 467 | DeviceDisconnectionHandler = null; 468 | } 469 | } 470 | 471 | // End On disconnected from device 472 | 473 | 474 | // On availability changed from bluetooth 475 | 476 | var BluetoothAvailabilityHandler = []; 477 | 478 | async function onAvailabilityChanged() { 479 | 480 | await BluetoothAvailabilityHandler.invokeMethodAsync('HandleAvailabilityChanged'); 481 | } 482 | 483 | window.ble.addBluetoothAvailabilityHandler = (bluetoothAvailabilityHandler) => { 484 | 485 | if (bluetoothAvailabilityHandler !== null) { 486 | 487 | BluetoothAvailabilityHandler = bluetoothAvailabilityHandler; 488 | navigator.bluetooth.addEventListener('onavailabilitychanged', onDisconnected); 489 | } 490 | else if (BluetoothAvailabilityHandler !== null && BluetoothAvailabilityHandler.length > 0) { 491 | 492 | navigator.bluetooth.removeEventListener('onavailabilitychanged', onDisconnected); 493 | BluetoothAvailabilityHandler = null; 494 | } 495 | } 496 | 497 | // End On availability changed from bluetooth --------------------------------------------------------------------------------