- A+
所属分类:.NET技术
1. 建立工程 bh003_ble
2. 添加 nuget 包
<PackageReference Include="BlazorHybrid.Maui.Permissions" Version="0.0.2" /> <PackageReference Include="BootstrapBlazor" Version="7.*" /> <PackageReference Include="Densen.Extensions.BootstrapBlazor" Version="7.*" />
BlazorHybrid.Maui.Permissions 因为源码比较长,主要是一些检查和申请权限,BLE权限相关代码,就不占用篇幅列出,感兴趣的同学直接打开源码参考
顺便打开可空 <Nullable>enable</Nullable>
3. 添加蓝牙权限
安卓
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <!--蓝牙--> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <!-- csproj文件指定SupportedOSPlatformVersion android 28.0 可以继续使用安卓9的权限 --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <!-- csproj文件指定SupportedOSPlatformVersion android 31.0 使用安卓12的权限 --> <!-- Android 12以下才需要定位权限,Android 9以下官方建议申请ACCESS_COARSE_LOCATION --> <!--<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>--> <!-- Android 12在不申请定位权限时,必须加上android:usesPermissionFlags="neverForLocation",否则搜不到设备 --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <!--蓝牙 END--> </manifest>
iOS
Info.plist
<key>UIBackgroundModes</key> <array> <string>bluetooth-central</string> <string>bluetooth-peripheral</string> </array> <key>NSBluetoothPeripheralUsageDescription</key> <string>此应用程序需要访问您的蓝牙。请根据要求授予权限.</string> <key>NSBluetoothAlwaysUsageDescription</key> <string>此应用程序需要访问您的蓝牙。请根据要求授予权限.</string>
以下是完整文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>LSRequiresIPhoneOS</key> <true/> <key>UIDeviceFamily</key> <array> <integer>1</integer> <integer>2</integer> </array> <key>UIRequiredDeviceCapabilities</key> <array> <string>arm64</string> </array> <key>UISupportedInterfaceOrientations</key> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> <key>UISupportedInterfaceOrientations~ipad</key> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> <key>XSAppIconAssets</key> <string>Assets.xcassets/appicon.appiconset</string> <key>UIBackgroundModes</key> <array> <string>bluetooth-central</string> <string>bluetooth-peripheral</string> </array> <key>NSBluetoothPeripheralUsageDescription</key> <string>此应用程序需要访问您的蓝牙。请根据要求授予权限.</string> <key>NSBluetoothAlwaysUsageDescription</key> <string>此应用程序需要访问您的蓝牙。请根据要求授予权限.</string> </dict> </plist>
Windows
Package.appxmanifest
4. 编辑 Index.html 文件,引用 BootstrapBlazor UI 库.
完整文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> <title>bh003_ble</title> <base href="/" /> <link href="_content/BootstrapBlazor.FontAwesome/css/font-awesome.min.css" rel="stylesheet"> <link href="_content/BootstrapBlazor/css/bootstrap.blazor.bundle.min.css" rel="stylesheet"> <link href="_content/BootstrapBlazor/css/motronic.min.css" rel="stylesheet"> <link href="css/app.css" rel="stylesheet" /> <link href="bh003_ble.styles.css" rel="stylesheet" /> </head> <body> <div class="status-bar-safe-area"></div> <div id="app">Loading...</div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss"><i class="fa-solid fa-xmark"></i></a> </div> <script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script> <script src="_framework/blazor.webview.js" autostart="false"></script> </body> </html>
5. 添加 BootstrapBlazorRoot 组件
Main.razor 文件添加 BootstrapBlazorRoot 组件
6. 添加命名空间引用
_Imports.razor
@using BootstrapBlazor.Components
7. 添加服务
MauiProgram.cs
添加
builder.Services.AddDensenExtensions(); builder.Services.ConfigureJsonLocalizationOptions(op => { // 忽略文化信息丢失日志 op.IgnoreLocalizerMissing = true; }); builder.Services.AddSingleton<BluetoothLEServices>(); builder.Services.AddScoped<IStorage, StorageService>();
完整文件
using bh003_ble.Data; using Microsoft.Extensions.Logging; using BlazorHybrid.Maui.Shared; using BootstrapBlazor.WebAPI.Services; namespace bh003_ble { public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); builder.Services.AddMauiBlazorWebView(); #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); builder.Logging.AddDebug(); #endif builder.Services.AddSingleton<WeatherForecastService>(); builder.Services.AddDensenExtensions(); builder.Services.ConfigureJsonLocalizationOptions(op => { // 忽略文化信息丢失日志 op.IgnoreLocalizerMissing = true; }); builder.Services.AddSingleton<BluetoothLEServices>(); builder.Services.AddScoped<IStorage, StorageService>(); return builder.Build(); } } }
8. 添加代码后置文件 Pages/Index.razor.cs
Index.razor.cs
using BlazorHybrid.Core.Device; using BlazorHybrid.Maui.Shared; using BootstrapBlazor.Components; using BootstrapBlazor.WebAPI.Services; using Microsoft.AspNetCore.Components; using System.Diagnostics.CodeAnalysis; namespace bh003_ble.Pages; public partial class Index : IAsyncDisposable { [Inject, NotNull] BluetoothLEServices? MyBleTester { get; set; } [Inject, NotNull] protected IStorage? Storage { get; set; } [Inject, NotNull] protected ToastService? ToastService { get; set; } public void SetTagDeviceName(BleTagDevice ble) { MyBleTester.TagDevice = ble; if (!isInit) { MyBleTester.OnMessage += OnMessage; MyBleTester.OnDataReceived += OnDataReceived; MyBleTester.OnStateConnect += OnStateConnect; isInit = true; } } public event Action<string>? OnMessage; public event Action<string>? OnDataReceived; public event Action<bool>? OnStateConnect; bool isInit = false; public async Task<List<BleDevice>?> StartScanAsync() => await MyBleTester.StartScanAsync(); public async Task<List<BleService>?> ConnectToKnownDeviceAsync(Guid deviceID, string? deviceName = null) => await MyBleTester.ConnectToKnownDeviceAsync(deviceID, deviceName); public async Task<List<BleCharacteristic>?> GetCharacteristicsAsync(Guid serviceid) => await MyBleTester.GetCharacteristicsAsync(serviceid); public async Task<string?> ReadDeviceName(Guid? serviceid, Guid? characteristic) => await MyBleTester.ReadDeviceName(serviceid, characteristic); public async Task<byte[]?> ReadDataAsync(Guid characteristic) => await MyBleTester.ReadDataAsync(characteristic); public async Task<bool> SendDataAsync(Guid characteristic, byte[] ary) => await MyBleTester.SendDataAsync(characteristic, ary); public async Task<bool> DisConnectDeviceAsync() => await MyBleTester.DisConnectDeviceAsync(); public Task<bool> BluetoothIsBusy() => MyBleTester.BluetoothIsBusy(); private bool IsScanning = false; private List<BleDevice>? Devices { get; set; } private List<BleService>? Services { get; set; } private List<BleCharacteristic>? Characteristics { get; set; } private string? ReadResult { get; set; } private string? Message { get; set; } = ""; BleTagDevice BleInfo { get; set; } = new BleTagDevice(); private List<SelectedItem> DemoList { get; set; } = new List<SelectedItem>() { new SelectedItem() { Text = "测试数据", Value = "" } }; private List<SelectedItem> DeviceList { get; set; } = new List<SelectedItem>(); private List<SelectedItem> ServiceidList { get; set; } = new List<SelectedItem>(); private List<SelectedItem> CharacteristicList { get; set; } = new List<SelectedItem>(); private Dictionary<string, object>? IsScanningCss => IsScanning ? new() { { "disabled", "" }, } : null; bool IsAutoConnect { get; set; } bool IsAuto { get; set; } bool IsInit { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await Init(); } } async Task<bool> Init() { try { if (IsInit) return true; if (await BluetoothIsBusy()) { await ToastService.Warning("蓝牙正在使用中,请稍后再试"); return false; } OnMessage += Tools_OnMessage; OnDataReceived += Tools_OnDataReceived; OnStateConnect += Tools_OnStateConnect; SetTagDeviceName(BleInfo); IsInit = true; StateHasChanged(); var deviceID = await Storage.GetValue("bleDeviceID", string.Empty); if (!string.IsNullOrEmpty(deviceID)) { BleInfo.Name = await Storage.GetValue("bleDeviceName", string.Empty); BleInfo.DeviceID = Guid.Parse(deviceID); var serviceid = await Storage.GetValue("bleServiceid", string.Empty); if (!string.IsNullOrEmpty(serviceid)) BleInfo.Serviceid = Guid.Parse(serviceid); var characteristic = await Storage.GetValue("bleCharacteristic", string.Empty); if (!string.IsNullOrEmpty(characteristic)) BleInfo.Characteristic = Guid.Parse(characteristic); var auto = await Storage.GetValue("bleAutoConnect", string.Empty); if (auto == "True") { IsAuto = true; await AutoRead(); } } return true; } catch (Exception ex) { System.Console.WriteLine(ex.Message); } return false; } private async Task AutoRead() { Services = null; Characteristics = null; Message = ""; ReadResult = ""; Devices = new List<BleDevice>() { new BleDevice() { Id = BleInfo.DeviceID, Name = BleInfo.Name } }; DeviceList = new List<SelectedItem>() { new SelectedItem() { Text = BleInfo.Name, Value = BleInfo.DeviceID.ToString() } }; IsAutoConnect = true; await OnDeviceSelect(); IsAutoConnect = false; } private async Task OnStateChanged(bool value) { await Storage.SetValue("bleAutoConnect", value.ToString()); } private void Tools_OnStateConnect(bool obj) { } private async void Tools_OnDataReceived(string message) { ReadResult = message; Tools_OnMessage(message); await InvokeAsync(StateHasChanged); } private async void Tools_OnMessage(string message) { if (Message != null && Message.Length > 500) Message = Message.Substring(0, 500); Message = $"{message}rn{Message}"; await InvokeAsync(StateHasChanged); } //扫描外设 private async void ScanDevice() { if (!await Init()) return; IsScanning = true; Devices = null; Services = null; Characteristics = null; Message = ""; ReadResult = ""; DeviceList = new List<SelectedItem>() { new SelectedItem() { Text = "请选择...", Value = "" } }; //开始扫描 Devices = await StartScanAsync(); if (Devices != null) { Devices.ForEach(a => DeviceList.Add(new SelectedItem() { Active = IsAutoConnect && a.Id == BleInfo.DeviceID, Text = a.Name ?? a.Id.ToString(), Value = a.Id.ToString() })); } IsScanning = false; //异步更新UI await InvokeAsync(StateHasChanged); } //连接外设 private async Task OnDeviceSelect(SelectedItem item) { if (IsAutoConnect || item.Value == "") return; BleInfo.Name = item.Text; BleInfo.DeviceID = Guid.Parse(item.Value); await OnDeviceSelect(); } private async Task OnDisConnectDevice() { if (await DisConnectDeviceAsync()) await ToastService.Success("断开成功"); else await ToastService.Error("断开失败"); } private async Task OnDeviceSelect() { Services = null; Characteristics = null; Message = ""; ReadResult = ""; ServiceidList = new List<SelectedItem>() { new SelectedItem() { Text = "请选择...", Value = "" } }; //连接外设 Services = await ConnectToKnownDeviceAsync(BleInfo.DeviceID, BleInfo.Name); if (Services != null) { Services.ForEach(a => ServiceidList.Add(new SelectedItem() { Active = IsAutoConnect && a.Id == BleInfo.Serviceid, Text = a.Name ?? a.Id.ToString(), Value = a.Id.ToString() })); await Storage.SetValue("bleDeviceID", BleInfo.DeviceID.ToString()); await Storage.SetValue("bleDeviceName", BleInfo.Name ?? "上次设备"); if (BleInfo.Serviceid != Guid.Empty && IsAutoConnect) { await OnServiceidSelect(); } } //异步更新UI await InvokeAsync(StateHasChanged); } private async Task OnServiceidSelect(SelectedItem item) { if (IsAutoConnect || item.Value == "") return; BleInfo.Serviceid = Guid.Parse(item.Value); await OnServiceidSelect(); } private async Task OnServiceidSelect() { Characteristics = null; Message = ""; ReadResult = ""; CharacteristicList = new List<SelectedItem>() { new SelectedItem() { Text = "请选择...", Value = "" } }; Characteristics = await GetCharacteristicsAsync(BleInfo.Serviceid); if (Characteristics != null) { Characteristics.ForEach(a => CharacteristicList.Add(new SelectedItem() { Active = IsAutoConnect && a.Id == BleInfo.Characteristic, Text = a.Name ?? a.Id.ToString(), Value = a.Id.ToString() })); await Storage.SetValue("bleServiceid", BleInfo.Serviceid.ToString()); if (BleInfo.Characteristic != Guid.Empty && IsAutoConnect) { await ReadDeviceName(); } } await InvokeAsync(StateHasChanged); } private async Task OnCharacteristSelect(SelectedItem item) { if (IsAutoConnect) return; BleInfo.Characteristic = Guid.Parse(item.Value); await ReadDeviceName(); } //读取数值 private async Task ReadDeviceName() { Message = ""; //读取数值 ReadResult = await ReadDeviceName(BleInfo.Serviceid, BleInfo.Characteristic); await Storage.SetValue("bleCharacteristic", BleInfo.Characteristic.ToString()); if (!string.IsNullOrEmpty(ReadResult)) await ToastService.Information("读取成功", ReadResult); //异步更新UI await InvokeAsync(StateHasChanged); } private async Task ReadDataAsync() { Message = ""; //读取数值 var res = await ReadDataAsync(BleInfo.Characteristic); if (!string.IsNullOrEmpty(ReadResult)) await ToastService.Information("读取成功", res.ToString()); //异步更新UI await InvokeAsync(StateHasChanged); } private async Task SendDataAsync() { Message = ""; //读取数值 var res = await SendDataAsync(BleInfo.Characteristic, null); await ToastService.Information("成功发送", res.ToString()); //异步更新UI await InvokeAsync(StateHasChanged); } ValueTask IAsyncDisposable.DisposeAsync() { OnMessage -= Tools_OnMessage; OnDataReceived -= Tools_OnDataReceived; OnStateConnect -= Tools_OnStateConnect; return new ValueTask(); } }
9. 添加 UI Pages/Index.razor
Index.razor
@page "/" <h3>蓝牙</h3> <div class="row g-3"> <div class="btn-group" role="group"> <Button Text="扫描外设" @attributes=IsScanningCss OnClick=ScanDevice /> @if (Devices != null) { <Button Text="连接" OnClick="OnDeviceSelect" /> <Button Text="断开" OnClick="OnDisConnectDevice" /> @if (Characteristics != null) { <Button Text="写入" OnClick="ReadDeviceName" /> <Button Text="读取" OnClick="ReadDeviceName" /> } } </div> </div> @if (Devices != null) { <div class="row g-3"> <div class="col-12 col-sm-3"> <Select TValue="Guid" Items="DeviceList" OnSelectedItemChanged="OnDeviceSelect" /> </div> @if (Services != null) { <div class="col-12 col-sm-3"> <Select TValue="Guid" Items="ServiceidList" OnSelectedItemChanged="OnServiceidSelect" /> </div> @if (Characteristics != null) { <div class="col-12 col-sm-3"> <Select TValue="Guid" Items="CharacteristicList" OnSelectedItemChanged="OnCharacteristSelect" /> </div> @if (ReadResult != null) { <div class="col-12 col-sm-3"> <Display TValue="string" Value="@ReadResult" /> </div> } } } </div> } @if (BleInfo.Name != null) { <div class="g-3"> 历史连接 <br /> @BleInfo.Name <br /> @BleInfo.DeviceID <br /> @BleInfo.Serviceid <br /> @BleInfo.Characteristic <br /> @ReadResult <br /> </div> } <Switch DisplayText="自动连接" OnText="自动连接" OffText="手动连接" Value="@IsAuto" OnValueChanged="@OnStateChanged" /> <pre style="max-height: 500px; overflow-y: scroll; white-space: pre-wrap; word-wrap: break-word;">@Message</pre>
10. 运行
11. 相关资料
如何远程调试 MAUI blazor / Blazor Hybrid
https://www.cnblogs.com/densen2014/p/16988516.html