Skip to content

Commit 6febc05

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

15 files changed

+1434
-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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,10 @@ public void InitHost(bool enableTrace)
8484
if (enableTrace)
8585
{
8686
builder.AddFilter("SharpBrick.PoweredUp.Bluetooth.BluetoothKernel", LogLevel.Debug);
87+
builder.AddFilter("SharpBrick.PoweredUp.BlueZ.BlueZPoweredUpBluetoothAdapter", LogLevel.Debug);
8788
}
8889
})
89-
.AddWinRTBluetooth()
90+
.AddBlueZBluetooth()
9091
;
9192

9293
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: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
// make sure it is powered on
38+
if (!await adapter.GetPoweredAsync())
39+
{
40+
await adapter.SetPoweredAsync(true);
41+
}
42+
43+
await adapter.WatchPropertiesAsync(AdapterPropertyChangedHandler);
44+
45+
return adapter;
46+
}
47+
48+
private async Task<IAdapter1> FindFirstAdapter()
49+
{
50+
var adapters = await Connection.System.FindProxies<IAdapter1>();
51+
return adapters.FirstOrDefault();
52+
}
53+
54+
private void AdapterPropertyChangedHandler(PropertyChanges changes)
55+
{
56+
_logger.LogDebug("Property changed {ChangedProperties}", changes.Changed);
57+
58+
foreach (var propertyChanged in changes.Changed)
59+
{
60+
switch (propertyChanged.Key)
61+
{
62+
case "Discovering":
63+
Discovering = (bool)propertyChanged.Value;
64+
break;
65+
}
66+
}
67+
}
68+
69+
private async Task<ICollection<IDevice1>> GetExistingDevicesAsync()
70+
=> await Connection.System.FindProxies<IDevice1>();
71+
72+
private IDevice1 GetSpecificDeviceAsync(ObjectPath objectPath)
73+
=> Connection.System.CreateProxy<IDevice1>(BlueZConstants.BlueZDBusServiceName, objectPath);
74+
75+
private async Task<bool> IsLegoWirelessProcotolDevice(IDevice1 device)
76+
=> (await device.GetUUIDsAsync()).NullToEmpty().Any(x => x.ToUpperInvariant() == PoweredUpBluetoothConstants.LegoHubService);
77+
78+
public async void Discover(Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler, CancellationToken cancellationToken = default)
79+
{
80+
_adapter ??= await GetAdapterAsync();
81+
82+
var existingDevices = await GetExistingDevicesAsync();
83+
84+
foreach (var device in existingDevices)
85+
{
86+
if (await IsLegoWirelessProcotolDevice(device))
87+
{
88+
var poweredUpDevice = new BlueZPoweredUpBluetoothDevice(device, discoveryHandler);
89+
await poweredUpDevice.Initialize();
90+
91+
_devices.Add(poweredUpDevice.DeviceInfo.BluetoothAddress, poweredUpDevice);
92+
93+
await poweredUpDevice.TryGetManufacturerDataAsync();
94+
}
95+
}
96+
97+
await Connection.System.WatchInterfacesAdded(NewDeviceAddedHandler);
98+
99+
await _adapter.SetDiscoveryFilterAsync(new Dictionary<string,object>()
100+
{
101+
{ "UUIDs", new string[] { PoweredUpBluetoothConstants.LegoHubService } }
102+
});
103+
104+
cancellationToken.Register(async () =>
105+
{
106+
if (Discovering)
107+
{
108+
await _adapter.StopDiscoveryAsync();
109+
}
110+
});
111+
112+
await _adapter.StartDiscoveryAsync();
113+
114+
async void NewDeviceAddedHandler((ObjectPath objectPath, IDictionary<string, IDictionary<string, object>> interfaces) args)
115+
{
116+
if (!args.interfaces.ContainsKey("org.bluez.Device1"))
117+
{
118+
return;
119+
}
120+
121+
var device = GetSpecificDeviceAsync(args.objectPath);
122+
var poweredUpDevice = new BlueZPoweredUpBluetoothDevice(device, discoveryHandler);
123+
124+
await poweredUpDevice.Initialize();
125+
126+
_devices.Add(poweredUpDevice.DeviceInfo.BluetoothAddress, poweredUpDevice);
127+
128+
await poweredUpDevice.TryGetManufacturerDataAsync();
129+
}
130+
}
131+
132+
public Task<IPoweredUpBluetoothDevice> GetDeviceAsync(ulong bluetoothAddress)
133+
{
134+
if (!_devices.ContainsKey(bluetoothAddress))
135+
{
136+
throw new ArgumentOutOfRangeException("Requested bluetooth device is not available from this adapter");
137+
}
138+
139+
return Task.FromResult<IPoweredUpBluetoothDevice>(_devices[bluetoothAddress]);
140+
}
141+
}
142+
}
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+
}

0 commit comments

Comments
 (0)