使用C#编写一个用于接收Amazfit Balance智能手表的心率广播数据的程序
前言
智能手表通过蓝牙低能耗(BLE)广播心率数据,因此我们可以使用C#的蓝牙库来扫描和接收Amazfit Balance智能手表广播数据。
为什么是Amazfit Balance智能手表
因为本人目前只有Amazfit Balance智能手表,哈哈。理论上只要手表或手环支持BLE心率广播功能,且完全符合公开的标准,均可以使用此手法来接收解码广播数据。其他品牌各位看官可以自己动手尝试下。
项目新建
- 新建一个.net 9控制台项目
- 修改
.csproj
把TargetFramework
内容修改为
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
因为需要使用Windows SDK的专有命名空间Windows.Devices.Bluetooth
来访问蓝牙,所以指定为windows独占,暂时无法跨平台。
程序本体
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Storage.Streams;
namespace AmazfitHeartRateReceiver;
internal class Program
{
// 用于存储检测到的手表设备
private static readonly Dictionary<ulong, BluetoothLEDevice> devices = [];
static void Main(string[] args)
{
Console.WriteLine("正在启动Amazfit Balance心率接收程序...");
StartHeartRateScanner();
Console.ReadLine(); // 保持程序运行
}
static void StartHeartRateScanner()
{
var watcher = new BluetoothLEAdvertisementWatcher
{
ScanningMode = BluetoothLEScanningMode.Active
};
// 添加心率服务过滤
watcher.AdvertisementFilter.Advertisement.ServiceUuids.Add(BluetoothUuidHelper.FromShortId(0x180D));
watcher.Received += async (sender, args) =>
{
try
{
// 获取蓝牙设备
var device = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
if (device == null) return;
// 检查是否已处理过该设备
if (devices.ContainsKey(device.BluetoothAddress)) return;
devices.Add(device.BluetoothAddress, device);
Console.WriteLine($"检测到设备: {device.Name} ({device.BluetoothAddress:X})");
// 获取心率服务
var hrService = (await device.GetGattServicesForUuidAsync(BluetoothUuidHelper.FromShortId(0x180D))).Services[0];
if (hrService == null) return;
// 获取心率特征值
var hrCharacteristic = (await hrService.GetCharacteristicsForUuidAsync(BluetoothUuidHelper.FromShortId(0x2A37))).Characteristics[0];
if (hrCharacteristic == null) return;
// 启用特征值通知
hrCharacteristic.ValueChanged += HeartRateValueChanged;
await hrCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
GattClientCharacteristicConfigurationDescriptorValue.Notify);
Console.WriteLine("已订阅心率数据通知");
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
}
};
watcher.Start();
Console.WriteLine("扫描已启动,等待Amazfit Balance手表广播...");
}
private static void HeartRateValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
{
try
{
using var reader = DataReader.FromBuffer(args.CharacteristicValue);
var heartRate = ParseHeartRateValue(reader);
Console.WriteLine($"[{DateTime.Now:T}] 心率: {heartRate} BPM");
}
catch (Exception ex)
{
Console.WriteLine($"解析错误: {ex.Message}");
}
}
private static int ParseHeartRateValue(DataReader reader)
{
reader.ByteOrder = ByteOrder.LittleEndian;
// 读取标志位
byte flags = reader.ReadByte();
bool is16bit = (flags & 0x01) != 0;
return is16bit ? reader.ReadUInt16() : reader.ReadByte();
}
}
程序说明
- 工作原理
- 使用蓝牙LE广播扫描发现附近设备
- 通过心率服务UUID(0x180D)过滤设备
- 订阅心率特征值(0x2A37)的通知
- 解析心率数据并实时显示
- 关于其他的UUID和特征值,可以到蓝牙技术联盟官网上面找
- 关键组件
BluetoothLEAdvertisementWatcher
:扫描BLE设备广播GattCharacteristic.ValueChanged
:接收心率数据变化通知- 心率数据解析:根据BLE规范解析标志位和数据格式
- 手表设置
- 手机进入「Zepp」应用
- 打开「设备」>「通用」>「蓝牙广播」
- 启用「开启蓝牙广播」
- 这样手表上的APP会多一个「心率推送」的,点开它
- 点开后,同步打开PC端的扫描,看到蓝牙已经连接上就不用管了,如果长时间丢失连接,手表出于节能的目的会自动关闭广播功能
- 注意事项
- 确保系统蓝牙已开启
- 手表无需与电脑配对
- 手表与电脑距离应在10米内
- 心率值格式根据BLE规范解析(8位或16位)
运行效果
开源项目
基于上述的原理,我进一步做成了一个带GUI的WPF开源项目,欢迎大家去Star
https://github.com/lishewen/AmazfitHeartRateReceiver
效果图
Web服务
在这个开源项目中,还做了一个小功能,见启动Web服务
按钮
点击后会拉起一个内置的ASP.Net Core的小型网站,默认地址为http://localhost:5001/
里面只有一个页面,是一个300px*300px的实时心率小卡片
OBS的小卡片
这个小卡片可用于在OBS直播中显示实时心率,添加一个浏览器源,按下面这样配置一下
这样就可以在直播魔兽打本的时候显示自己的心率了
在WPF中拉起Web服务的实现细节
- 安装Nuget包
Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting
- 修改
.csproj
,加入
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
</ItemGroup>
- 实现代码
private void StartWebServer()
{
// 创建并启动Web主机
_webHost = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseUrls(_webServerUrl);
webBuilder.Configure(app => { /* 配置中间件 */ });
})
.Build();
_webServerTask = _webHost.StartAsync(_cancellationTokenSource.Token);
// 自动打开默认浏览器
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = _webServerUrl,
UseShellExecute = true
});
}
private void StopWebServer()
{
// 停止并释放Web主机资源
_cancellationTokenSource?.Cancel();
_webHost?.StopAsync().Wait();
_webHost?.Dispose();
_webHost = null;
}