mirror of
https://gitee.com/LongbowEnterprise/BootstrapBlazor.git
synced 2024-11-29 18:49:08 +08:00
feat(BluetoothDevice): add GetCurrentTime method (#4501)
* doc: 更新示例 * feat: 增加 BluetoothDeviceInfo 类 * doc: 更新示例 * feat: 增加蓝牙服务扩展方法 * feat: 增加 SoftwareRevision 参数 * feat: 增加 GetAllServices 扩展方法 * doc: 更新示例 * chore: 增加命名空间配置 * test: 增加测试代码 * feat: 增加 CurrentTime 解析方法 * feat: 增加 GetCurrentTime 方法 * doc: 更新示例 * feat: 支持时区 * doc: 更新注释 * doc: 更新示例 * test: 增加单元测试 * test: 增加单元测试 * doc: 更新示例 * test: 更新单元测试
This commit is contained in:
parent
3f3f8b92d7
commit
c5e1e145f3
@ -217,7 +217,7 @@ visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public
|
||||
[*.cs]
|
||||
# Add file header
|
||||
file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the Apache 2.0 License\nSee the LICENSE file in the project root for more information.\nMaintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
|
@ -21,6 +21,9 @@ private IBluetoothService? BluetoothService { get; set; }</Pre>
|
||||
<DemoBlock Title="@Localizer["BaseUsageTitle"]"
|
||||
Introduction="@Localizer["BaseUsageIntro"]"
|
||||
Name="Normal">
|
||||
<section ignore>
|
||||
@Localizer["UsageDesc"]
|
||||
</section>
|
||||
<div class="row form-inline g-3">
|
||||
<div class="col-12">
|
||||
<Button Text="@Localizer["BluetoothRequestText"]" Icon="fa-brands fa-bluetooth" OnClick="RequestDevice"></Button>
|
||||
@ -36,6 +39,25 @@ private IBluetoothService? BluetoothService { get; set; }</Pre>
|
||||
<label class="d-flex align-items-center"><progress value="@_batteryValue" max="100" class="ms-3"></progress><span class="ms-3">@_batteryValueString</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center">
|
||||
<Button Text="@Localizer["BluetoothGetCurrentTimeText"]" Icon="fa-solid fa-clock" IsDisabled="@(_blueDevice is not { Connected: true })" OnClick="GetTimeValue"></Button>
|
||||
<label class="ms-3">@_currentTimeValueString</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center">
|
||||
<Button Text="@Localizer["BluetoothDeviceInfoText"]" Icon="fa-solid fa-microchip" IsDisabled="@(_blueDevice is not { Connected: true })" OnClick="GetDeviceInfoValue"></Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div>
|
||||
@foreach (var info in _deviceInfoList)
|
||||
{
|
||||
<div>@info</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DemoBlock>
|
||||
|
||||
|
@ -24,9 +24,20 @@ public partial class Bluetooth
|
||||
|
||||
private string? _batteryValueString = null;
|
||||
|
||||
private string? _currentTimeValueString = null;
|
||||
|
||||
private async Task RequestDevice()
|
||||
{
|
||||
_blueDevice = await BluetoothService.RequestDevice();
|
||||
_blueDevice = await BluetoothService.RequestDevice(new BluetoothRequestOptions()
|
||||
{
|
||||
Filters = [
|
||||
new BluetoothFilter()
|
||||
{
|
||||
NamePrefix = "Argo"
|
||||
}
|
||||
],
|
||||
OptionalServices = ["device_information"]
|
||||
});
|
||||
if (BluetoothService.IsSupport == false)
|
||||
{
|
||||
await ToastService.Error(Localizer["NotSupportBluetoothTitle"], Localizer["NotSupportBluetoothContent"]);
|
||||
@ -60,6 +71,11 @@ public partial class Bluetooth
|
||||
{
|
||||
await ToastService.Error("Disconnect", _blueDevice.ErrorMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
_batteryValue = null;
|
||||
_batteryValueString = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,4 +97,36 @@ public partial class Bluetooth
|
||||
_batteryValueString = $"{_batteryValue} %";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GetTimeValue()
|
||||
{
|
||||
_currentTimeValueString = null;
|
||||
|
||||
if(_blueDevice != null)
|
||||
{
|
||||
var val = await _blueDevice.GetCurrentTime();
|
||||
if (val.HasValue && !string.IsNullOrEmpty(_blueDevice.ErrorMessage))
|
||||
{
|
||||
await ToastService.Error("Current Time", _blueDevice.ErrorMessage);
|
||||
return;
|
||||
}
|
||||
_currentTimeValueString = val.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<string> _deviceInfoList = [];
|
||||
|
||||
private async Task GetDeviceInfoValue()
|
||||
{
|
||||
_deviceInfoList.Clear();
|
||||
if (_blueDevice != null)
|
||||
{
|
||||
var info = await _blueDevice.GetDeviceInfo();
|
||||
_deviceInfoList.Add($"Manufacturer Name: {info?.ManufacturerName}");
|
||||
_deviceInfoList.Add($"Module Number: {info?.ModelNumber}");
|
||||
_deviceInfoList.Add($"Firmware Revision: {info?.FirmwareRevision}");
|
||||
_deviceInfoList.Add($"Hardware Revision: {info?.HardwareRevision}");
|
||||
_deviceInfoList.Add($"Software Revision: {info?.SoftwareRevision}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5901,7 +5901,10 @@
|
||||
"BluetoothGetBatteryText": "Battery",
|
||||
"BluetoothGetHeartRateText": "HeartRate",
|
||||
"BaseUsageTitle": "Basic usage",
|
||||
"BaseUsageIntro": "Request communication with Bluetooth devices through the <code>IBluetoothService</code> service"
|
||||
"BaseUsageIntro": "Request communication with Bluetooth devices through the <code>IBluetoothService</code> service",
|
||||
"BluetoothGetCurrentTimeText": "Time",
|
||||
"BluetoothDeviceInfoText": "Device Info",
|
||||
"UsageDesc": "Click the Scan button and select the phone to test in the pop-up window"
|
||||
},
|
||||
"BootstrapBlazor.Server.Components.Samples.FileIcons": {
|
||||
"Title": "File Icon",
|
||||
|
@ -5901,7 +5901,10 @@
|
||||
"BluetoothGetBatteryText": "读取电量",
|
||||
"BluetoothGetHeartRateText": "读取心率",
|
||||
"BaseUsageTitle": "基础用法",
|
||||
"BaseUsageIntro": "通过 <code>IBluetoothService</code> 服务,请求与蓝牙设备通讯"
|
||||
"BaseUsageIntro": "通过 <code>IBluetoothService</code> 服务,请求与蓝牙设备通讯",
|
||||
"BluetoothGetCurrentTimeText": "读取时间",
|
||||
"BluetoothDeviceInfoText": "读取硬件信息",
|
||||
"UsageDesc": "点击扫描按钮,在弹窗中选中手机进行测试"
|
||||
},
|
||||
"BootstrapBlazor.Server.Components.Samples.FileIcons": {
|
||||
"Title": "File Icon 文件图标",
|
||||
|
27
src/BootstrapBlazor/Extensions/BluetoothExtensions.cs
Normal file
27
src/BootstrapBlazor/Extensions/BluetoothExtensions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the Apache 2.0 License
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BootstrapBlazor.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Bluetooth 扩展方法
|
||||
/// </summary>
|
||||
public static class BluetoothExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获得指定蓝牙服务字符串集合
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static List<string> GetServicesList(this IEnumerable<BluetoothServices> services) => services.Select(i =>
|
||||
{
|
||||
var v = i.ToString();
|
||||
var attributes = typeof(BluetoothServices).GetField(v)!.GetCustomAttribute<JsonPropertyNameAttribute>(false)!;
|
||||
return attributes.Name;
|
||||
}).ToList();
|
||||
}
|
@ -3,6 +3,8 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace BootstrapBlazor.Components;
|
||||
|
||||
/// <summary>
|
||||
@ -97,6 +99,41 @@ sealed class BluetoothDevice : IBluetoothDevice
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc />
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<BluetoothDeviceInfo?> GetDeviceInfo(CancellationToken token = default)
|
||||
{
|
||||
BluetoothDeviceInfo? ret = null;
|
||||
if (Connected)
|
||||
{
|
||||
ErrorMessage = null;
|
||||
ret = await _module.InvokeAsync<BluetoothDeviceInfo?>("getDeviceInfo", token, _clientId, _interop, nameof(OnError));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc />
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<DateTimeOffset?> GetCurrentTime(CancellationToken token = default)
|
||||
{
|
||||
DateTimeOffset? ret = null;
|
||||
if (Connected)
|
||||
{
|
||||
ErrorMessage = null;
|
||||
var timeString = await _module.InvokeAsync<string?>("getCurrentTime", token, _clientId, _interop, nameof(OnError));
|
||||
if (DateTimeOffset.TryParseExact(timeString, "yyyy-MM-ddTHH:mm:sszzz", DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var d))
|
||||
{
|
||||
ret = d;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
/// <summary>
|
||||
/// JavaScript 报错回调方法
|
||||
/// </summary>
|
||||
|
@ -0,0 +1,88 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// 蓝牙设备信息
|
||||
/// </summary>
|
||||
public class BluetoothDeviceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 获得/设置 ManufacturerName
|
||||
/// </summary>
|
||||
public string? ManufacturerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 ModelNumber
|
||||
/// </summary>
|
||||
public string? ModelNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 HardwareRevision
|
||||
/// </summary>
|
||||
public string? HardwareRevision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 FirmwareRevision
|
||||
/// </summary>
|
||||
public string? FirmwareRevision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 SoftwareRevision
|
||||
/// </summary>
|
||||
public string? SoftwareRevision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 SystemId
|
||||
/// </summary>
|
||||
public SystemId? SystemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 IEEERegulatoryCertificationDataList
|
||||
/// </summary>
|
||||
public string? IEEERegulatoryCertificationDataList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 PnPID
|
||||
/// </summary>
|
||||
public PnPID? PnPID { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SystemId 类
|
||||
/// </summary>
|
||||
public class SystemId
|
||||
{
|
||||
/// <summary>
|
||||
/// 获得/设置 ManufacturerIdentifier
|
||||
/// </summary>
|
||||
public string? ManufacturerIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 OrganizationallyUniqueIdentifier
|
||||
/// </summary>
|
||||
public string? OrganizationallyUniqueIdentifier { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PnPID 类
|
||||
/// </summary>
|
||||
public class PnPID
|
||||
{
|
||||
/// <summary>
|
||||
/// 获得/设置 VendorIdSource
|
||||
/// </summary>
|
||||
public string? VendorIdSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 ProductId
|
||||
/// </summary>
|
||||
public string? ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 ProductVersion
|
||||
/// </summary>
|
||||
public string? ProductVersion { get; set; }
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BootstrapBlazor.Components;
|
||||
@ -39,6 +40,18 @@ public class BluetoothRequestOptions
|
||||
/// <summary>
|
||||
/// A boolean value indicating that the requesting script can accept all Bluetooth devices. The default is false.
|
||||
/// </summary>
|
||||
/// <remarks>This option is appropriate when devices have not advertised enough information for filtering to be useful. When acceptAllDevices is set to true you should omit all filters and exclusionFilters, and you must set optionalServices to be able to use the returned device.</remarks>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public bool AcceptAllDevices { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得所有蓝牙服务
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static List<string> GetAllServices() => Enum.GetNames(typeof(BluetoothServices)).Select(i =>
|
||||
{
|
||||
var v = i.ToString();
|
||||
var attributes = typeof(BluetoothServices).GetField(v)!.GetCustomAttribute<JsonPropertyNameAttribute>(false)!;
|
||||
return attributes.Name;
|
||||
}).ToList();
|
||||
}
|
||||
|
250
src/BootstrapBlazor/Services/Bluetooth/BluetoothServices.cs
Normal file
250
src/BootstrapBlazor/Services/Bluetooth/BluetoothServices.cs
Normal file
@ -0,0 +1,250 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the Apache 2.0 License
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
|
||||
|
||||
using BootstrapBlazor.Core.Converter;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BootstrapBlazor.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Bluetooth 设备服务枚举
|
||||
/// </summary>
|
||||
[JsonEnumConverter]
|
||||
public enum BluetoothServices
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用访问
|
||||
/// </summary>
|
||||
[JsonPropertyName("generic_access")]
|
||||
GenericAccess,
|
||||
|
||||
/// <summary>
|
||||
/// 通用属性
|
||||
/// </summary>
|
||||
[JsonPropertyName("generic_attribute")]
|
||||
GenericAttribute,
|
||||
|
||||
/// <summary>
|
||||
/// 即时闹钟
|
||||
/// </summary>
|
||||
[JsonPropertyName("immediate_alert")]
|
||||
ImmediateAlert,
|
||||
|
||||
/// <summary>
|
||||
/// 连接丢失
|
||||
/// </summary>
|
||||
[JsonPropertyName("link_loss")]
|
||||
LinkLoss,
|
||||
|
||||
/// <summary>
|
||||
/// 发送功率
|
||||
/// </summary>
|
||||
[JsonPropertyName("tx_power")]
|
||||
TXPower,
|
||||
|
||||
/// <summary>
|
||||
/// 当前时间
|
||||
/// </summary>
|
||||
[JsonPropertyName("current_time")]
|
||||
CurrentTime,
|
||||
|
||||
/// <summary>
|
||||
/// 参照时间更新
|
||||
/// </summary>
|
||||
[JsonPropertyName("reference_time_update")]
|
||||
ReferenceTimeUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// 下个日光节约时间(夏令时)更改
|
||||
/// </summary>
|
||||
[JsonPropertyName("next_dst_change")]
|
||||
NextDstChange,
|
||||
|
||||
/// <summary>
|
||||
/// 葡萄糖
|
||||
/// </summary>
|
||||
[JsonPropertyName("glucose")]
|
||||
Glucose,
|
||||
|
||||
/// <summary>
|
||||
/// 温度计
|
||||
/// </summary>
|
||||
[JsonPropertyName("health_thermometer")]
|
||||
HealthThermometer,
|
||||
|
||||
/// <summary>
|
||||
/// 设备信息
|
||||
/// </summary>
|
||||
[JsonPropertyName("device_information")]
|
||||
DeviceInformation,
|
||||
|
||||
/// <summary>
|
||||
/// 心率
|
||||
/// </summary>
|
||||
[JsonPropertyName("heart_rate")]
|
||||
HeartRate,
|
||||
|
||||
/// <summary>
|
||||
/// 手机报警状态
|
||||
/// </summary>
|
||||
[JsonPropertyName("phone_alert_status")]
|
||||
PhoneAlertStatus,
|
||||
|
||||
/// <summary>
|
||||
/// 电池数据
|
||||
/// </summary>
|
||||
[JsonPropertyName("battery_service")]
|
||||
BatteryService,
|
||||
|
||||
/// <summary>
|
||||
/// 血压
|
||||
/// </summary>
|
||||
[JsonPropertyName("blood_pressure")]
|
||||
BloodPressure,
|
||||
|
||||
/// <summary>
|
||||
/// 闹钟通知
|
||||
/// </summary>
|
||||
[JsonPropertyName("alert_notification")]
|
||||
AlertNotification,
|
||||
|
||||
/// <summary>
|
||||
/// HID设备
|
||||
/// </summary>
|
||||
[JsonPropertyName("human_interface_device")]
|
||||
HumanInterfaceDevice,
|
||||
|
||||
/// <summary>
|
||||
/// 扫描参数
|
||||
/// </summary>
|
||||
[JsonPropertyName("scan_parameters")]
|
||||
ScanParameters,
|
||||
|
||||
/// <summary>
|
||||
/// 跑步速度、节奏
|
||||
/// </summary>
|
||||
[JsonPropertyName("running_speed_and_cadence")]
|
||||
RunningSpeedAndCadence,
|
||||
|
||||
/// <summary>
|
||||
/// 自动化输入输出
|
||||
/// </summary>
|
||||
[JsonPropertyName("automation_io")]
|
||||
AutomationIO,
|
||||
|
||||
/// <summary>
|
||||
/// 循环速度、节奏
|
||||
/// </summary>
|
||||
[JsonPropertyName("cycling_speed_and_cadence")]
|
||||
CyclingSpeedAndCadence,
|
||||
|
||||
/// <summary>
|
||||
/// 骑行能量
|
||||
/// </summary>
|
||||
[JsonPropertyName("cycling_power")]
|
||||
CyclingPower,
|
||||
|
||||
/// <summary>
|
||||
/// 定位及导航
|
||||
/// </summary>
|
||||
[JsonPropertyName("location_and_navigation")]
|
||||
LocationAndNavigation,
|
||||
|
||||
/// <summary>
|
||||
/// 环境传感
|
||||
/// </summary>
|
||||
[JsonPropertyName("environmental_sensing")]
|
||||
EnvironmentalSensing,
|
||||
|
||||
/// <summary>
|
||||
/// 身体组成
|
||||
/// </summary>
|
||||
[JsonPropertyName("body_composition")]
|
||||
BodyComposition,
|
||||
|
||||
/// <summary>
|
||||
/// 用户数据
|
||||
/// </summary>
|
||||
[JsonPropertyName("user_data")]
|
||||
UserData,
|
||||
|
||||
/// <summary>
|
||||
/// 体重秤
|
||||
/// </summary>
|
||||
[JsonPropertyName("weight_scale")]
|
||||
WeightScale,
|
||||
|
||||
/// <summary>
|
||||
/// 设备绑定管理
|
||||
/// </summary>
|
||||
[JsonPropertyName("bond_management")]
|
||||
BondManagement,
|
||||
|
||||
/// <summary>
|
||||
/// 动态血糖检测
|
||||
/// </summary>
|
||||
[JsonPropertyName("continuous_glucose_monitoring")]
|
||||
ContinuousGlucoseMonitoring,
|
||||
|
||||
/// <summary>
|
||||
/// 互联网协议支持
|
||||
/// </summary>
|
||||
[JsonPropertyName("internet_protocol_support")]
|
||||
InternetProtocolSupport,
|
||||
|
||||
/// <summary>
|
||||
/// 室内定位
|
||||
/// </summary>
|
||||
[JsonPropertyName("indoor_positioning")]
|
||||
IndoorPositioning,
|
||||
|
||||
/// <summary>
|
||||
/// 脉搏血氧计
|
||||
/// </summary>
|
||||
[JsonPropertyName("pulse_oximeter")]
|
||||
PulseOximeter,
|
||||
|
||||
/// <summary>
|
||||
/// HTTP代理
|
||||
/// </summary>
|
||||
[JsonPropertyName("http_proxy")]
|
||||
HttpProxy,
|
||||
|
||||
/// <summary>
|
||||
/// 传输发现
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport_discovery")]
|
||||
TransportDiscovery,
|
||||
|
||||
/// <summary>
|
||||
/// 对象传输
|
||||
/// </summary>
|
||||
[JsonPropertyName("object_transfer")]
|
||||
ObjectTransfer,
|
||||
|
||||
/// <summary>
|
||||
/// 健康设备
|
||||
/// </summary>
|
||||
[JsonPropertyName("fitness_machine")]
|
||||
FitnessMachine,
|
||||
|
||||
/// <summary>
|
||||
/// 节点配置
|
||||
/// </summary>
|
||||
[JsonPropertyName("mesh_provisioning")]
|
||||
MeshProvisioning,
|
||||
|
||||
/// <summary>
|
||||
/// 节点代理
|
||||
/// </summary>
|
||||
[JsonPropertyName("mesh_proxy")]
|
||||
MeshProxy,
|
||||
|
||||
/// <summary>
|
||||
/// 重连配置
|
||||
/// </summary>
|
||||
[JsonPropertyName("reconnection_configuration")]
|
||||
ReconnectionConfiguration
|
||||
}
|
@ -88,6 +88,18 @@ sealed class DefaultBluetoothService : IBluetoothService
|
||||
return device;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc />
|
||||
/// </summary>
|
||||
/// <param name="optionalServices"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public Task<IBluetoothDevice?> RequestDevice(List<string> optionalServices, CancellationToken token = default)
|
||||
{
|
||||
var options = new BluetoothRequestOptions() { AcceptAllDevices = true, OptionalServices = optionalServices };
|
||||
return RequestDevice(options, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JavaScript 报错回调方法
|
||||
/// </summary>
|
||||
|
@ -48,4 +48,18 @@ public interface IBluetoothDevice : IAsyncDisposable
|
||||
/// <remarks>比如获得电量方法为 ReadValue("battery_service", "battery_level")</remarks>
|
||||
/// <returns></returns>
|
||||
Task<byte[]?> ReadValue(string serviceName, string characteristicName, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获得设备信息方法
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<BluetoothDeviceInfo?> GetDeviceInfo(CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获得设备当前时间方法
|
||||
/// </summary>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<DateTimeOffset?> GetCurrentTime(CancellationToken token = default);
|
||||
}
|
||||
|
@ -38,4 +38,12 @@ public interface IBluetoothService
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<IBluetoothDevice?> RequestDevice(BluetoothRequestOptions? options = null, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// 请求蓝牙配对方法
|
||||
/// </summary>
|
||||
/// <param name="optionalServices"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<IBluetoothDevice?> RequestDevice(List<string> optionalServices, CancellationToken token = default);
|
||||
}
|
||||
|
@ -56,6 +56,149 @@ export async function connect(id, invoke, method) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function getDeviceInfo(id, 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('device_information');
|
||||
const characteristics = await service.getCharacteristics();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
ret = {};
|
||||
let dv = null;
|
||||
for (const characteristic of characteristics) {
|
||||
switch (characteristic.uuid) {
|
||||
case BluetoothUUID.getCharacteristic('manufacturer_name_string'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.ManufacturerName = decoder.decode(dv);
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('model_number_string'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.ModelNumber = decoder.decode(dv);
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('hardware_revision_string'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.HardwareRevision = decoder.decode(dv);
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('firmware_revision_string'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.FirmwareRevision = decoder.decode(dv);
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('software_revision_string'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.SoftwareRevision = decoder.decode(dv);
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('system_id'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.SystemId = {
|
||||
ManufacturerIdentifier: padHex(dv.getUint8(4)) + padHex(dv.getUint8(3)) +
|
||||
padHex(dv.getUint8(2)) + padHex(dv.getUint8(1)) +
|
||||
padHex(dv.getUint8(0)),
|
||||
OrganizationallyUniqueIdentifier: padHex(dv.getUint8(7)) + padHex(dv.getUint8(6)) +
|
||||
padHex(dv.getUint8(5))
|
||||
}
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('ieee_11073-20601_regulatory_certification_data_list'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.IEEERegulatoryCertificationDataList = decoder.decode(dv);
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('pnp_id'):
|
||||
dv = await characteristic.readValue();
|
||||
ret.PnPID = {
|
||||
VendorIdSource: dv.getUint8(0) === 1 ? 'Bluetooth' : 'USB',
|
||||
ProductId: dv.getUint8(3) | dv.getUint8(4) << 8,
|
||||
ProductVersion: dv.getUint8(5) | dv.getUint8(6) << 8,
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown Characteristic: ' + characteristic.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
invoke.invokeMethodAsync(method, err.toString());
|
||||
console.log(err);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function getCurrentTime(id, 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('current_time');
|
||||
const characteristics = await service.getCharacteristics();
|
||||
let zone = 0;
|
||||
let dt = null;
|
||||
for (const characteristic of characteristics) {
|
||||
console.log(characteristic);
|
||||
|
||||
switch (characteristic.uuid) {
|
||||
case BluetoothUUID.getCharacteristic('local_time_information'):
|
||||
let dv = await characteristic.readValue();
|
||||
zone = dv.getUint8(0) - 12;
|
||||
break;
|
||||
|
||||
case BluetoothUUID.getCharacteristic('current_time'):
|
||||
let dv = await characteristic.readValue();
|
||||
const year = dv.getUint16(0, true);
|
||||
const month = dv.getUint8(2);
|
||||
const day = dv.getUint8(3);
|
||||
const hours = dv.getUint8(4);
|
||||
const minutes = dv.getUint8(5);
|
||||
const seconds = dv.getUint8(6);
|
||||
dt = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown Characteristic: ' + characteristic.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
if (dt) {
|
||||
ret = `${dt}${getZonePrefix(zone)}${zone}:00`;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
invoke.invokeMethodAsync(method, err.toString());
|
||||
console.log(err);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const getZonePrefix = zone => zone >= 0 ? "+" : "-";
|
||||
|
||||
const padHex = value => {
|
||||
return ('00' + value.toString(16).toUpperCase()).slice(-2);
|
||||
}
|
||||
|
||||
export async function readValue(id, serviceName, characteristicName, invoke, method) {
|
||||
let ret = null;
|
||||
const bt = Data.get(id);
|
||||
@ -69,6 +212,7 @@ export async function readValue(id, serviceName, characteristicName, invoke, met
|
||||
if (server.connected === false) {
|
||||
await server.connect();
|
||||
}
|
||||
|
||||
const service = await server.getPrimaryService(serviceName);
|
||||
const characteristic = await service.getCharacteristic(characteristicName);
|
||||
const dv = await characteristic.readValue();
|
||||
|
@ -3,6 +3,8 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
namespace UnitTest.Services;
|
||||
|
||||
public class BluetoothServiceTest : BootstrapBlazorTestBase
|
||||
@ -18,7 +20,9 @@ public class BluetoothServiceTest : BootstrapBlazorTestBase
|
||||
Context.JSInterop.Setup<bool>("disconnect", matcher => matcher.Arguments.Count == 3 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(true);
|
||||
|
||||
var bluetoothService = Context.Services.GetRequiredService<IBluetoothService>();
|
||||
var device = await bluetoothService.RequestDevice();
|
||||
|
||||
var services = new List<BluetoothServices>() { BluetoothServices.DeviceInformation };
|
||||
var device = await bluetoothService.RequestDevice(services.GetServicesList());
|
||||
Assert.NotNull(device);
|
||||
Assert.Equal("test", device.Name);
|
||||
Assert.Equal("id_1234", device.Id);
|
||||
@ -135,4 +139,89 @@ public class BluetoothServiceTest : BootstrapBlazorTestBase
|
||||
Assert.Equal(["test-manufacturer-data"], filter.OptionalManufacturerData);
|
||||
Assert.Equal(["test-optional-service"], filter.OptionalServices);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DateTimeOffset_Ok()
|
||||
{
|
||||
var val = "2018-12-04T13:53:42+07:00";
|
||||
Assert.True(DateTimeOffset.TryParseExact(val, "yyyy-MM-ddTHH:mm:sszzz", DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var d));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDeviceInfo_null()
|
||||
{
|
||||
Context.JSInterop.Setup<bool>("init", matcher => matcher.Arguments.Count == 0).SetResult(true);
|
||||
Context.JSInterop.Setup<string[]?>("requestDevice", matcher => matcher.Arguments.Count == 4 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(["test", "id_1234"]);
|
||||
Context.JSInterop.Setup<bool>("connect", matcher => matcher.Arguments.Count == 3 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(true);
|
||||
Context.JSInterop.Setup<BluetoothDeviceInfo?>("getDeviceInfo", matcher => matcher.Arguments.Count == 3 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(new BluetoothDeviceInfo() { ManufacturerName = "test" });
|
||||
|
||||
var bluetoothService = Context.Services.GetRequiredService<IBluetoothService>();
|
||||
var device = await bluetoothService.RequestDevice();
|
||||
Assert.NotNull(device);
|
||||
|
||||
await device.Connect();
|
||||
var v = await device.GetDeviceInfo();
|
||||
Assert.Equal("test", v?.ManufacturerName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCurrentTime_null()
|
||||
{
|
||||
Context.JSInterop.Setup<bool>("init", matcher => matcher.Arguments.Count == 0).SetResult(true);
|
||||
Context.JSInterop.Setup<string[]?>("requestDevice", matcher => matcher.Arguments.Count == 4 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(["test", "id_1234"]);
|
||||
Context.JSInterop.Setup<bool>("connect", matcher => matcher.Arguments.Count == 3 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult(true);
|
||||
Context.JSInterop.Setup<string?>("getCurrentTime", matcher => matcher.Arguments.Count == 3 && (matcher.Arguments[0]?.ToString()?.StartsWith("bb_bt_") ?? false)).SetResult("2024-10-10T10:05:10+07:00");
|
||||
|
||||
var bluetoothService = Context.Services.GetRequiredService<IBluetoothService>();
|
||||
var device = await bluetoothService.RequestDevice();
|
||||
Assert.NotNull(device);
|
||||
|
||||
await device.Connect();
|
||||
var v = await device.GetCurrentTime();
|
||||
Assert.Equal("2024-10-10 10:05:10", v.Value.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
Assert.Equal(7, v.Value.Offset.TotalHours);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BluetoothDeviceInfo_Ok()
|
||||
{
|
||||
var info = new BluetoothDeviceInfo()
|
||||
{
|
||||
FirmwareRevision = "test",
|
||||
HardwareRevision = "test",
|
||||
IEEERegulatoryCertificationDataList = "test",
|
||||
ManufacturerName = "test",
|
||||
ModelNumber = "test",
|
||||
SoftwareRevision = "test",
|
||||
SystemId = new SystemId()
|
||||
{
|
||||
ManufacturerIdentifier = "test",
|
||||
OrganizationallyUniqueIdentifier = "test",
|
||||
},
|
||||
PnPID = new PnPID()
|
||||
{
|
||||
ProductId = "test",
|
||||
ProductVersion = "test",
|
||||
VendorIdSource = "test",
|
||||
}
|
||||
};
|
||||
Assert.Equal("test", info.FirmwareRevision);
|
||||
Assert.Equal("test", info.HardwareRevision);
|
||||
Assert.Equal("test", info.SoftwareRevision);
|
||||
Assert.Equal("test", info.IEEERegulatoryCertificationDataList);
|
||||
Assert.Equal("test", info.ManufacturerName);
|
||||
Assert.Equal("test", info.ModelNumber);
|
||||
Assert.Equal("test", info.SystemId.ManufacturerIdentifier);
|
||||
Assert.Equal("test", info.SystemId.OrganizationallyUniqueIdentifier);
|
||||
Assert.Equal("test", info.PnPID.ProductId);
|
||||
Assert.Equal("test", info.PnPID.ProductVersion);
|
||||
Assert.Equal("test", info.PnPID.VendorIdSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAllServices_Ok()
|
||||
{
|
||||
var services = BluetoothRequestOptions.GetAllServices();
|
||||
Assert.True(services.Count > 0);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user