本文介绍了使用.Net Core在Raspberry Pi上使用蓝牙LE的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'd like to build a GATT client in .NET Core. It will deploy to a RPi3 running Raspbian Lite controlling multiple BLE devices. Is there currently support for Bluetooth LE in the .Net Core Framework (2.2 or 3 preview)?

我知道在RPi的Windows 10 IoT上使用UWP库的替代方法,但我宁愿运行Raspbian Lite.当前是否有其他替代方案可以用于这种堆栈?

I'm aware of an alternative using a UWP library on Windows 10 IoT on the RPi, but I'd prefer to run Raspbian Lite instead. Are there currently any other alternatives for such a stack?

推荐答案

背景

BlueZ是Linux上的Bluetooth堆栈.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使您可以控制各种系统服务,并且为许多平台(包括.Net Core)提供了D-Bus绑定/程序包.因此,使用面向Linux的.Net(例如Raspbian Lite)编写GATT客户端或GATT服务器应该有点简单.

Background

BlueZ is the Bluetooth stack on Linux. The BlueZ developers encourage the use of their high-level D-Bus APIs. (Source: https://youtu.be/VMDyebKT5c4?t=2102 or https://elinux.org/images/3/32/Doing_Bluetooth_Low_Energy_on_Linux.pdf, slide 22.) D-Bus lets you control all sorts of system services, and there are D-Bus bindings/packages for lots of platforms including .Net Core. So it should be somewhat simple to write a GATT client or GATT server using .Net targeting Linux (e.g. Raspbian Lite).

对于.Net Core,您可以使用Tmds.DBus 访问D-Bus.Tmds.DBus带有一个工具,可为D-Bus服务生成C#接口.我使用BlueZ交互式命令行工具 bluetoothctl 扫描并连接到BLE外设,然后使用 dotnet dbus codegen --bus system --service org.bluez 生成C#接口.

For .Net Core you can use Tmds.DBus to access D-Bus. Tmds.DBus comes with a tool to generate the C# interfaces for a D-Bus service. I used bluetoothctl, the BlueZ interactive command-line tool, to scan and connect to a BLE peripheral, and then used dotnet dbus codegen --bus system --service org.bluez to generate C# interfaces.

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服务的特征值:

Example usage. Given a BLE peripheral address, connects and prints characteristic values for the "Device Information" GATT service:

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上运行(在安装了5.50版本的BlueZ ),可能会很有用.但是,如果您在使用该软件包时遇到问题,建议您尝试访问直接使用BlueZ D-Bus API .蓝牙 C源代码就是一个很好的例子有关如何使用D-Bus API进行扫描,连接,配对等的信息.

NuGet Package

Based on the code above, I published HashtagChris.DotNetBlueZ, a quick attempt at a .Net Core library for BlueZ. It works on my Raspberry Pi (after installing the 5.50 release of BlueZ) and may be useful. However if you run into issues using the package I recommend trying to access the BlueZ D-Bus APIs directly. The C source code for bluetoothctl serves as a good example for how to use the D-Bus APIs to scan, connect, pair, etc.

这篇关于使用.Net Core在Raspberry Pi上使用蓝牙LE的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

09-18 15:01