web-dev-qa-db-ja.com

.Net Coreを使用してRaspberry PiでBluetooth LEを利用する

.NET CoreでGATTクライアントを構築したいのですが。複数のBLEデバイスを制御するRaspbian Liteを実行するRPi3に展開します。現在、.NET Core Framework(2.2または3プレビュー)でBluetooth LEをサポートしていますか?

RPi上のWindows 10 IoTでUWPライブラリを使用する別の方法を知っていますが、代わりにRaspbian Liteを実行したいと思います。現在、そのようなスタックの他の選択肢はありますか?

8
Raymond Brink

バックグラウンド

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を使用すると、あらゆる種類のシステムサービスを制御できます。NetCoreを含む多くのプラットフォーム用の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パッケージ

上記のコードに基づいて、BlueZの.Net Coreライブラリをすばやく試す HashtagChris.DotNetBlueZ を公開しました。それは私のRaspberry Piで動作し( BlueZの5.50リリースをインストールした後 の後)、役立つ場合があります。ただし、パッケージを使用して問題が発生した場合は、 BlueZ D-Bus API に直接アクセスすることをお勧めします。 bluetoothctlのCソースコード は、D-Bus APIを使用してスキャン、接続、ペアリングなどを行う方法の良い例です。

9
Chris Sidi