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 @@

@Localizer["BluetoothTitle"]

- +

@Localizer["BluetoothIntro"]

- -

@((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"]

- - - - -
- @value % -
@message
-
@statusMessage
-
@errorMessage
-
+

注意事项

+

可以通过调用 IBluetoothService 实例方法 GetAvailability 方法后,判断其实例属性 IsAvailable 检查当前终端是否有蓝牙模块

-

@Localizer["BluetoothHeartRate"]

- - - - - -

-
@message
-
@statusMessage
-
@errorMessage
-
+

IBluetoothService 实例属性 IsSupport 是表示当前浏览器是否支持蓝牙功能

+ +

IBluetoothServiceIBluetoothDevice 所有实例方法均有返回值,可通过查看其实例属性 ErrorMessage 获得上一次执行的错误描述信息

+ +

IBluetoothDevice 实例方法 ReadValue 是通用方法,通过参数指定ServicesCharacteristics

- +

原生方法 getDevices 暂未封装,因为需要设置浏览器才能开启

- +

可根据自己的业务需求自定义扩展方法,内置扩展方法列表如下:

- +
    +
  • GetBatteryValue 读取电量方法
  • +
- +

相关文档

- + 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); + } +}