diff --git a/exclusion.dic b/exclusion.dic
index b58aa92bdae..c3bf7a7acd2 100644
--- a/exclusion.dic
+++ b/exclusion.dic
@@ -106,3 +106,4 @@ Validatable
noselect
Urls
Pharmacode
+bluetooth
diff --git a/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj b/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj
index 1cc8df04f25..1ed132b1c2f 100644
--- a/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj
+++ b/src/BootstrapBlazor.Server/BootstrapBlazor.Server.csproj
@@ -29,7 +29,6 @@
-
diff --git a/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor b/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor
index 469194715b6..6189781f746 100644
--- a/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor
+++ b/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor
@@ -3,74 +3,109 @@
- @((MarkupString)Localizer["Tips"].Value)
+@((MarkupString)Localizer["BluetoothDescription"].Value)
+
+[Inject, NotNull]
+private IBluetoothService? BluetoothService { get; set; }
+
+
+
+ - @((MarkupString)Localizer["BluetoothTipsLi1"].Value)
+ - @((MarkupString)Localizer["BluetoothTipsLi2"].Value)
+
+ @((MarkupString)Localizer["BluetoothTipsTitle"].Value)
- @if (ShowUI)
- {
- @Localizer["InnerUI"]
+
+
+
+1. 服务注入
+
+[Inject]
+[NotNull]
+private IBluetoothService? BluetoothService { get; set; }
+
+2. 列出蓝牙设备
+调用 BluetoothService 实例方法 RequestDevice 即可,通过 IsSupport 进行浏览器是否支持蓝牙
+
+_serialPort = await BluetoothService.RequestDevice(["battery_service"]);
+if (BluetoothService.IsSupport == false)
+{
+ await ToastService.Error(Localizer["NotSupportBluetoothTitle"], Localizer["NotSupportBluetoothContent"]);
+}
-
+3. 连接设备
+调用 IBluetoothDevice 实例方法 Connect 即可
+private async Task Connect()
+{
+ if (_blueDevice != null)
+ {
+ var ret = await _blueDevice.Connect();
+ if (ret == false && !string.IsNullOrEmpty(_blueDevice.ErrorMessage))
+ {
+ await ToastService.Error("Connect", _blueDevice.ErrorMessage);
+ }
}
- else
+}
+
+4. 断开设备
+
+调用 IBluetoothDevice 实例方法 Disconnect 断开连接,请注意路由切换时,请调用其 DisposeAsync 方法释放资源
+
+private async Task Disconnect()
+{
+ if (_blueDevice != null)
{
- @Localizer["BasicUsage"]
-
-
-
-
-
-
+ var ret = await _blueDevice.Disconnect();
+ if (ret == false && !string.IsNullOrEmpty(_blueDevice.ErrorMessage))
+ {
+ await ToastService.Error("Disconnect", _blueDevice.ErrorMessage);
+ }
}
-
-@message
-@statusMessage
-@errorMessage
-
-
-
+}
-@Localizer["BluetoothTitle"]
-
-
-
-
-
-
- @message
- @statusMessage
- @errorMessage
-
+注意事项
+可以通过调用 IBluetoothService 实例方法 GetAvailability 方法后,判断其实例属性 IsAvailable 检查当前终端是否有蓝牙模块
-@Localizer["BluetoothHeartRate"]
-
-
-
-
-
-
- @message
- @statusMessage
- @errorMessage
-
+IBluetoothService 实例属性 IsSupport 是表示当前浏览器是否支持蓝牙功能
+
+IBluetoothService 与 IBluetoothDevice 所有实例方法均有返回值,可通过查看其实例属性 ErrorMessage 获得上一次执行的错误描述信息
+
+IBluetoothDevice 实例方法 ReadValue 是通用方法,通过参数指定Services 与 Characteristics
-
+原生方法 getDevices 暂未封装,因为需要设置浏览器才能开启
-
+可根据自己的业务需求自定义扩展方法,内置扩展方法列表如下:
-
+
+ GetBatteryValue 读取电量方法
+
-
+相关文档
-
+
+ - Service List:[传送门]
+ - Characteristics List:[传送门]
+ - Descriptors List:[传送门]
+
diff --git a/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor.cs b/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor.cs
index 654e9a23119..447ac818d14 100644
--- a/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor.cs
+++ b/src/BootstrapBlazor.Server/Components/Samples/Bluetooth.razor.cs
@@ -9,347 +9,75 @@ namespace BootstrapBlazor.Server.Components.Samples;
///
public partial class Bluetooth
{
- Printer printer { get; set; } = new Printer();
+ [Inject, NotNull]
+ private IBluetoothService? BluetoothService { get; set; }
- ///
- /// 显示内置界面
- ///
- bool ShowUI { get; set; } = false;
+ [Inject, NotNull]
+ private ToastService? ToastService { get; set; }
- private string? message;
- private string? statusMessage;
- private string? errorMessage;
+ private string? BluetoothDeviceName => _blueDevice?.Name ?? "Unknown or Unsupported Device";
- private Task OnResult(string? result)
- {
- message = result;
- StateHasChanged();
- return Task.CompletedTask;
- }
+ private IBluetoothDevice? _blueDevice;
- private Task OnUpdateStatus(string message)
- {
- statusMessage = message;
- StateHasChanged();
- return Task.CompletedTask;
- }
+ private string? _batteryValue = null;
- private Task OnError(string message)
- {
- errorMessage = message;
- StateHasChanged();
- return Task.CompletedTask;
- }
+ private string? _batteryValueString = null;
- private Task OnGetDevices(List? devices)
+ private async Task RequestDevice()
{
- message = "";
- if (devices == null || devices!.Count == 0) return Task.CompletedTask;
- message += $"已配对设备{devices.Count}:{Environment.NewLine}";
- devices.ForEach(a => message += $" {a}{Environment.NewLine}");
- //this.message = this.message.Replace(Environment.NewLine, "
");
- StateHasChanged();
- return Task.CompletedTask;
- }
+ _blueDevice = await BluetoothService.RequestDevice(["battery_service"]);
+ if (BluetoothService.IsSupport == false)
+ {
+ await ToastService.Error(Localizer["NotSupportBluetoothTitle"], Localizer["NotSupportBluetoothContent"]);
+ return;
+ }
- ///
- /// 切换 UI 方法
- ///
- public void SwitchUI()
- {
- ShowUI = !ShowUI;
+ if (_blueDevice == null && !string.IsNullOrEmpty(BluetoothService.ErrorMessage))
+ {
+ await ToastService.Error("Request", BluetoothService.ErrorMessage);
+ }
}
- Heartrate heartrate { get; set; } = new Heartrate();
-
- private Task OnUpdateValue(int v)
+ private async Task Connect()
{
- value = v;
- statusMessage = $"心率{value}";
- StateHasChanged();
- return Task.CompletedTask;
+ if (_blueDevice != null)
+ {
+ var ret = await _blueDevice.Connect();
+ if (ret == false && !string.IsNullOrEmpty(_blueDevice.ErrorMessage))
+ {
+ await ToastService.Error("Connect", _blueDevice.ErrorMessage);
+ }
+ }
}
- ///
- /// 获取心率
- ///
- public Task GetHeartrate() => heartrate.GetHeartrate();
-
- ///
- /// 停止获取心率
- ///
- public Task StopHeartrate() => heartrate.StopHeartrate();
-
- BatteryLevel batteryLevel { get; set; } = new BatteryLevel();
-
- private decimal? value = 0;
-
- private Task OnUpdateValue(decimal v)
+ private async Task Disconnect()
{
- value = v;
- statusMessage = Localizer["DeviceBattery", value];
- StateHasChanged();
- return Task.CompletedTask;
+ if (_blueDevice != null)
+ {
+ var ret = await _blueDevice.Disconnect();
+ if (ret == false && !string.IsNullOrEmpty(_blueDevice.ErrorMessage))
+ {
+ await ToastService.Error("Disconnect", _blueDevice.ErrorMessage);
+ }
+ }
}
- private Task OnUpdateStatus(BluetoothDevice device)
+ private async Task GetBatteryValue()
{
- statusMessage = device.Status;
- StateHasChanged();
- return Task.CompletedTask;
- }
-
- ///
- /// 获取设备电量
- ///
- public Task GetBatteryLevel() => batteryLevel.GetBatteryLevel();
+ _batteryValue = null;
+ _batteryValueString = null;
- ///
- /// 获得属性方法
- ///
- ///
- private AttributeItem[] GetAttributes() =>
- [
- new()
- {
- Name = "Commands",
- Description = Localizer["CommandsAttr"],
- Type = "string?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "Print",
- Description = Localizer["PrintAttr"],
- Type = "async Task",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateStatus",
- Description = Localizer["OnUpdateStatusAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateError",
- Description = Localizer["OnUpdateErrorAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "PrinterElement",
- Description = Localizer["PrinterElementAttr"],
- Type = "ElementReference",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
+ if (_blueDevice != null)
{
- Name = "Opt",
- Description = Localizer["OptAttr"],
- Type = "PrinterOption",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "ShowUI",
- Description = Localizer["ShowUIAttr"],
- Type = "bool",
- ValueList = "True|False",
- DefaultValue = "False"
- },
- new()
- {
- Name = "Debug",
- Description = Localizer["DebugAttr"],
- Type = "bool",
- ValueList = "True|False",
- DefaultValue = "False"
- },
- new()
- {
- Name = "DeviceName",
- Description = Localizer["DeviceNameAttr"],
- Type = "string?",
- ValueList = "-",
- DefaultValue = "-"
- },
- ];
+ var val = await _blueDevice.GetBatteryValue();
+ if (val == 0 && !string.IsNullOrEmpty(_blueDevice.ErrorMessage))
+ {
+ await ToastService.Error("Battery Value", _blueDevice.ErrorMessage);
+ return;
+ }
- ///
- /// 获得属性方法
- ///
- ///
- private AttributeItem[] GetPrinterOptionAttributes() =>
- [
- new()
- {
- Name = "NamePrefix",
- Description = Localizer["NamePrefixAttr"],
- Type = "string?",
- ValueList = "-",
- DefaultValue = "null"
- },
- new()
- {
- Name = "MaxChunk",
- Description = Localizer["MaxChunkAttr"],
- Type = "int",
- ValueList = "-",
- DefaultValue = "100"
- },
- ];
-
- ///
- /// 获得蓝牙设备类
- ///
- ///
- private AttributeItem[] GetBluetoothDeviceAttributes() =>
- [
- new()
- {
- Name = "Name",
- Description = Localizer["NameAttr"],
- Type = "string?",
- ValueList = "-",
- DefaultValue = "null"
- },
- new()
- {
- Name = "Value",
- Description = Localizer["ValueAttr"],
- Type = "decimal?",
- ValueList = "-",
- DefaultValue = "null"
- },
- new()
- {
- Name = "Status",
- Description = Localizer["StatusAttr"],
- Type = "string?",
- ValueList = "-",
- DefaultValue = "null"
- },
- new()
- {
- Name = "Error",
- Description = Localizer["ErrorAttr"],
- Type = "string?",
- ValueList = "-",
- DefaultValue = "null"
+ _batteryValue = $"{val}";
+ _batteryValueString = $"{_batteryValue} %";
}
- ];
-
- ///
- /// 获得属性方法
- ///
- ///
- private AttributeItem[] GetAttributesBatteryLevel() =>
- [
- new()
- {
- Name = "GetBatteryLevel",
- Description = Localizer["GetBatteryLevelAttr"],
- Type = "async Task",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateValue",
- Description = Localizer["OnUpdateValueAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateStatus",
- Description = Localizer["OnUpdateStatusAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateError",
- Description = Localizer["OnUpdateErrorAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "BatteryLevelElement",
- Description = Localizer["BatteryLevelElementAttr"],
- Type = "ElementReference",
- ValueList = "-",
- DefaultValue = "-"
- }
- ];
-
-
- ///
- /// 获得属性方法
- ///
- ///
- private AttributeItem[] GetAttributesHeartrate() =>
- [
- new()
- {
- Name = "GetHeartrate",
- Description = Localizer["GetHeartrateAttr"],
- Type = "async Task",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "StopHeartrate",
- Description = Localizer["StopHeartrateAttr"],
- Type = "async Task",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateValue",
- Description = Localizer["OnUpdateValueAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateStatus",
- Description = Localizer["OnUpdateStatusAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "OnUpdateError",
- Description = Localizer["OnUpdateErrorAttr"],
- Type = "Func?",
- ValueList = "-",
- DefaultValue = "-"
- },
- new()
- {
- Name = "HeartrateElement",
- Description = Localizer["HeartrateElementAttr"],
- Type = "ElementReference",
- ValueList = "-",
- DefaultValue = "-"
- }
- ];
+ }
}
diff --git a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
index ef631fb4efc..9fbf8dea4a9 100644
--- a/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
+++ b/src/BootstrapBlazor.Server/Extensions/MenusLocalizerExtensions.cs
@@ -531,11 +531,6 @@ void AddData(DemoMenuItem item)
Url = "block"
},
new()
- {
- Text = Localizer["Bluetooth"],
- Url = "blue-tooth"
- },
- new()
{
Text = Localizer["Card"],
Url = "card"
@@ -1386,6 +1381,11 @@ void AddServices(DemoMenuItem item)
Url = "ocr"
},
new()
+ {
+ Text = Localizer["Bluetooth"],
+ Url = "blue-tooth"
+ },
+ new()
{
Text = Localizer["BrowserFinger"],
Url = "browser-finger"
diff --git a/src/BootstrapBlazor.Server/Locales/en-US.json b/src/BootstrapBlazor.Server/Locales/en-US.json
index e14050c5a3f..40037573798 100644
--- a/src/BootstrapBlazor.Server/Locales/en-US.json
+++ b/src/BootstrapBlazor.Server/Locales/en-US.json
@@ -1683,7 +1683,7 @@
"OnScreenKeyboardText": "OnScreenKeyboard",
"NotificationsText": "Notification",
"SignaturePadText": "SignaturePad",
- "BluetoothText": "Bluetooth & Printer",
+ "BluetoothText": "Bluetooth Service",
"PdfReaderText": "PDF Reader",
"VideoPlayerText": "VideoPlayer",
"FileViewerText": "FileViewer",
@@ -4702,7 +4702,7 @@
"OnScreenKeyboard": "OnScreenKeyboard",
"RibbonTab": "RibbonTab",
"PulseButton": "PulseButton",
- "Bluetooth": "Bluetooth & Printer",
+ "Bluetooth": "IBluetooth",
"PdfReader": "PDF Reader",
"VideoPlayer": "VideoPlayer",
"FileViewer": "FileViewer",
@@ -5887,56 +5887,21 @@
"OnScreenKeyboardsBasicTitle": "Basic"
},
"BootstrapBlazor.Server.Components.Samples.Bluetooth": {
- "BluetoothTitle": "Bluetooth & Printer",
+ "BluetoothTitle": "IBluetooth Service ",
+ "BluetoothIntro": "Provides methods to query Bluetooth availability and request access to devices",
+ "BluetoothDescription": "Allows websites to communicate with Bluetooth devices connected to the user's computer. For example, you can connect to a Bluetooth printer to print.",
+ "BluetoothTipsLi1": "This feature is available only in secure contexts (HTTPS)",
+ "BluetoothTipsLi2": "This is an experimental technology Check the Browser compatibility table carefully before using this in production",
+ "BluetoothTipsTitle": "Note: The IBluetoothService interface instance inherits IAsyncDisposable. When switching routes, you need to release its resources by calling its DisposeAsync.",
+ "NotSupportBluetoothTitle": "Scan Devices",
+ "NotSupportBluetoothContent": "The current browser does not support serial port operations. Please change to Edge or Chrome browser.",
+ "BluetoothRequestText": "Scan",
+ "BluetoothConnectText": "Connect",
+ "BluetoothDisconnectText": "Disconnect",
+ "BluetoothGetBatteryText": "Battery",
+ "BluetoothGetHeartRateText": "HeartRate",
"BaseUsageTitle": "Basic usage",
- "BaseUsageIntro": "After connecting the device, perform operations such as printing, and write the corresponding commands(ESC/POS/CPCL) according to the characteristics of the printer",
- "BluetoothBatteryLevelTitle": "Bluetooth Battery Level",
- "BluetoothBatteryLevelIntro": "Click to connect device",
- "ConnectButtonText": "Connect",
- "DisconnectButtonText": "Disconnect",
- "ReconnectButtonText": "Reconnect",
- "PrintButtonText": "Print",
- "Url1": "1. Printer, for printable label/barcode/QR code",
- "BluetoothsTitle2": "Bluetooth Heart Rate",
- "GetHeartrateButtonText": "Get heart rate",
- "StopHeartrateButtonText": "Stop heart rate",
- "Url2": "2. Heart Rate",
- "BluetoothHeartRate": "Bluetooth Heart Rate Band",
- "BatteryLevelTitle": "Heart rate monitor",
- "BatteryLevelIntro": "Connect the heart rate band",
- "GetBatteryLevelButtonText": "Get Battery Level",
- "Url3": "3. Battery Level",
- "Tips": "ServiceUUID, Default 0xff00
Common Printers ServiceUUID:
0000ff00-0000-1000-8000-00805f9b34fb
e7810a71-73ae-499d-8c15-faa9aef0c3f2
0000fee7-0000-1000-8000-00805f9b34fb
Set up component services UUID : printer.Opt.ServiceUuid=?",
- "InnerUI": "Built-in UI",
- "BasicUsage": "Basic usage",
- "PrinterComponent": "Printer Component",
- "BatteryLevelComponent": "BatteryLevel Component",
- "HeartrateComponent": "Heartrate Component",
- "BluetoothDeviceClass": "BluetoothDevice Class",
- "PrinterOptionClass": "PrinterOption Class",
- "DeviceBattery": "Device power {0}%",
- "CommandsAttr": "Print instructions (cpcl/esp/pos code)",
- "PrintAttr": "Print",
- "OnUpdateStatusAttr": "Status update callback method",
- "OnUpdateErrorAttr": "Error update callback method",
- "PrinterElementAttr": "The reference object for UI interface elements, if left blank, the entire page will be used",
- "OptAttr": "Printer Options",
- "ShowUIAttr": "Obtain/Set Display Built-in UI",
- "DebugAttr": "Obtain/Set Display Log",
- "DeviceNameAttr": "Obtain/Set Device Name",
- "NamePrefixAttr": "Initial search for device name prefix, default to null",
- "MaxChunkAttr": "Data slice size, default to 100",
- "NameAttr": "Device Name",
- "ValueAttr": "Device value: such as heart rate/battery level%",
- "StatusAttr": "Status",
- "ErrorAttr": "Error",
- "GetBatteryLevelAttr": "Query battery level",
- "OnUpdateValueAttr": "Numerical update callback method",
- "BatteryLevelElementAttr": "Reference objects for UI interface elements",
- "GetHeartrateAttr": "Connect the heart rate band",
- "StopHeartrateAttr": "Stop monitoring heart rate",
- "HeartrateElementAttr": "Reference objects for UI interface elements",
- "SwitchUI": "Switch UI"
+ "BaseUsageIntro": "Request communication with Bluetooth devices through the IBluetoothService service"
},
"BootstrapBlazor.Server.Components.Samples.FileIcons": {
"Title": "File Icon",
diff --git a/src/BootstrapBlazor.Server/Locales/zh-CN.json b/src/BootstrapBlazor.Server/Locales/zh-CN.json
index 95c943a0580..60d5e8709a2 100644
--- a/src/BootstrapBlazor.Server/Locales/zh-CN.json
+++ b/src/BootstrapBlazor.Server/Locales/zh-CN.json
@@ -1683,7 +1683,7 @@
"NotificationsText": "浏览器通知 Notification",
"OnScreenKeyboardText": "屏幕键盘 OnScreenKeyboard",
"SignaturePadText": "手写签名 SignaturePad",
- "BluetoothText": "蓝牙和打印 Bluetooth & Printer",
+ "BluetoothText": "蓝牙服务 IBluetoothService",
"PdfReaderText": "PDF阅读器 PDF Reader",
"VideoPlayerText": "视频播放器 VideoPlayer",
"FileViewerText": "文件预览器 FileViewer",
@@ -4702,7 +4702,7 @@
"OnScreenKeyboard": "屏幕键盘 OnScreenKeyboard",
"RibbonTab": "选项卡菜单 RibbonTab",
"PulseButton": "心跳按钮 PulseButton",
- "Bluetooth": "蓝牙和打印 Bluetooth & Printer",
+ "Bluetooth": "蓝牙服务 IBluetoothService",
"PdfReader": "PDF阅读器 PDF Reader",
"VideoPlayer": "视频播放器 VideoPlayer",
"FileViewer": "文件预览器 FileViewer",
@@ -5887,56 +5887,21 @@
"OnScreenKeyboardsBasicTitle": "基础用法"
},
"BootstrapBlazor.Server.Components.Samples.Bluetooth": {
- "BluetoothTitle": "Bluetooth & Printer 蓝牙和打印",
+ "BluetoothTitle": "IBluetoothService 蓝牙服务",
+ "BluetoothIntro": "提供了查询蓝牙可用性和请求访问设备的方法",
+ "BluetoothDescription": "允许网站与连接到用户计算机的蓝牙设备进行通信。例如可以连接蓝牙打印机进行打印操作",
+ "BluetoothTipsLi1": "该功能仅在部分或所有支持浏览器的安全上下文(HTTPS)中可用",
+ "BluetoothTipsLi2": "这是一项实验性技术,在生产中使用之前请仔细,检查 浏览器兼容性表",
+ "BluetoothTipsTitle": "注意:IBluetoothService 接口实例继承 IAsyncDisposable 路由切换时需要对其进行资源释放,调用其 DisposeAsync 即可",
+ "NotSupportBluetoothTitle": "扫描设备",
+ "NotSupportBluetoothContent": "当前浏览器不支持串口操作,请更换 Edge 或者 Chrome 浏览器",
+ "BluetoothRequestText": "扫描",
+ "BluetoothConnectText": "连接",
+ "BluetoothDisconnectText": "断开",
+ "BluetoothGetBatteryText": "读取电量",
+ "BluetoothGetHeartRateText": "读取心率",
"BaseUsageTitle": "基础用法",
- "BaseUsageIntro": "连接设备后再执行打印等操作,根据打印机特性写入相应ESC/POS/CPCL命令",
- "BluetoothBatteryLevelTitle": "蓝牙设备电量",
- "BluetoothBatteryLevelIntro": "点击连接设备",
- "ConnectButtonText": "连接",
- "DisconnectButtonText": "断开",
- "ReconnectButtonText": "重连",
- "PrintButtonText": "打印",
- "Url1": "1. 蓝牙打印机 Printer,可打印标签/条码/QR码",
- "BluetoothsTitle2": "Bluetooth Heart Rate 蓝牙心率带",
- "GetHeartrateButtonText": "查询心率",
- "StopHeartrateButtonText": "停止读取",
- "Url2": "2. 蓝牙心率带 Heart Rate",
- "BluetoothHeartRate": "蓝牙心率带",
- "BatteryLevelTitle": "心率带",
- "BatteryLevelIntro": "连接心率带",
- "GetBatteryLevelButtonText": "查询电量",
- "Url3": "3. 蓝牙设备电量 Battery Level",
- "Tips": "服务UUID/ServiceUUID, 默认0xff00
常见打印机ServiceUUID:
0000ff00-0000-1000-8000-00805f9b34fb
e7810a71-73ae-499d-8c15-faa9aef0c3f2
0000fee7-0000-1000-8000-00805f9b34fb
设置组件服务UUID : printer.Opt.ServiceUuid=?",
- "InnerUI": "内置UI",
- "BasicUsage": "基本用法",
- "PrinterComponent": "Printer 组件",
- "BatteryLevelComponent": "BatteryLevel 组件",
- "HeartrateComponent": "Heartrate 组件",
- "BluetoothDeviceClass": "BluetoothDevice 类",
- "PrinterOptionClass": "PrinterOption 类",
- "DeviceBattery": "设备电量{0}%",
- "CommandsAttr": "打印指令(cpcl/esp/pos代码)",
- "PrintAttr": "打印",
- "OnUpdateStatusAttr": "状态更新回调方法",
- "OnUpdateErrorAttr": "错误更新回调方法",
- "PrinterElementAttr": "UI界面元素的引用对象,为空则使用整个页面",
- "OptAttr": "打印机选项",
- "ShowUIAttr": "获得/设置 显示内置UI",
- "DebugAttr": "获得/设置 显示Log",
- "DeviceNameAttr": "获得/设置 设备名称",
- "NamePrefixAttr": "初始搜索设备名称前缀,默认 null",
- "MaxChunkAttr": "数据切片大小,默认100",
- "NameAttr": "设备名称",
- "ValueAttr": "设备数值:例如心率/电量%",
- "StatusAttr": "状态",
- "ErrorAttr": "错误",
- "GetBatteryLevelAttr": "查询电量",
- "OnUpdateValueAttr": "数值更新回调方法",
- "BatteryLevelElementAttr": "UI界面元素的引用对象",
- "GetHeartrateAttr": "连接心率带",
- "StopHeartrateAttr": "停止监听心率",
- "HeartrateElementAttr": "UI界面元素的引用对象",
- "SwitchUI": "切换界面"
+ "BaseUsageIntro": "通过 IBluetoothService 服务,请求与蓝牙设备通讯"
},
"BootstrapBlazor.Server.Components.Samples.FileIcons": {
"Title": "File Icon 文件图标",
diff --git a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
index 7b0645bf1a9..19475fa14c8 100644
--- a/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
+++ b/src/BootstrapBlazor/Extensions/BootstrapBlazorServiceCollectionExtensions.cs
@@ -69,7 +69,7 @@ public static IServiceCollection AddBootstrapBlazor(this IServiceCollection serv
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
-
+ services.TryAddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/src/BootstrapBlazor/Extensions/IBluetoothDeviceExtensions.cs b/src/BootstrapBlazor/Extensions/IBluetoothDeviceExtensions.cs
new file mode 100644
index 00000000000..c27f5d1395f
--- /dev/null
+++ b/src/BootstrapBlazor/Extensions/IBluetoothDeviceExtensions.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+namespace BootstrapBlazor.Components;
+
+///
+/// IBluetoothDevice 扩展方法
+///
+public static class IBluetoothDeviceExtensions
+{
+ ///
+ /// 获得 设备电量方法
+ ///
+ ///
+ ///
+ public static async Task GetBatteryValue(this IBluetoothDevice blueDevice)
+ {
+ byte value = 0;
+ var data = await blueDevice.ReadValue("battery_service", "battery_level");
+ if (data is { Length: > 0 })
+ {
+ value = data[0];
+ }
+ return value;
+ }
+}
diff --git a/src/BootstrapBlazor/Services/Bluetooth/BluetoothDevice.cs b/src/BootstrapBlazor/Services/Bluetooth/BluetoothDevice.cs
new file mode 100644
index 00000000000..6141e88e3dc
--- /dev/null
+++ b/src/BootstrapBlazor/Services/Bluetooth/BluetoothDevice.cs
@@ -0,0 +1,126 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+namespace BootstrapBlazor.Components;
+
+///
+/// 蓝牙设备
+///
+sealed class BluetoothDevice : IBluetoothDevice
+{
+ private readonly JSModule _module;
+
+ private readonly string _clientId;
+
+ private readonly DotNetObjectReference _interop;
+
+ ///
+ ///
+ ///
+ public string? Name { get; }
+
+ ///
+ ///
+ ///
+ public string? Id { get; }
+
+ ///
+ ///
+ ///
+ public string? ErrorMessage { get; private set; }
+
+ ///
+ ///
+ ///
+ public bool Connected { get; private set; }
+
+ public BluetoothDevice(JSModule module, string clientId, string[] args)
+ {
+ _module = module;
+ _clientId = clientId;
+ _interop = DotNetObjectReference.Create(this);
+
+ if (args.Length == 2)
+ {
+ Name = args[0];
+ Id = args[1];
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public async Task Connect(CancellationToken token = default)
+ {
+ if (Connected == false)
+ {
+ ErrorMessage = null;
+ Connected = await _module.InvokeAsync("connect", token, _clientId, _interop, nameof(OnError));
+ }
+ return Connected;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public async Task Disconnect(CancellationToken token = default)
+ {
+ var ret = false;
+ if (Connected)
+ {
+ ErrorMessage = null;
+ ret = await _module.InvokeAsync("disconnect", token, _clientId, _interop, nameof(OnError));
+ if (ret)
+ {
+ Connected = false;
+ }
+ }
+ return ret;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public async Task ReadValue(string serviceName, string characteristicName, CancellationToken token = default)
+ {
+ byte[]? ret = null;
+ if (Connected)
+ {
+ ErrorMessage = null;
+ ret = await _module.InvokeAsync("readValue", token, _clientId, serviceName, characteristicName, _interop, nameof(OnError));
+ }
+ return ret;
+ }
+
+ ///
+ /// JavaScript 报错回调方法
+ ///
+ ///
+ [JSInvokable]
+ public void OnError(string message)
+ {
+ ErrorMessage = message;
+ }
+
+ private async ValueTask DisposeAsync(bool disposing)
+ {
+ if (disposing)
+ {
+ await _module.InvokeVoidAsync("dispose", _clientId);
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ await DisposeAsync(true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/BootstrapBlazor/Services/Bluetooth/DefaultBluetoothService.cs b/src/BootstrapBlazor/Services/Bluetooth/DefaultBluetoothService.cs
new file mode 100644
index 00000000000..abe3fb8b6eb
--- /dev/null
+++ b/src/BootstrapBlazor/Services/Bluetooth/DefaultBluetoothService.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+namespace BootstrapBlazor.Components;
+
+sealed class DefaultBluetoothService : IBluetoothService
+{
+ ///
+ ///
+ ///
+ public bool IsSupport { get; private set; }
+
+ ///
+ ///
+ ///
+ public bool IsAvailable { get; private set; }
+
+ ///
+ ///
+ ///
+ public string? ErrorMessage { get; private set; }
+
+ [NotNull]
+ private JSModule? _module = null;
+
+ private readonly IJSRuntime _runtime;
+
+ private readonly string _deviceId;
+
+ private readonly DotNetObjectReference _interop;
+
+ ///
+ /// 构造函数
+ ///
+ ///
+ public DefaultBluetoothService(IJSRuntime jsRuntime)
+ {
+ _runtime = jsRuntime;
+ _deviceId = $"bb_bt_{GetHashCode()}";
+ _interop = DotNetObjectReference.Create(this);
+ }
+
+ private async Task LoadModule()
+ {
+ var module = await _runtime.LoadModule("./_content/BootstrapBlazor/modules/bt.js");
+
+ IsSupport = await module.InvokeAsync("init");
+ return module;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task GetAvailability(CancellationToken token = default)
+ {
+ _module ??= await LoadModule();
+
+ var ret = false;
+ if (IsSupport)
+ {
+ ret = await _module.InvokeAsync("getAvailability", token);
+ IsAvailable = ret;
+ }
+ return ret;
+ }
+
+ ///
+ ///
+ ///
+ public async Task RequestDevice(string[] optionalServices, CancellationToken token = default)
+ {
+ _module ??= await LoadModule();
+
+ BluetoothDevice? device = null;
+ if (IsSupport)
+ {
+ ErrorMessage = null;
+ var parameters = await _module.InvokeAsync("requestDevice", token, _deviceId, optionalServices, _interop, nameof(OnError));
+ if (parameters != null)
+ {
+ device = new BluetoothDevice(_module, _deviceId, parameters);
+ }
+ }
+ return device;
+ }
+
+ ///
+ /// JavaScript 报错回调方法
+ ///
+ ///
+ [JSInvokable]
+ public void OnError(string message)
+ {
+ ErrorMessage = message;
+ }
+}
diff --git a/src/BootstrapBlazor/Services/Bluetooth/IBluetoothDevice.cs b/src/BootstrapBlazor/Services/Bluetooth/IBluetoothDevice.cs
new file mode 100644
index 00000000000..2dfd74f0b14
--- /dev/null
+++ b/src/BootstrapBlazor/Services/Bluetooth/IBluetoothDevice.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+namespace BootstrapBlazor.Components;
+
+///
+/// IBluetoothDevice 接口
+///
+public interface IBluetoothDevice : IAsyncDisposable
+{
+ ///
+ /// 获得 当前设备连接状态
+ ///
+ bool Connected { get; }
+
+ ///
+ /// 获得 设备 Id
+ ///
+ string? Id { get; }
+
+ ///
+ /// 获得 设备名称
+ ///
+ string? Name { get; }
+
+ ///
+ /// 获得 上次运行错误描述信息
+ ///
+ string? ErrorMessage { get; }
+
+ ///
+ /// 连接方法
+ ///
+ ///
+ Task Connect(CancellationToken token = default);
+
+ ///
+ /// 断开连接方法
+ ///
+ ///
+ Task Disconnect(CancellationToken token = default);
+
+ ///
+ /// 获得设备指定值方法
+ ///
+ /// 比如获得电量方法为 ReadValue("battery_service", "battery_level")
+ ///
+ Task ReadValue(string serviceName, string characteristicName, CancellationToken token = default);
+}
diff --git a/src/BootstrapBlazor/Services/Bluetooth/IBluetoothService.cs b/src/BootstrapBlazor/Services/Bluetooth/IBluetoothService.cs
new file mode 100644
index 00000000000..f7ceadd248f
--- /dev/null
+++ b/src/BootstrapBlazor/Services/Bluetooth/IBluetoothService.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+namespace BootstrapBlazor.Components;
+
+///
+/// 蓝牙服务接口
+///
+public interface IBluetoothService
+{
+ ///
+ /// 获得 浏览器是否支持蓝牙
+ ///
+ bool IsSupport { get; }
+
+ ///
+ /// 获得 是否有蓝牙模块
+ ///
+ bool IsAvailable { get; }
+
+ ///
+ /// 获得 上次运行错误描述信息
+ ///
+ string? ErrorMessage { get; }
+
+ ///
+ /// 获得所有可用串口
+ ///
+ ///
+ Task GetAvailability(CancellationToken token = default);
+
+ ///
+ /// 请求蓝牙配对方法
+ ///
+ /// 请求服务列表 请参考 https://github.com/WebBluetoothCG/registries/blob/master/gatt_assigned_services.txt
+ ///
+ ///
+ Task RequestDevice(string[] optionalServices, CancellationToken token = default);
+}
diff --git a/src/BootstrapBlazor/Services/Serial/DefaultSerialService.cs b/src/BootstrapBlazor/Services/Serial/DefaultSerialService.cs
index 09b6880ceca..c8a5f7b9527 100644
--- a/src/BootstrapBlazor/Services/Serial/DefaultSerialService.cs
+++ b/src/BootstrapBlazor/Services/Serial/DefaultSerialService.cs
@@ -37,13 +37,13 @@ private async Task LoadModule()
/// get the current position of the device
///
///
- public async Task GetPort()
+ public async Task GetPort(CancellationToken token = default)
{
_module ??= await LoadModule();
if (IsSupport)
{
- var ret = await _module.InvokeAsync("getPort", _serialPortId);
+ var ret = await _module.InvokeAsync("getPort", token, _serialPortId);
if (ret)
{
_serialPort = new SerialPort(_module, _serialPortId);
diff --git a/src/BootstrapBlazor/Services/Serial/ISerialPort.cs b/src/BootstrapBlazor/Services/Serial/ISerialPort.cs
index bf983fcbf88..c64f1b2ace4 100644
--- a/src/BootstrapBlazor/Services/Serial/ISerialPort.cs
+++ b/src/BootstrapBlazor/Services/Serial/ISerialPort.cs
@@ -18,13 +18,13 @@ public interface ISerialPort : IAsyncDisposable
/// 关闭端口方法
///
///
- Task Close(CancellationToken token = default);
+ Task Close(CancellationToken token = default);
///
/// 打开端口方法
///
///
- Task Open(SerialPortOptions options, CancellationToken token = default);
+ Task Open(SerialPortOptions options, CancellationToken token = default);
///
/// 接收数据回调方法
diff --git a/src/BootstrapBlazor/Services/Serial/ISerialService.cs b/src/BootstrapBlazor/Services/Serial/ISerialService.cs
index aa5fbeb72e3..570f37da4ff 100644
--- a/src/BootstrapBlazor/Services/Serial/ISerialService.cs
+++ b/src/BootstrapBlazor/Services/Serial/ISerialService.cs
@@ -12,11 +12,11 @@ public interface ISerialService
///
/// 获得/设置 是否支持串口通讯
///
- public bool IsSupport { get; }
+ bool IsSupport { get; }
///
/// 获得所有可用串口
///
///
- Task GetPort();
+ Task GetPort(CancellationToken token = default);
}
diff --git a/src/BootstrapBlazor/Services/Serial/SerialPort.cs b/src/BootstrapBlazor/Services/Serial/SerialPort.cs
index 3ff78f84fa8..ab99feaa481 100644
--- a/src/BootstrapBlazor/Services/Serial/SerialPort.cs
+++ b/src/BootstrapBlazor/Services/Serial/SerialPort.cs
@@ -37,7 +37,7 @@ public async Task Write(byte[] data, CancellationToken token = default)
///
///
///
- public async Task Open(SerialPortOptions options, CancellationToken token = default)
+ public async Task Open(SerialPortOptions options, CancellationToken token = default)
{
DotNetObjectReference? interop = null;
if (DataReceive != null)
@@ -45,19 +45,21 @@ public async Task Open(SerialPortOptions options, CancellationToken token = defa
interop = DotNetObjectReference.Create(this);
}
IsOpen = await jsModule.InvokeAsync("open", token, serialPortId, interop, nameof(DataReceiveCallback), options);
+ return IsOpen;
}
///
///
///
///
- public async Task Close(CancellationToken token = default)
+ public async Task Close(CancellationToken token = default)
{
var ret = await jsModule.InvokeAsync("close", token, serialPortId);
if (ret)
{
IsOpen = false;
}
+ return ret;
}
///
diff --git a/src/BootstrapBlazor/wwwroot/modules/bt.js b/src/BootstrapBlazor/wwwroot/modules/bt.js
new file mode 100644
index 00000000000..aed1d74a616
--- /dev/null
+++ b/src/BootstrapBlazor/wwwroot/modules/bt.js
@@ -0,0 +1,112 @@
+import Data from "./data.js"
+
+export async function init() {
+ return navigator.bluetooth !== void 0;
+}
+
+export async function getAvailability() {
+ let ret = false;
+ if (navigator.bluetooth) {
+ ret = await navigator.bluetooth.getAvailability();
+ }
+ return ret;
+}
+
+export async function requestDevice(id, optionalServices, invoke, method) {
+ let ret = await getAvailability();
+ if (ret === false) {
+ return null;
+ }
+
+ let device = null;
+ const bt = { device: null };
+ Data.set(id, bt);
+ try {
+ const ret = await navigator.bluetooth.requestDevice({
+ acceptAllDevices: true,
+ optionalServices: optionalServices
+ });
+ bt.device = ret;
+ device = [ret.name, ret.id];
+ }
+ catch (err) {
+ invoke.invokeMethodAsync(method, err.toString());
+ console.log(err);
+ }
+ return device;
+}
+
+export async function connect(id, invoke, method) {
+ let ret = false;
+ const bt = Data.get(id);
+ if (bt === null) {
+ return ret;
+ }
+
+ try {
+ const { device } = bt;
+ if (device.gatt.connected === false) {
+ await device.gatt.connect();
+ }
+ ret = true;
+ }
+ catch (err) {
+ invoke.invokeMethodAsync(method, err.toString());
+ console.log(err);
+ }
+ return ret;
+}
+
+export async function readValue(id, serviceName, characteristicName, invoke, method) {
+ let ret = null;
+ const bt = Data.get(id);
+ if (bt === null) {
+ return ret;
+ }
+
+ try {
+ const { device } = bt;
+ const server = device.gatt;
+ if (server.connected === false) {
+ await server.connect();
+ }
+ const service = await server.getPrimaryService(serviceName);
+ const characteristic = await service.getCharacteristic(characteristicName);
+ const dv = await characteristic.readValue();
+ ret = new Uint8Array(dv.byteLength);
+ for (let index = 0; index < dv.byteLength; index++) {
+ ret[index] = dv.getUint8(index);
+ }
+ }
+ catch (err) {
+ invoke.invokeMethodAsync(method, err.toString());
+ console.log(err);
+ }
+ return ret;
+}
+
+export async function disconnect(id, invoke, method) {
+ let ret = false;
+ const bt = Data.get(id);
+ if (bt === null) {
+ return ret;
+ }
+
+ try {
+ const { device } = bt;
+ if (device.gatt.connected === true) {
+ device.gatt.disconnect();
+ }
+ ret = true;
+ }
+ catch (err) {
+ invoke.invokeMethodAsync(method, err.toString());
+ console.log(err);
+ }
+ return ret;
+}
+
+export async function dispose(id) {
+ await disconnect(id);
+ Data.remove(id);
+}
diff --git a/test/UnitTest/Services/BluetoothServiceTest.cs b/test/UnitTest/Services/BluetoothServiceTest.cs
new file mode 100644
index 00000000000..a44551dc1d0
--- /dev/null
+++ b/test/UnitTest/Services/BluetoothServiceTest.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+// Website: https://www.blazor.zone or https://argozhang.github.io/
+
+namespace UnitTest.Services;
+
+public class BluetoothServiceTest : BootstrapBlazorTestBase
+{
+ [Fact]
+ public async Task RequestDevice_Ok()
+ {
+ Context.JSInterop.Setup("init", matcher => matcher.Arguments.Count == 0).SetResult(true);
+ Context.JSInterop.Setup("getAvailability", matcher => matcher.Arguments.Count == 0).SetResult(true);
+ Context.JSInterop.Setup("requestDevice", matcher => matcher.Arguments.Count == 4 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(["test", "id_1234"]);
+ Context.JSInterop.Setup("connect", matcher => matcher.Arguments.Count == 3 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(true);
+ Context.JSInterop.Setup("readValue", matcher => matcher.Arguments.Count == 5 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult([0x31]);
+ Context.JSInterop.Setup("disconnect", matcher => matcher.Arguments.Count == 3 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(true);
+
+ var bluetoothService = Context.Services.GetRequiredService();
+ var device = await bluetoothService.RequestDevice(["battery_service"]);
+ Assert.NotNull(device);
+ Assert.Equal("test", device.Name);
+ Assert.Equal("id_1234", device.Id);
+ Assert.Null(device.ErrorMessage);
+
+ await device.Connect();
+ Assert.True(device.Connected);
+
+ var val = await device.ReadValue("battery_service", "battery_level");
+ Assert.Equal([0x31], val);
+
+ var v = await device.GetBatteryValue();
+ Assert.Equal(0x31, v);
+
+ var mi = device.GetType().GetMethod("OnError");
+ Assert.NotNull(mi);
+ mi.Invoke(device, ["test"]);
+ Assert.Equal("test", device.ErrorMessage);
+
+ await device.Disconnect();
+ Assert.False(device.Connected);
+
+ await device.DisposeAsync();
+ }
+
+ [Fact]
+ public async Task ReadValue_null()
+ {
+ Context.JSInterop.Setup("init", matcher => matcher.Arguments.Count == 0).SetResult(true);
+ Context.JSInterop.Setup("requestDevice", matcher => matcher.Arguments.Count == 4 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(["test", "id_1234"]);
+ Context.JSInterop.Setup("readValue", matcher => matcher.Arguments.Count == 5 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(null);
+ var bluetoothService = Context.Services.GetRequiredService();
+ var device = await bluetoothService.RequestDevice(["battery_service"]);
+ Assert.NotNull(device);
+ var v = await device.GetBatteryValue();
+ Assert.Equal(0x0, v);
+ }
+
+ [Fact]
+ public async Task GetAvailability_Ok()
+ {
+ Context.JSInterop.Setup("init", matcher => matcher.Arguments.Count == 0).SetResult(true);
+ Context.JSInterop.Setup("getAvailability", matcher => matcher.Arguments.Count == 0).SetResult(true);
+ var bluetoothService = Context.Services.GetRequiredService();
+
+ await bluetoothService.GetAvailability();
+ Assert.True(bluetoothService.IsSupport);
+ Assert.True(bluetoothService.IsAvailable);
+ Assert.Null(bluetoothService.ErrorMessage);
+
+ var mi = bluetoothService.GetType().GetMethod("OnError");
+ Assert.NotNull(mi);
+ mi.Invoke(bluetoothService, ["test"]);
+ Assert.Equal("test", bluetoothService.ErrorMessage);
+ }
+}