Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xamarin Bluetooth Stack (and Adjustment) #146

Merged
merged 11 commits into from
Mar 31, 2021
224 changes: 121 additions & 103 deletions powered-up.sln

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/SharpBrick.PoweredUp.Mobile/INativeDeviceInfoProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SharpBrick.PoweredUp.Mobile
{
public interface INativeDeviceInfoProvider
{
NativeDeviceInfo GetNativeDeviceInfo(object device);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using SharpBrick.PoweredUp.Bluetooth;

namespace SharpBrick.PoweredUp.Mobile
{
public static class IServiceCollectionExtensionsForXamarin
{
public static IServiceCollection AddXamarinBluetooth(this IServiceCollection self, INativeDeviceInfoProvider deviceInfoProvider)
=> self
.AddSingleton<IBluetoothLE>(CrossBluetoothLE.Current)
.AddSingleton<INativeDeviceInfoProvider>(deviceInfoProvider)
.AddSingleton<IPoweredUpBluetoothAdapter, XamarinPoweredUpBluetoothAdapter>();
}
}
9 changes: 9 additions & 0 deletions src/SharpBrick.PoweredUp.Mobile/NativeDeviceInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SharpBrick.PoweredUp.Mobile
{
public class NativeDeviceInfo
{
public string MacAddress { get; set; }

public ulong MacAddressNumeric { get; set; }
}
}
15 changes: 15 additions & 0 deletions src/SharpBrick.PoweredUp.Mobile/SharpBrick.PoweredUp.Mobile.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Plugin.BLE" Version="2.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SharpBrick.PoweredUp\SharpBrick.PoweredUp.csproj" />
</ItemGroup>

</Project>
122 changes: 122 additions & 0 deletions src/SharpBrick.PoweredUp.Mobile/XamarinPoweredUpBluetoothAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;
using SharpBrick.PoweredUp.Bluetooth;

namespace SharpBrick.PoweredUp.Mobile
{
public class XamarinPoweredUpBluetoothAdapter : IPoweredUpBluetoothAdapter
{
private readonly IAdapter _bluetoothAdapter;
private readonly INativeDeviceInfoProvider _deviceInfoProvider;
private readonly Dictionary<ulong, IDevice> _discoveredDevices = new Dictionary<ulong, IDevice>();

public XamarinPoweredUpBluetoothAdapter(IBluetoothLE bluetooth, INativeDeviceInfoProvider deviceInfoProvider)
{
_bluetoothAdapter = bluetooth.Adapter;
_deviceInfoProvider = deviceInfoProvider;
}

public void Discover(Func<PoweredUpBluetoothDeviceInfo, Task> discoveryHandler, CancellationToken cancellationToken = default)
{
_bluetoothAdapter.ScanMode = ScanMode.Balanced;

_bluetoothAdapter.DeviceDiscovered += ReceivedHandler;

cancellationToken.Register(async () =>
{
await _bluetoothAdapter.StopScanningForDevicesAsync().ConfigureAwait(false);
_bluetoothAdapter.DeviceDiscovered -= ReceivedHandler;
});

_bluetoothAdapter.StartScanningForDevicesAsync(new Guid[] { new Guid(PoweredUpBluetoothConstants.LegoHubService) }, DeviceFilter, false).ConfigureAwait(false);

async void ReceivedHandler(object sender, DeviceEventArgs args)
{
var info = new PoweredUpBluetoothDeviceInfo();

var advertisementRecord = args.Device.AdvertisementRecords.FirstOrDefault(x => x.Type == AdvertisementRecordType.ManufacturerSpecificData);

if (advertisementRecord?.Data?.Length > 0)
{
var data = advertisementRecord.Data.ToList();
data.RemoveRange(0, 2);
info.ManufacturerData = data.ToArray();

info.Name = args.Device.Name;
info.BluetoothAddress = _deviceInfoProvider.GetNativeDeviceInfo(args.Device.NativeDevice).MacAddressNumeric;

AddInternalDevice(args.Device, info);
await discoveryHandler(info).ConfigureAwait(false);
}
}
}

private void AddInternalDevice(IDevice device, PoweredUpBluetoothDeviceInfo info)
{
if (!_discoveredDevices.ContainsKey(info.BluetoothAddress))
{
_discoveredDevices.Add(info.BluetoothAddress, device);
}
else
{
_discoveredDevices[info.BluetoothAddress] = device;
}
}

private bool DeviceFilter(IDevice arg)
{
if (arg == null)
{
return false;
}

var manufacturerData = arg.AdvertisementRecords.FirstOrDefault(x => x.Type == AdvertisementRecordType.ManufacturerSpecificData);

if (manufacturerData?.Data == null || manufacturerData.Data.Length < 8)
{
return false;
}

// https://lego.github.io/lego-ble-wireless-protocol-docs/index.html#advertising
// Length and Data Type Name seems to be already trimmed away
// Manufacturer ID should be 0x0397 but seems in little endian encoding. I found no notice for this in the documentation except in version number encoding

return manufacturerData.Data[0] == 0x97 && manufacturerData.Data[1] == 0x03;
}

public async Task<IPoweredUpBluetoothDevice> GetDeviceAsync(ulong bluetoothAddress)
{
if (!_discoveredDevices.ContainsKey(bluetoothAddress))
{
CancellationTokenSource cts = new CancellationTokenSource(10000);

// trigger scan for 10 seconds
Discover((deviceInfo) =>
{
return Task.Run(() =>
{
cts.Cancel(false);
});

}, cts.Token);

// 60 seconds will be ignored here, because the cancelation will happen after 10 seconds
await Task.Delay(60000, cts.Token).ContinueWith(task => { });
tthiery marked this conversation as resolved.
Show resolved Hide resolved

if (!_discoveredDevices.ContainsKey(bluetoothAddress))
{
throw new NotSupportedException("Given bt address does not belong to a discovered device");
}
}

return new XamarinPoweredUpBluetoothDevice(_discoveredDevices[bluetoothAddress], _bluetoothAdapter);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;
using SharpBrick.PoweredUp.Bluetooth;

namespace SharpBrick.PoweredUp.Mobile
{
public class XamarinPoweredUpBluetoothCharacteristic : IPoweredUpBluetoothCharacteristic
{
private ICharacteristic _characteristic;

public Guid Uuid => _characteristic.Id;

public XamarinPoweredUpBluetoothCharacteristic(ICharacteristic characteristic)
{
this._characteristic = characteristic;
}

public async Task<bool> NotifyValueChangeAsync(Func<byte[], Task> notificationHandler)
{
if (notificationHandler is null)
{
throw new ArgumentNullException(nameof(notificationHandler));
}

_characteristic.ValueUpdated += ValueUpdatedHandler;

void ValueUpdatedHandler(object sender, CharacteristicUpdatedEventArgs e)
{
notificationHandler(e.Characteristic.Value);
}

await _characteristic.StartUpdatesAsync();

return true;
}

public async Task<bool> WriteValueAsync(byte[] data)
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}

return await _characteristic.WriteAsync(data);
}
}
}
49 changes: 49 additions & 0 deletions src/SharpBrick.PoweredUp.Mobile/XamarinPoweredUpBluetoothDevice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using SharpBrick.PoweredUp.Bluetooth;

namespace SharpBrick.PoweredUp.Mobile
{
public class XamarinPoweredUpBluetoothDevice : IPoweredUpBluetoothDevice
{
private IDevice _device;
private IAdapter _adapter;

public string Name => this._device.Name;

public XamarinPoweredUpBluetoothDevice(IDevice device, IAdapter bluetoothAdapter)
{
this._device = device;
this._adapter = bluetoothAdapter;
}

#region IDisposible

~XamarinPoweredUpBluetoothDevice() => Dispose(false);

public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }

protected virtual void Dispose(bool disposing)
{
_device?.Dispose();
_device = null;
_adapter = null;
}

#endregion

public async Task<IPoweredUpBluetoothService> GetServiceAsync(Guid serviceId)
{
await _adapter.ConnectToDeviceAsync(_device, new ConnectParameters(true, true)).ConfigureAwait(false);

if (!_adapter.ConnectedDevices.Contains(_device)) return null;

var service = await _device.GetServiceAsync(serviceId).ConfigureAwait(false);

return new XamarinPoweredUpBluetoothService(service);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Threading.Tasks;
using Plugin.BLE.Abstractions.Contracts;
using SharpBrick.PoweredUp.Bluetooth;

namespace SharpBrick.PoweredUp.Mobile
{
public class XamarinPoweredUpBluetoothService : IPoweredUpBluetoothService
{
private IService _service;

public Guid Uuid => _service.Id;

public XamarinPoweredUpBluetoothService(IService service)
{
_service = service ?? throw new ArgumentNullException(nameof(service));
}

#region IDisposable

~XamarinPoweredUpBluetoothService() => Dispose(false);

public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }

protected virtual void Dispose(bool disposing)
{
_service.Dispose();
_service = null;
}

#endregion

public async Task<IPoweredUpBluetoothCharacteristic> GetCharacteristicAsync(Guid guid)
{
var characteristic = await _service.GetCharacteristicAsync(guid);

if (characteristic == null) return null;

// await characteristic.StartUpdatesAsync();
return new XamarinPoweredUpBluetoothCharacteristic(characteristic);

}
}
}
10 changes: 10 additions & 0 deletions src/SharpBrick.PoweredUp/CompilerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace System.Runtime.CompilerServices
{
#if NETSTANDARD2_1
/// <summary>
/// Dummy compilerServices which is only included in .NET5 (upwards). To be compatible with .NetStandard 2.1 a dummy is required.
/// <see href="https://developercommunity.visualstudio.com/t/error-cs0518-predefined-type-systemruntimecompiler/1244809" />
/// </summary>
internal static class IsExternalInit { }
#endif
}
3 changes: 2 additions & 1 deletion src/SharpBrick.PoweredUp/SharpBrick.PoweredUp.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFrameworks>netstandard2.1;net5.0</TargetFrameworks>
Berdsen marked this conversation as resolved.
Show resolved Hide resolved
<LangVersion>9.0</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down