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}\r\n{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