Skip to content

Commit 8edf0dc

Browse files
BlueZ bluetooth backend implementation
#64 non-breaking
1 parent 9a3831e commit 8edf0dc

15 files changed

+1418
-3
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ var serviceProvider = new ServiceCollection()
3838
.AddLogging()
3939
.AddPoweredUp()
4040
.AddWinRTBluetooth() // using WinRT Bluetooth on Windows
41+
//.AddBlueZBluetooth() // using BlueZ Bluetooth on Linux
4142
.BuildServiceProvider();
4243

4344
var host = serviceProvider.GetService<PoweredUpHost>();
@@ -155,6 +156,7 @@ var serviceProvider = new ServiceCollection()
155156
.AddLogging()
156157
.AddPoweredUp()
157158
.AddWinRTBluetooth() // using WinRT Bluetooth on Windows
159+
//.AddBlueZBluetooth() // using BlueZ Bluetooth on Linux
158160
.BuildServiceProvider();
159161

160162
using (var scope = serviceProvider.CreateScope()) // create a scoped DI container per intented active connection/protocol. If disposed, disposes all disposable artifacts.
@@ -236,6 +238,8 @@ DI Container Elements
236238
- [X] .NET Core 3.1 (on Windows 10 using WinRT)
237239
- Library uses `Span<T>` / C# 8.0 and is therefore not supported in .NET Framework 1.0 - 4.8 and UWP Apps until arrival of .NET 5 (WinForms and WPF work in .NET Core 3.1)
238240
- Library uses WinRT for communication therefore only Windows 10
241+
- [X] .NET Core 3.1 / .NET 5 on Linux using BlueZ
242+
- Requires `bluez` to be installed and configured.
239243
- [ ] Xamarin (on iOS / Android using ?)
240244
- [ ] Blazor (on Browser using WebBluetooth)
241245
- Hub Model

examples/SharpBrick.PoweredUp.Examples/BaseExample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void InitHost(bool enableTrace)
8686
builder.AddFilter("SharpBrick.PoweredUp.Bluetooth.BluetoothKernel", LogLevel.Debug);
8787
}
8888
})
89-
.AddWinRTBluetooth()
89+
.AddBlueZBluetooth()
9090
;
9191

9292
Configure(serviceCollection);

examples/SharpBrick.PoweredUp.Examples/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ static async Task Main(string[] args)
1818
//example = new Example.ExampleMotorVirtualPort();
1919
//example = new Example.ExampleHubActions();
2020
//example = new Example.ExampleTechnicMediumHubAccelerometer();
21-
//example = new Example.ExampleTechnicMediumHubGyroSensor();
21+
example = new Example.ExampleTechnicMediumHubGyroSensor();
2222
//example = new Example.ExampleVoltage();
2323
//example = new Example.ExampleTechnicMediumTemperatureSensor();
2424
//example = new Example.ExampleMotorInputCombinedMode();
@@ -33,7 +33,7 @@ static async Task Main(string[] args)
3333
//example = new Example.ExampleHubPropertyObserving();
3434
//example = new Example.ExampleDiscoverByType();
3535
//example = new Example.ExampleCalibrationSteering();
36-
example = new Example.ExampleTechnicMediumHubGestSensor();
36+
//example = new Example.ExampleTechnicMediumHubGestSensor();
3737

3838
// NOTE: Examples are programmed object oriented style. Base class implements methods Configure, DiscoverAsync and ExecuteAsync to be overwriten on demand.
3939
await example.InitHostAndDiscoverAsync(enableTrace);

examples/SharpBrick.PoweredUp.Examples/SharpBrick.PoweredUp.Examples.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<ProjectReference Include="..\..\src\SharpBrick.PoweredUp\SharpBrick.PoweredUp.csproj" />
1111
<ProjectReference Include="..\..\src\SharpBrick.PoweredUp.WinRT\SharpBrick.PoweredUp.WinRT.csproj" />
12+
<ProjectReference Include="..\..\src\SharpBrick.PoweredUp.BlueZ\SharpBrick.PoweredUp.BlueZ.csproj" />
1213
</ItemGroup>
1314

1415
<ItemGroup>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace SharpBrick.PoweredUp.BlueZ
2+
{
3+
internal class BlueZConstants
4+
{
5+
public const string BlueZDBusServiceName = "org.bluez";
6+
}
7+
}
8+
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.Logging;
7+
using SharpBrick.PoweredUp.Bluetooth;
8+
using SharpBrick.PoweredUp.BlueZ.Utilities;
9+
using Tmds.DBus;
10+
11+
namespace SharpBrick.PoweredUp.BlueZ
12+
{
13+
public class BlueZPoweredUpBluetoothAdapter : IPoweredUpBluetoothAdapter
14+
{
15+
private readonly ILogger<BlueZPoweredUpBluetoothAdapter> _logger;
16+
private readonly string _adapterObjectPath;
17+
private readonly Dictionary<ulong, IPoweredUpBluetoothDevice> _devices = new Dictionary<ulong, IPoweredUpBluetoothDevice>();
18+
private IAdapter1 _adapter;
19+
20+
public bool Discovering { get; set; } = false;
21+
22+
public BlueZPoweredUpBluetoothAdapter(
23+
ILogger<BlueZPoweredUpBluetoothAdapter> logger,
24+
string adapterObjectPath = null) //"/org/bluez/hci0")
25+
{
26+
_logger = logger;
27+
_adapterObjectPath = adapterObjectPath;
28+
}
29+
30+
private async Task<IAdapter1> GetAdapterAsync()
31+
{
32+
var adapter = !string.IsNullOrEmpty(_adapterObjectPath) ? Connection.System.CreateProxy<IAdapter1>(BlueZConstants.BlueZDBusServiceName, _adapterObjectPath) : await FindFirstAdapter();
33+
34+
// validate the adapter
35+
await adapter.GetAliasAsync();
36+
37+
await adapter.WatchPropertiesAsync(AdapterPropertyChangedHandler);
38+
39+
return adapter;
40+
}
41+
42+
private async Task<IAdapter1> FindFirstAdapter()
43+
{
44+
var adapters = await Connection.System.FindProxies<IAdapter1>();
45+
return adapters.FirstOrDefault();
46+
}
47+
48+
private void AdapterPropertyChangedHandler(PropertyChanges changes)
49+
{
50+
_logger.LogDebug("Property changed {ChangedProperties}", changes.Changed);
51+
52+
foreach (var propertyChanged in changes.Changed)
53+
{
54+
switch (propertyChanged.Key)
55+
{
56+
case "Discovering":
57+
Discovering = (bool)propertyChanged.Value;
58+
break;
59+
}
60+
}
61+
}
62+
63+
private async Task<ICollection<IDevice1>> GetExistingDevicesAsync()
64+
=> await Connection.System.FindProxies<IDevice1>();
65+
66+
private IDevice1 GetSpecificDeviceAsync(ObjectPath objectPath)
67+
=> Connection.System.CreateProxy<IDevice1>(BlueZConstants.BlueZDBusServiceName, objectPath);
68+
69+
//private IPoweredUpBluetoothDevice ConstructDevice(IDevice1 device, Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler = null)
70+
// => new BlueZPoweredUpBluetoothDevice(device, discoveryHandler);
71+
72+
private async Task<bool> IsLegoWirelessProcotolDevice(IDevice1 device)
73+
=> (await device.GetUUIDsAsync()).NullToEmpty().Any(x => x.ToUpperInvariant() == PoweredUpBluetoothConstants.LegoHubService);
74+
75+
public async void Discover(Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler, CancellationToken cancellationToken = default)
76+
{
77+
_adapter ??= await GetAdapterAsync();
78+
79+
var existingDevices = await GetExistingDevicesAsync();
80+
81+
foreach (var device in existingDevices)
82+
{
83+
if (await IsLegoWirelessProcotolDevice(device))
84+
{
85+
var poweredUpDevice = new BlueZPoweredUpBluetoothDevice(device, discoveryHandler);
86+
await poweredUpDevice.Initialize();
87+
88+
_devices.Add(poweredUpDevice.DeviceInfo.BluetoothAddress, poweredUpDevice);
89+
90+
await poweredUpDevice.GetManufacturerDataAndInvokeHandlerAsync();
91+
}
92+
}
93+
94+
await Connection.System.WatchInterfacesAdded(NewDeviceAddedHandler);
95+
96+
await _adapter.SetDiscoveryFilterAsync(new Dictionary<string,object>()
97+
{
98+
{ "UUIDs", new string[] { PoweredUpBluetoothConstants.LegoHubService } }
99+
});
100+
101+
cancellationToken.Register(async () =>
102+
{
103+
if (Discovering)
104+
{
105+
await _adapter.StopDiscoveryAsync();
106+
}
107+
});
108+
109+
await _adapter.StartDiscoveryAsync();
110+
111+
async void NewDeviceAddedHandler((ObjectPath objectPath, IDictionary<string, IDictionary<string, object>> interfaces) args)
112+
{
113+
if (!args.interfaces.ContainsKey("org.bluez.Device1"))
114+
{
115+
return;
116+
}
117+
118+
var device = GetSpecificDeviceAsync(args.objectPath);
119+
var poweredUpDevice = new BlueZPoweredUpBluetoothDevice(device, discoveryHandler);
120+
121+
await poweredUpDevice.Initialize();
122+
123+
_devices.Add(poweredUpDevice.DeviceInfo.BluetoothAddress, poweredUpDevice);
124+
125+
await poweredUpDevice.GetManufacturerDataAndInvokeHandlerAsync();
126+
}
127+
}
128+
129+
public Task<IPoweredUpBluetoothDevice> GetDeviceAsync(ulong bluetoothAddress)
130+
{
131+
if (!_devices.ContainsKey(bluetoothAddress))
132+
{
133+
throw new ArgumentOutOfRangeException("Requested bluetooth device is not available from this adapter");
134+
}
135+
136+
return Task.FromResult<IPoweredUpBluetoothDevice>(_devices[bluetoothAddress]);
137+
}
138+
}
139+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using Polly;
5+
using SharpBrick.PoweredUp.Bluetooth;
6+
using Tmds.DBus;
7+
8+
namespace SharpBrick.PoweredUp.BlueZ
9+
{
10+
internal class BlueZPoweredUpBluetoothCharacteristic : IPoweredUpBluetoothCharacteristic
11+
{
12+
private IGattCharacteristic1 _characteristic;
13+
14+
public BlueZPoweredUpBluetoothCharacteristic(IGattCharacteristic1 characteristic, Guid uuid)
15+
{
16+
Uuid = uuid;
17+
_characteristic = characteristic ?? throw new ArgumentNullException(nameof(characteristic));
18+
}
19+
20+
public Guid Uuid { get; }
21+
22+
public async Task<bool> NotifyValueChangeAsync(Func<byte[], Task> notificationHandler)
23+
{
24+
if (notificationHandler is null)
25+
{
26+
throw new ArgumentNullException(nameof(notificationHandler));
27+
}
28+
29+
await _characteristic.WatchPropertiesAsync(PropertyChangedHandler);
30+
31+
await _characteristic.StartNotifyAsync();
32+
33+
return true;
34+
35+
void PropertyChangedHandler(PropertyChanges propertyChanges)
36+
{
37+
foreach (var propertyChanged in propertyChanges.Changed)
38+
{
39+
if (propertyChanged.Key == "Value")
40+
{
41+
notificationHandler((byte[])propertyChanged.Value);
42+
}
43+
}
44+
}
45+
}
46+
47+
public async Task<bool> WriteValueAsync(byte[] data)
48+
{
49+
if (data is null)
50+
{
51+
throw new ArgumentNullException(nameof(data));
52+
}
53+
54+
await Policy
55+
.Handle<Tmds.DBus.DBusException>()
56+
.WaitAndRetryForeverAsync(_ => TimeSpan.FromMilliseconds(10))
57+
.ExecuteAsync(() => _characteristic.WriteValueAsync(data, new Dictionary<string, object>()));
58+
59+
return true;
60+
}
61+
}
62+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using SharpBrick.PoweredUp.Bluetooth;
7+
using Tmds.DBus;
8+
9+
namespace SharpBrick.PoweredUp.BlueZ
10+
{
11+
internal class BlueZPoweredUpBluetoothDevice : IPoweredUpBluetoothDevice
12+
{
13+
private readonly Func<PoweredUpBluetoothDeviceInfo, Task> _discoveryHandler;
14+
private IDevice1 _device;
15+
16+
internal PoweredUpBluetoothDeviceInfo DeviceInfo { get; private set; } = new PoweredUpBluetoothDeviceInfo();
17+
internal bool Connected { get; private set; } = false;
18+
internal bool ServicesResolved { get; private set;} = false;
19+
20+
internal BlueZPoweredUpBluetoothDevice(IDevice1 device, Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler = null)
21+
{
22+
_discoveryHandler = discoveryHandler;
23+
_device = device;
24+
}
25+
26+
public string Name { get; private set; } = string.Empty;
27+
28+
internal async Task Initialize()
29+
{
30+
await _device.WatchPropertiesAsync(DevicePropertyChangedHandler);
31+
32+
await GetSafeDeviceInfoAsync();
33+
}
34+
35+
internal async Task GetManufacturerDataAndInvokeHandlerAsync()
36+
{
37+
try
38+
{
39+
var manufacturerData = await _device.GetManufacturerDataAsync();
40+
DeviceInfo.ManufacturerData = (byte[])manufacturerData.First().Value;
41+
42+
await _discoveryHandler(DeviceInfo);
43+
}
44+
catch
45+
{
46+
// we can ignore errors here, this will throw an exception for existing devices (only after reboot)
47+
// manufacturer data will be returned when discovery is turned on
48+
}
49+
}
50+
51+
private async void DevicePropertyChangedHandler(PropertyChanges changes)
52+
{
53+
foreach (var propertyChanged in changes.Changed)
54+
{
55+
switch (propertyChanged.Key)
56+
{
57+
case "ManufacturerData":
58+
DeviceInfo.ManufacturerData = (byte[])((IDictionary<ushort,object>)propertyChanged.Value).First().Value;
59+
await _discoveryHandler(DeviceInfo);
60+
break;
61+
case "Connected":
62+
Connected = (bool)propertyChanged.Value;
63+
break;
64+
case "ServicesResolved":
65+
ServicesResolved = (bool)propertyChanged.Value;
66+
break;
67+
}
68+
}
69+
}
70+
71+
internal async Task GetSafeDeviceInfoAsync()
72+
{
73+
var btAddress = await _device.GetAddressAsync();
74+
DeviceInfo.BluetoothAddress = Utilities.BluetoothAddressFormatter.ConvertToInteger(btAddress);
75+
DeviceInfo.Name = Name = await _device.GetNameAsync();
76+
}
77+
78+
~BlueZPoweredUpBluetoothDevice() => Dispose(false);
79+
public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }
80+
81+
protected virtual async void Dispose(bool disposing)
82+
{
83+
if (Connected)
84+
await _device?.DisconnectAsync(); // dangerous to await here, try to find a better way
85+
86+
_device = null;
87+
}
88+
89+
private async Task WaitForConnectionAndServicesResolved(CancellationToken token)
90+
{
91+
while (!Connected || !ServicesResolved)
92+
{
93+
await Task.Delay(25, token);
94+
}
95+
}
96+
97+
public async Task<IPoweredUpBluetoothService> GetServiceAsync(Guid serviceId)
98+
{
99+
var connectionTimeout = TimeSpan.FromSeconds(5);
100+
101+
var cancellationTokenSource = new CancellationTokenSource();
102+
103+
await _device.ConnectAsync();
104+
105+
cancellationTokenSource.CancelAfter(connectionTimeout);
106+
107+
await WaitForConnectionAndServicesResolved(cancellationTokenSource.Token);
108+
109+
var gattServices = await Connection.System.FindProxies<IGattService1>();
110+
111+
foreach (var gattService in gattServices)
112+
{
113+
var gattUuid = Guid.Parse(await gattService.GetUUIDAsync());
114+
115+
if (gattUuid == serviceId)
116+
{
117+
return new BlueZPoweredUpBluetoothService(gattService, gattUuid);
118+
}
119+
}
120+
121+
throw new ArgumentOutOfRangeException(nameof(serviceId), $"Service with id {serviceId} not found");
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)