ygoe
ygoe

Reputation: 20393

Bluetooth LE GATT server running on a Raspberry Pi with .NET 8

I have cobbled together this .NET 8 application that should run on a Raspberry Pi (Raspberry Pi OS, 64-bit Linux) and do the following:

It contains loads of D-Bus interfaces that have been largely created by ChatGPT and I partially fixed them according to the BlueZ specification. I'm new to BlueZ and D-Bus so I can only roughly imagine what all of it does. Also, the AI wasn't all that helpful because I had to explore, guess and ask for specific APIs again and again that turned out to be essential but were left out before (like the GattManager and GattApplication).

The code has several issues though that I couldn't resolve so far:

Up to here I could identify the errors from the spec, but now I'm lost and the GattApplication part isn't really described anymore. Is it even correct?

So what have I missed? Are GATT services and characteristics only connected through the ObjectPath string value with common prefixes, or do I have to explicitly make that connection in the code somewhere? And what does that exception message mean?

Any help is appreciated as I wasn't able to find any resources about this online. I'm not sure if anybody has ever done that before.

Here's the code (Program.cs):

using Tmds.DBus;

namespace RadarSender;

public class Program
{
    public static async Task Main()
    {
        // Create a DBus connection with manual connection management
        var connection = new Connection(Address.System);
        await connection.ConnectAsync();

        // Access the LEAdvertisingManager1 interface
        const string adapterPath = "/org/bluez/hci0";
        var advertisingManager = connection.CreateProxy<ILEAdvertisingManager1>("org.bluez", adapterPath);

        // Advertisement setup
        const string advertisementPath = "/com/example/Advertisement";
        Dictionary<string, object> manufacturerData = [];
        manufacturerData["0x8001"] = new byte[] { 0x12, 0x34, 0x56 };
        var advertisement = new LEAdvertisement(advertisementPath, manufacturerData);
        await connection.RegisterObjectAsync(advertisement);

        await advertisingManager.RegisterAdvertisementAsync(advertisement.ObjectPath, new Dictionary<string, object>());
        Console.WriteLine("BLE Advertisement started.");

        // Create GATT objects
        var application = new GattApplication("/com/example/gattapp");
        var service = new GattService("/com/example/gattapp/service0", "12345678-1234-5678-1234-56789abcdef0");
        var characteristic = new GattCharacteristic("/com/example/gattapp/service0/char0", "12345678-1234-5678-1234-56789abcdef1");

        // Register objects with DBus
        await connection.RegisterObjectAsync(application);
        await connection.RegisterObjectAsync(service);
        await connection.RegisterObjectAsync(characteristic);

        // Register application with BlueZ
        var gattManager = connection.CreateProxy<IGattManager1>("org.bluez", "/org/bluez/hci0");
        await gattManager.RegisterApplicationAsync(application.ObjectPath, new Dictionary<string, object>());
        Console.WriteLine("GATT server started.");

        Console.WriteLine("Press Enter to exit...");
        Console.ReadLine();

        // Cleanup
        await advertisingManager.UnregisterAdvertisementAsync(advertisement.ObjectPath);
        connection.UnregisterObject(advertisement.ObjectPath);
        await gattManager.UnregisterApplicationAsync(application.ObjectPath);
        connection.UnregisterObject(service.ObjectPath);
        connection.UnregisterObject(characteristic.ObjectPath);
        connection.Dispose();
    }
}

[DBusInterface("org.bluez.LEAdvertisingManager1")]
public interface ILEAdvertisingManager1 : IDBusObject
{
    Task RegisterAdvertisementAsync(ObjectPath advertisement, IDictionary<string, object> options);
    Task UnregisterAdvertisementAsync(ObjectPath advertisement);
}

[DBusInterface("org.bluez.LEAdvertisement1")]
public interface ILEAdvertisement1 : IDBusObject
{
    Task<string[]> GetIncludesAsync();
    Task<string> GetTypeAsync();
    Task<string[]> GetServiceUUIDsAsync();
    Task<string> GetLocalNameAsync();
    Task<ushort> GetAppearanceAsync();
    Task<ushort> GetTxPowerAsync();
    Task<IDictionary<string, object>> GetManufacturerDataAsync();
    Task<IDictionary<string, object>> GetServiceDataAsync();
    Task ReleaseAsync();
}

public class LEAdvertisement : ILEAdvertisement1
{
    private readonly IDictionary<string, object> manufacturerData;

    public LEAdvertisement(string path, IDictionary<string, object> manufacturerData)
    {
        ObjectPath = new ObjectPath(path);
        this.manufacturerData = manufacturerData;
    }

    public ObjectPath ObjectPath { get; }

    public Task<string[]> GetIncludesAsync() => Task.FromResult<string[]>(["local-name", "tx-power"]);

    public Task<string> GetTypeAsync() => Task.FromResult("peripheral");

    public Task<string[]> GetServiceUUIDsAsync() => Task.FromResult<string[]>(["12345678-1234-5678-1234-56789abcdef0"]);

    public Task<string> GetLocalNameAsync() => Task.FromResult("RaspberryPiBLE");

    public Task<ushort> GetAppearanceAsync() => Task.FromResult<ushort>(0);

    public Task<ushort> GetTxPowerAsync() => Task.FromResult<ushort>(10);

    public Task<IDictionary<string, object>> GetManufacturerDataAsync() => Task.FromResult(manufacturerData);

    public Task<IDictionary<string, object>> GetServiceDataAsync() => Task.FromResult<IDictionary<string, object>>(new Dictionary<string, object>());

    public Task ReleaseAsync()
    {
        Console.WriteLine("Advertisement released.");
        return Task.CompletedTask;
    }
}

[DBusInterface("org.bluez.GattManager1")]
public interface IGattManager1 : IDBusObject
{
    Task RegisterApplicationAsync(ObjectPath application, IDictionary<string, object> options);
    Task UnregisterApplicationAsync(ObjectPath application);
}

public class GattApplication : IDBusObject
{
    public GattApplication(string path)
    {
        ObjectPath = new ObjectPath(path);
    }

    public ObjectPath ObjectPath { get; }
}


[DBusInterface("org.bluez.GattService1")]
public interface IGattService1 : IDBusObject
{
    Task<string> GetUUIDAsync();
    Task<bool> GetPrimaryAsync();
    Task<ObjectPath[]> GetIncludesAsync();
    Task<ObjectPath[]> GetCharacteristicsAsync();
}

public class GattService : IGattService1
{
    private readonly string uuid;

    public GattService(string path, string uuid)
    {
        ObjectPath = new ObjectPath(path);
        this.uuid = uuid;
    }

    public ObjectPath ObjectPath { get; }

    public Task<string> GetUUIDAsync() => Task.FromResult(uuid);

    public Task<bool> GetPrimaryAsync() => Task.FromResult(true);

    public Task<ObjectPath[]> GetIncludesAsync() => Task.FromResult<ObjectPath[]>([]);

    public Task<ObjectPath[]> GetCharacteristicsAsync()
    {
        // TODO: Add future characteristics here
        var characteristicPath = new ObjectPath($"{ObjectPath}/char0");
        return Task.FromResult(new[] { characteristicPath });
    }
}

[DBusInterface("org.bluez.GattCharacteristic1")]
public interface IGattCharacteristic1 : IDBusObject
{
    Task<byte[]> ReadValueAsync(IDictionary<string, object> options);
    Task WriteValueAsync(byte[] value, IDictionary<string, object> options);
    Task StartNotifyAsync();
    Task StopNotifyAsync();
    Task<string> GetUUIDAsync();
    Task<string[]> GetFlagsAsync();
    Task<ObjectPath> GetServiceAsync();
    Task<bool> GetNotifyingAsync();
    Task<byte[]> GetValueAsync();
}

public class GattCharacteristic : IGattCharacteristic1
{
    private readonly string uuid;

    public GattCharacteristic(string path, string uuid)
    {
        ObjectPath = new ObjectPath(path);
        this.uuid = uuid;
    }

    public ObjectPath ObjectPath { get; }

    public Task<string[]> GetFlagsAsync() => Task.FromResult<string[]>(["read", "write", "notify"]);

    public Task<byte[]> ReadValueAsync(IDictionary<string, object> options)
    {
        Console.WriteLine("Characteristic read requested.");
        return Task.FromResult(new byte[] { 0x01, 0x02, 0x03 });
    }

    public Task WriteValueAsync(byte[] value, IDictionary<string, object> options)
    {
        Console.WriteLine($"Characteristic write requested: {BitConverter.ToString(value)}");
        return Task.CompletedTask;
    }

    public Task StartNotifyAsync()
    {
        Console.WriteLine("Notifications started.");
        return Task.CompletedTask;
    }

    public Task StopNotifyAsync()
    {
        Console.WriteLine("Notifications stopped.");
        return Task.CompletedTask;
    }

    public Task<string> GetUUIDAsync() => Task.FromResult(uuid);

    // TODO: Make configurable
    public Task<ObjectPath> GetServiceAsync() => Task.FromResult(new ObjectPath("/com/example/gattapp/service0"));

    public Task<bool> GetNotifyingAsync() => Task.FromResult(false);

    public Task<byte[]> GetValueAsync() => Task.FromResult(new byte[] { 0x11, 0x22, 0x33, 0x44 });
}

And the corresponding project file (.csproj):

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Tmds.DBus" Version="0.21.2" />
  </ItemGroup>

</Project>

Upvotes: 0

Views: 111

Answers (0)

Related Questions