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/INativeDeviceInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace SharpBrick.PoweredUp.Mobile
{
public interface INativeDeviceInfo
Berdsen marked this conversation as resolved.
Show resolved Hide resolved
{
NativeDevice 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, INativeDeviceInfo deviceInfo)
=> self
.AddSingleton<IBluetoothLE>(CrossBluetoothLE.Current)
.AddSingleton<INativeDeviceInfo>(deviceInfo)
.AddSingleton<IPoweredUpBluetoothAdapter, XamarinPoweredUpBluetoothAdapter>();
}
}
9 changes: 9 additions & 0 deletions src/SharpBrick.PoweredUp.Mobile/NativeDevice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SharpBrick.PoweredUp.Mobile
{
public class NativeDevice
{
public string MacAddress { get; set; }

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

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

<ItemGroup>
<PackageReference Include="Plugin.BLE" Version="2.1.1" />
<PackageReference Include="Prism.Core" Version="8.0.0.1909" />
Berdsen marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

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

</Project>
142 changes: 142 additions & 0 deletions src/SharpBrick.PoweredUp.Mobile/XamarinPoweredUpBluetoothAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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 INativeDeviceInfo _deviceInfo;
private readonly Dictionary<ulong, IDevice> _discoveredDevices = new Dictionary<ulong, IDevice>();

public XamarinPoweredUpBluetoothAdapter(IBluetoothLE bluetooth, INativeDeviceInfo deviceInfo)
Berdsen marked this conversation as resolved.
Show resolved Hide resolved
{
_bluetoothAdapter = bluetooth.Adapter;
_deviceInfo = deviceInfo;
}

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 = _deviceInfo.GetNativeDeviceInfo(args.Device.NativeDevice).MacAddressNumeric;
Berdsen marked this conversation as resolved.
Show resolved Hide resolved

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;
Berdsen marked this conversation as resolved.
Show resolved Hide resolved

System.Diagnostics.Debug.WriteLine(arg.Name);
Berdsen marked this conversation as resolved.
Show resolved Hide resolved

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

switch (manufacturerData.Data[3])
Berdsen marked this conversation as resolved.
Show resolved Hide resolved
{
case 0x00:
System.Diagnostics.Debug.WriteLine("System: LEGO Wedo 2.0, Device: WeDo Hub");
break;
case 0x20:
System.Diagnostics.Debug.WriteLine("System: LEGO Duplo, Device: Duplo Train");
break;
case 0x40:
System.Diagnostics.Debug.WriteLine("System: System, Device: Boost Hub");
break;
case 0x41:
System.Diagnostics.Debug.WriteLine("System: System, Device: 2 Port Hub");
break;
case 0x42:
System.Diagnostics.Debug.WriteLine("System: System, Device: 2 Port Handset");
break;
default:
if (manufacturerData.Data[3] >= 96 && manufacturerData.Data[3] < 128)
{
System.Diagnostics.Debug.WriteLine("System: LEGO System, Device: Currently unknown");
}
break;
}

return manufacturerData.Data[0] == 0x97 || manufacturerData.Data[1] == 0x03;
tthiery marked this conversation as resolved.
Show resolved Hide resolved
}

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