使用 .Net Core 在 Raspberry Pi 上使用蓝牙 LE

Posted

技术标签:

【中文标题】使用 .Net Core 在 Raspberry Pi 上使用蓝牙 LE【英文标题】:Utilizing bluetooth LE on Raspberry Pi using .Net Core 【发布时间】:2019-05-24 19:01:07 【问题描述】:

我想在 .NET Core 中构建 GATT 客户端。它将部署到运行 Raspbian Lite 的 RPi3 控制多个 BLE 设备。 .Net Core Framework(2.2 或 3 预览版)中当前是否支持蓝牙 LE?

我知道在 RPi 上的 Windows 10 IoT 上使用 UWP 库的替代方法,但我更喜欢运行 Raspbian Lite。这种堆栈目前还有其他替代方案吗?

【问题讨论】:

看起来像广告,但Bluetooth Framework 支持 Windows 10 IoT (.NET Core)。包中有控制台演示。不幸的是,我们没有在 raspbian 上对其进行测试,因为它使用了一些与 Windows 相关的功能。 感谢@MikePetrichenko 的回复。我刚刚尝试了蓝牙框架,但没有成功。首先它需要 libunwind.so.8,我可以安装它,但随后它在典型的 Windows 库上出现异常而失败:system.dllnotfoundexception:无法加载 dll 'advapi32.dll 这可能是对库的提示解决这个问题:github.com/dotnet/corefx/issues/5737#issuecomment-175916673 感谢您的尝试。我不是 100% 确定,但看起来可以修改 lib 以使 iut 在您的平台上工作。您能否给我发送一封电子邮件至 mike@btframework.com 并附上您的任务描述,以便我可以尝试删除 advapi 依赖并向您发送测试版本?如果你觉得可以的话,我可以在接下来的几周内完成。 我也有同样的要求。在 Raspbian 上使用 dotnet core 设置 gatt 服务器有什么想法吗? 【参考方案1】:

背景

BlueZ 是 Linux 上的蓝牙堆栈。 BlueZ 开发人员鼓励使用他们的高级D-Bus APIs。 (来源:https://youtu.be/VMDyebKT5c4?t=2102 或 https://elinux.org/images/3/32/Doing_Bluetooth_Low_Energy_on_Linux.pdf,幻灯片 22。)D-Bus 让您可以控制各种系统服务,并且有适用于包括 .Net Core 在内的许多平台的 D-Bus 绑定/包。因此,使用面向 Linux 的 .Net(例如 Raspbian Lite)编写 GATT 客户端或 GATT 服务器应该有点简单。

解决方案

对于 .Net Core,您可以使用 Tmds.DBus 访问 D-Bus。 Tmds.DBus 附带一个为 D-Bus 服务生成 C# 接口的工具。我使用 BlueZ 交互式命令行工具 bluetoothctl 扫描并连接到 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(在installing the 5.50 release of BlueZ 之后)并且可能有用。但是,如果您在使用该软件包时遇到问题,我建议您尝试直接访问 BlueZ D-Bus APIs。 C source code for bluetoothctl 是如何使用 D-Bus API 进行扫描、连接、配对等的一个很好的例子。

【讨论】:

甜蜜!很高兴在我的第一个谷歌上找到这个,你在 18 小时前发布了一个答案! :-D 在我看来,对于连接 BLE 设备这样的基本事情,不需要使用第三方库。也就是说,我一定会尝试一下!如果可行,我会接受这个作为解决方案。 @RaymondBrink 我认为可以肯定地说,.NET Core 不是基于 Linux 上的桌面或设备平台的想法构建的(与 Windows .NET Core 或 .NET Framework 不同) .它可能会随着 System.Maui(又名 Xamarin Forms)的出现而改变,但他们已经宣布他们的 Linux 支持是社区驱动的。即使在 Xamarin 中,基本包也不包括 BLE。 如何创建 GATT 服务器并使用 Raspberry Pi 上的 D-Bus 对其进行广告宣传? @ParsaKarami 你找到方法了吗?我目前正在做类似的事情,我需要创建一个 GATT 服务器并在 Raspberry Pi 4 中宣传它

以上是关于使用 .Net Core 在 Raspberry Pi 上使用蓝牙 LE的主要内容,如果未能解决你的问题,请参考以下文章

[IOT] - Raspberry Pi 3B + Windows 10 IOT Core + .Net Core Web 部署

.NET Core Docker Image for Linux-arm (Raspberry pi)

使用单声道时的 Serial.IO.Ports 问题,适用于 dotnet core 3.1 / arm / raspberry pi 4

Raspberry Pi起步(Snappy Ubuntu Core)

Raspberry Pi 和带单声道的帧缓冲输入

sh GPS和推送通知以及速度测试来自:http://decryption.net.au/index.php/2013/10/05/raspberry-pi-portable-3g4g-network