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:
Argo Zhang 2024-10-22 01:01:29 -07:00 committed by GitHub
parent 3f3f8b92d7
commit c5e1e145f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 763 additions and 5 deletions

View File

@ -217,7 +217,7 @@ visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public
[*.cs] [*.cs]
# Add file header # 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 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_style_expression_bodied_local_functions = false:silent
csharp_using_directive_placement = outside_namespace:silent csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_simple_using_statement = true:suggestion

View File

@ -21,6 +21,9 @@ private IBluetoothService? BluetoothService { get; set; }</Pre>
<DemoBlock Title="@Localizer["BaseUsageTitle"]" <DemoBlock Title="@Localizer["BaseUsageTitle"]"
Introduction="@Localizer["BaseUsageIntro"]" Introduction="@Localizer["BaseUsageIntro"]"
Name="Normal"> Name="Normal">
<section ignore>
@Localizer["UsageDesc"]
</section>
<div class="row form-inline g-3"> <div class="row form-inline g-3">
<div class="col-12"> <div class="col-12">
<Button Text="@Localizer["BluetoothRequestText"]" Icon="fa-brands fa-bluetooth" OnClick="RequestDevice"></Button> <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> <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> </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> </div>
</DemoBlock> </DemoBlock>

View File

@ -24,9 +24,20 @@ public partial class Bluetooth
private string? _batteryValueString = null; private string? _batteryValueString = null;
private string? _currentTimeValueString = null;
private async Task RequestDevice() 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) if (BluetoothService.IsSupport == false)
{ {
await ToastService.Error(Localizer["NotSupportBluetoothTitle"], Localizer["NotSupportBluetoothContent"]); await ToastService.Error(Localizer["NotSupportBluetoothTitle"], Localizer["NotSupportBluetoothContent"]);
@ -60,6 +71,11 @@ public partial class Bluetooth
{ {
await ToastService.Error("Disconnect", _blueDevice.ErrorMessage); await ToastService.Error("Disconnect", _blueDevice.ErrorMessage);
} }
else
{
_batteryValue = null;
_batteryValueString = null;
}
} }
} }
@ -81,4 +97,36 @@ public partial class Bluetooth
_batteryValueString = $"{_batteryValue} %"; _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}");
}
}
} }

View File

@ -5901,7 +5901,10 @@
"BluetoothGetBatteryText": "Battery", "BluetoothGetBatteryText": "Battery",
"BluetoothGetHeartRateText": "HeartRate", "BluetoothGetHeartRateText": "HeartRate",
"BaseUsageTitle": "Basic usage", "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": { "BootstrapBlazor.Server.Components.Samples.FileIcons": {
"Title": "File Icon", "Title": "File Icon",

View File

@ -5901,7 +5901,10 @@
"BluetoothGetBatteryText": "读取电量", "BluetoothGetBatteryText": "读取电量",
"BluetoothGetHeartRateText": "读取心率", "BluetoothGetHeartRateText": "读取心率",
"BaseUsageTitle": "基础用法", "BaseUsageTitle": "基础用法",
"BaseUsageIntro": "通过 <code>IBluetoothService</code> 服务,请求与蓝牙设备通讯" "BaseUsageIntro": "通过 <code>IBluetoothService</code> 服务,请求与蓝牙设备通讯",
"BluetoothGetCurrentTimeText": "读取时间",
"BluetoothDeviceInfoText": "读取硬件信息",
"UsageDesc": "点击扫描按钮,在弹窗中选中手机进行测试"
}, },
"BootstrapBlazor.Server.Components.Samples.FileIcons": { "BootstrapBlazor.Server.Components.Samples.FileIcons": {
"Title": "File Icon 文件图标", "Title": "File Icon 文件图标",

View 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();
}

View File

@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using System.Globalization;
namespace BootstrapBlazor.Components; namespace BootstrapBlazor.Components;
/// <summary> /// <summary>
@ -97,6 +99,41 @@ sealed class BluetoothDevice : IBluetoothDevice
return ret; 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> /// <summary>
/// JavaScript 报错回调方法 /// JavaScript 报错回调方法
/// </summary> /// </summary>

View File

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

View File

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using System.Reflection;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace BootstrapBlazor.Components; namespace BootstrapBlazor.Components;
@ -39,6 +40,18 @@ public class BluetoothRequestOptions
/// <summary> /// <summary>
/// A boolean value indicating that the requesting script can accept all Bluetooth devices. The default is false. /// A boolean value indicating that the requesting script can accept all Bluetooth devices. The default is false.
/// </summary> /// </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)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool AcceptAllDevices { get; set; } 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();
} }

View 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
}

View File

@ -88,6 +88,18 @@ sealed class DefaultBluetoothService : IBluetoothService
return device; 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> /// <summary>
/// JavaScript 报错回调方法 /// JavaScript 报错回调方法
/// </summary> /// </summary>

View File

@ -48,4 +48,18 @@ public interface IBluetoothDevice : IAsyncDisposable
/// <remarks>比如获得电量方法为 ReadValue("battery_service", "battery_level")</remarks> /// <remarks>比如获得电量方法为 ReadValue("battery_service", "battery_level")</remarks>
/// <returns></returns> /// <returns></returns>
Task<byte[]?> ReadValue(string serviceName, string characteristicName, CancellationToken token = default); 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);
} }

View File

@ -38,4 +38,12 @@ public interface IBluetoothService
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IBluetoothDevice?> RequestDevice(BluetoothRequestOptions? options = null, CancellationToken token = default); 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);
} }

View File

@ -56,6 +56,149 @@ export async function connect(id, invoke, method) {
return ret; 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) { export async function readValue(id, serviceName, characteristicName, invoke, method) {
let ret = null; let ret = null;
const bt = Data.get(id); const bt = Data.get(id);
@ -69,6 +212,7 @@ export async function readValue(id, serviceName, characteristicName, invoke, met
if (server.connected === false) { if (server.connected === false) {
await server.connect(); await server.connect();
} }
const service = await server.getPrimaryService(serviceName); const service = await server.getPrimaryService(serviceName);
const characteristic = await service.getCharacteristic(characteristicName); const characteristic = await service.getCharacteristic(characteristicName);
const dv = await characteristic.readValue(); const dv = await characteristic.readValue();

View File

@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
using System.Globalization;
namespace UnitTest.Services; namespace UnitTest.Services;
public class BluetoothServiceTest : BootstrapBlazorTestBase 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); 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 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.NotNull(device);
Assert.Equal("test", device.Name); Assert.Equal("test", device.Name);
Assert.Equal("id_1234", device.Id); Assert.Equal("id_1234", device.Id);
@ -135,4 +139,89 @@ public class BluetoothServiceTest : BootstrapBlazorTestBase
Assert.Equal(["test-manufacturer-data"], filter.OptionalManufacturerData); Assert.Equal(["test-manufacturer-data"], filter.OptionalManufacturerData);
Assert.Equal(["test-optional-service"], filter.OptionalServices); 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);
}
} }