c# - 使用 .Net Core 在 Raspberry Pi 上使用蓝牙 LE
问题描述
我想在 .NET Core 中构建一个 GATT 客户端。它将部署到运行 Raspbian Lite 的 RPi3 控制多个 BLE 设备。.Net Core Framework(2.2 或 3 预览版)中当前是否支持蓝牙 LE?
我知道在 RPi 上的 Windows 10 IoT 上使用 UWP 库的替代方法,但我更喜欢运行 Raspbian Lite。这种堆栈目前还有其他替代方案吗?
解决方案
背景
BlueZ 是 Linux 上的蓝牙堆栈。BlueZ 开发人员鼓励使用他们的高级D-Bus API。(来源:https ://youtu.be/VMDyebKT5c4?t=2102或https://elinux.org/images/3/32/Doing_Bluetooth_Low_Energy_on_Linux.pdf,幻灯片 22。)D-Bus 让您控制各种系统服务,并且有许多平台的 D-Bus 绑定/包,包括 .Net Core。因此,使用面向 Linux 的 .Net(例如 Raspbian Lite)编写 GATT 客户端或 GATT 服务器应该有点简单。
解决方案
对于 .Net Core,您可以使用Tmds.DBus访问 D-Bus。Tmds.DBus 附带一个为 D-Bus 服务生成 C# 接口的工具。我使用bluetoothctl
BlueZ 交互式命令行工具扫描并连接到 BLE 外围设备,然后用于dotnet dbus codegen --bus system --service org.bluez
生成 C# 接口。
示例代码
dotnet dbus codegen
生成的代码摘录:
[DBusInterface("org.bluez.Adapter1")]
interface IAdapter1 : IDBusObject
{
Task StartDiscoveryAsync();
Task SetDiscoveryFilterAsync(IDictionary<string, object> Properties);
Task StopDiscoveryAsync();
Task RemoveDeviceAsync(ObjectPath Device);
Task<string[]> GetDiscoveryFiltersAsync();
Task<T> GetAsync<T>(string prop);
Task<Adapter1Properties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[DBusInterface("org.bluez.Device1")]
interface IDevice1 : IDBusObject
{
Task DisconnectAsync();
Task ConnectAsync();
Task ConnectProfileAsync(string UUID);
Task DisconnectProfileAsync(string UUID);
Task PairAsync();
Task CancelPairingAsync();
Task<T> GetAsync<T>(string prop);
Task<Device1Properties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[DBusInterface("org.bluez.GattService1")]
interface IGattService1 : IDBusObject
{
Task<T> GetAsync<T>(string prop);
Task<GattService1Properties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
[DBusInterface("org.bluez.GattCharacteristic1")]
interface IGattCharacteristic1 : IDBusObject
{
Task<byte[]> ReadValueAsync(IDictionary<string, object> Options);
Task WriteValueAsync(byte[] Value, IDictionary<string, object> Options);
Task<(CloseSafeHandle fd, ushort mtu)> AcquireWriteAsync(IDictionary<string, object> Options);
Task<(CloseSafeHandle fd, ushort mtu)> AcquireNotifyAsync(IDictionary<string, object> Options);
Task StartNotifyAsync();
Task StopNotifyAsync();
Task<T> GetAsync<T>(string prop);
Task<GattCharacteristic1Properties> GetAllAsync();
Task SetAsync(string prop, object val);
Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
}
示例用法。给定一个 BLE 外设地址,连接并打印“设备信息”GATT 服务的特征值:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// The code generated by `dotnet dbus codegen`.
using bluez.DBus;
// See https://developers.redhat.com/blog/2017/09/18/connecting-net-core-d-bus/ or https://github.com/tmds/Tmds.DBus
using Tmds.DBus;
// Use the `bluetoothctl` command-line tool or the Bluetooth Manager GUI to scan for devices and possibly pair.
// Then you can use this program to connect and print "Device Information" GATT service values.
class Program
{
static string defaultAdapterName = "hci0";
static TimeSpan timeout = TimeSpan.FromSeconds(15);
static async Task Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: BlueZExample <deviceAddress> [adapterName]");
Console.WriteLine("Example: BlueZExample AA:BB:CC:11:22:33 hci1");
return;
}
var deviceAddress = args[0];
var adapterName = args.Length > 1 ? args[1] : defaultAdapterName;
// Get the Bluetooth adapter.
var adapterObjectPath = $"/org/bluez/{adapterName}";
var adapter = Connection.System.CreateProxy<IAdapter1>(BluezConstants.DBusService, adapterObjectPath);
if (adapter == null)
{
Console.WriteLine($"Bluetooth adapter '{adapterName}' not found.");
}
// Find the Bluetooth peripheral.
var device = await adapter.GetDeviceAsync(deviceAddress);
if (device == null)
{
Console.WriteLine($"Bluetooth peripheral with address '{deviceAddress}' not found. Use `bluetoothctl` or Bluetooth Manager to scan and possibly pair first.");
return;
}
Console.WriteLine("Connecting...");
await device.ConnectAsync();
await WaitForPropertyValueAsync<bool>("Connected", device.GetConnectedAsync, value: true, timeout);
Console.WriteLine("Connected.");
Console.WriteLine("Waiting for services to resolve...");
await WaitForPropertyValueAsync<bool>("ServicesResolved", device.GetServicesResolvedAsync, value: true, timeout);
var servicesUUID = await device.GetUUIDsAsync();
Console.WriteLine($"Device offers {servicesUUID.Length} service(s).");
var deviceInfoServiceFound = servicesUUID.Any(uuid => String.Equals(uuid, GattConstants.DeviceInformationServiceUUID, StringComparison.OrdinalIgnoreCase));
if (!deviceInfoServiceFound)
{
Console.WriteLine("Device doesn't have the Device Information Service. Try pairing first?");
return;
}
// Console.WriteLine("Retrieving Device Information service...");
var service = await device.GetServiceAsync(GattConstants.DeviceInformationServiceUUID);
var modelNameCharacteristic = await service.GetCharacteristicAsync(GattConstants.ModelNameCharacteristicUUID);
var manufacturerCharacteristic = await service.GetCharacteristicAsync(GattConstants.ManufacturerNameCharacteristicUUID);
int characteristicsFound = 0;
if (modelNameCharacteristic != null)
{
characteristicsFound++;
Console.WriteLine("Reading model name characteristic...");
var modelNameBytes = await modelNameCharacteristic.ReadValueAsync(timeout);
Console.WriteLine($"Model name: {Encoding.UTF8.GetString(modelNameBytes)}");
}
if (manufacturerCharacteristic != null)
{
characteristicsFound++;
Console.WriteLine("Reading manufacturer characteristic...");
var manufacturerBytes = await manufacturerCharacteristic.ReadValueAsync(timeout);
Console.WriteLine($"Manufacturer: {Encoding.UTF8.GetString(manufacturerBytes)}");
}
if (characteristicsFound == 0)
{
Console.WriteLine("Model name and manufacturer characteristics not found.");
}
}
static async Task WaitForPropertyValueAsync<T>(string propertyName, Func<Task<T>> action, T value, TimeSpan timeout)
{
// Ideally we'd wait for D-Bus PropertyChanged events to fire, but for now we'll poll.
// Also ideally we'd be able to read property values for any D-Bus object, but for now we take a function.
var watch = Stopwatch.StartNew();
while (watch.Elapsed <= timeout)
{
await Task.Delay(50);
if ((await action()).Equals(value))
{
return;
}
}
throw new TimeoutException($"Timed out waiting for {propertyName} to equal {value}.");
}
}
// Extensions that make it easier to get a D-Bus object or read a characteristic value.
static class Extensions
{
public static Task<IReadOnlyList<IDevice1>> GetDevicesAsync(this IAdapter1 adapter)
{
return GetProxiesAsync<IDevice1>(adapter, BluezConstants.Device1Interface);
}
public static async Task<IDevice1> GetDeviceAsync(this IAdapter1 adapter, string deviceAddress)
{
var devices = await GetProxiesAsync<IDevice1>(adapter, BluezConstants.Device1Interface);
var matches = new List<IDevice1>();
foreach (var device in devices)
{
if (String.Equals(await device.GetAddressAsync(), deviceAddress, StringComparison.OrdinalIgnoreCase))
{
matches.Add(device);
}
}
// BlueZ can get in a weird state, probably due to random public BLE addresses.
if (matches.Count > 1)
{
throw new Exception($"{matches.Count} devices found with the address {deviceAddress}!");
}
return matches.FirstOrDefault();
}
public static async Task<IGattService1> GetServiceAsync(this IDevice1 device, string serviceUUID)
{
var services = await GetProxiesAsync<IGattService1>(device, BluezConstants.GattServiceInterface);
foreach (var service in services)
{
if (String.Equals(await service.GetUUIDAsync(), serviceUUID, StringComparison.OrdinalIgnoreCase))
{
return service;
}
}
return null;
}
public static async Task<IGattCharacteristic1> GetCharacteristicAsync(this IGattService1 service, string characteristicUUID)
{
var characteristics = await GetProxiesAsync<IGattCharacteristic1>(service, BluezConstants.GattCharacteristicInterface);
foreach (var characteristic in characteristics)
{
if (String.Equals(await characteristic.GetUUIDAsync(), characteristicUUID, StringComparison.OrdinalIgnoreCase))
{
return characteristic;
}
}
return null;
}
public static async Task<byte[]> ReadValueAsync(this IGattCharacteristic1 characteristic, TimeSpan timeout)
{
var options = new Dictionary<string, object>();
var readTask = characteristic.ReadValueAsync(options);
var timeoutTask = Task.Delay(timeout);
await Task.WhenAny(new Task[] { readTask, timeoutTask });
if (!readTask.IsCompleted)
{
throw new TimeoutException("Timed out waiting to read characteristic value.");
}
return await readTask;
}
private static async Task<IReadOnlyList<T>> GetProxiesAsync<T>(IDBusObject rootObject, string interfaceName)
{
// Console.WriteLine("GetProxiesAsync called.");
var objectManager = Connection.System.CreateProxy<IObjectManager>(BluezConstants.DBusService, "/");
var objects = await objectManager.GetManagedObjectsAsync();
var matchingObjects = objects
.Where(obj => obj.Value.Keys.Contains(interfaceName))
.Select(obj => obj.Key)
.Where(objectPath => objectPath.ToString().StartsWith($"{rootObject.ObjectPath}/"));
var proxies = matchingObjects
.Select(objectPath => Connection.System.CreateProxy<T>(BluezConstants.DBusService, objectPath))
.ToList();
// Console.WriteLine($"GetProxiesAsync returning {proxies.Count} proxies of type {typeof(T)}.");
return proxies;
}
}
static class GattConstants
{
// "Device Information" GATT service
// https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=244369
public const string DeviceInformationServiceUUID = "0000180a-0000-1000-8000-00805f9b34fb";
public const string ModelNameCharacteristicUUID = "00002a24-0000-1000-8000-00805f9b34fb";
public const string ManufacturerNameCharacteristicUUID = "00002a29-0000-1000-8000-00805f9b34fb";
}
static class BluezConstants
{
public const string DBusService = "org.bluez";
public const string Adapter1Interface = "org.bluez.Adapter1";
public const string Device1Interface = "org.bluez.Device1";
public const string GattServiceInterface = "org.bluez.GattService1";
public const string GattCharacteristicInterface = "org.bluez.GattCharacteristic1";
}
NuGet 包
基于上面的代码,我发布了HashtagChris.DotNetBlueZ,这是对 BlueZ 的 .Net Core 库的快速尝试。它适用于我的 Raspberry Pi(在安装了 BlueZ 的 5.50 版本之后)并且可能有用。但是,如果您在使用该软件包时遇到问题,我建议您尝试直接访问BlueZ D-Bus API。bluetoothctl的C 源代码是如何使用 D-Bus API 进行扫描、连接、配对等的一个很好的示例。
推荐阅读
- postgresql - 如何通过通知获取插入/更新的行
- sql - 用连接中的最后一个可用值替换零值
- node.js - 扫描作为 cron 字符串的 DynamoDB 属性并根据当前时间进行过滤
- database - 动态外键
- php - php解析错误:语法错误,意外'}',期待文件结束?
- json - 我需要使用 NiFi 将输入日期(2020-12-03T15:20:50.000Z)转换为 YYYY-MM-DD HH:MM:SS 格式
- javascript - 模糊背景图像,但不是该图像上的形状
- c# - 不调用种子方法
- java - 如何在elasticsearch java api中查询特定映射?
- python - 如何在异步函数中并行化 for 循环并跟踪 for 循环执行状态?