sharee.bike-App/LockItBLE/Services/BluetoothLock/BLE/LockItByScanService.cs
2022-09-20 13:51:55 +02:00

310 lines
11 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Plugin.BLE;
using Plugin.BLE.Abstractions;
using Plugin.BLE.Abstractions.Contracts;
using Serilog;
using TINK.Model.Connector;
using TINK.Model.Device;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.Tdo;
using Xamarin.Essentials;
namespace TINK.Services.BluetoothLock.BLE
{
/// <summary> Manages ILockIt- Locks.</summary>
public abstract class LockItByScanServiceBase : LockItServiceBase
{
/// <summary> Service to manage bluetooth stack. </summary>
private IBluetoothLE BluetoothService { get; }
/// <summary> Returns true if location permission is required (Android) but on given. </summary>
private Func<Task<bool>> IsLocationPermissionMissingDelegate { get; }
/// <summary> Returns true if location service is required (Android) and off. </summary>
private Func<bool> IsLocationRequiredAndOffDelegate { get; }
private Func<IDevice, LockInfoAuthTdo, IAdapter, Task<LockItBase>> AuthenticateDelegate { get; set; }
public LockItByScanServiceBase(
ICipher cipher,
Func<IDevice, LockInfoAuthTdo, IAdapter, Task<LockItBase>> authenticateDelegate,
IBluetoothLE bluetoothLE,
Func<Task<bool>> isLocationPermissionMissingDelegate,
Func<bool> isLocationRequiredAndOffDelegate) : base(cipher)
{
BluetoothService = bluetoothLE
?? throw new ArgumentException($"Can not instantiate {nameof(LockItByScanServiceBase)}- object. No bluetooth service available.");
IsLocationPermissionMissingDelegate = isLocationPermissionMissingDelegate
?? throw new ArgumentException($"Can not instantiate {nameof(LockItByScanServiceBase)}- object. No location permission missing delegate available.");
IsLocationRequiredAndOffDelegate = isLocationRequiredAndOffDelegate
?? throw new ArgumentException($"Can not instantiate {nameof(LockItByScanServiceBase)}- object. No location .");
AuthenticateDelegate = authenticateDelegate;
}
/// <summary> Connects to lock.</summary>
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
/// <param name="authInfo"> Info required to connect to lock.</param>
/// <param name="connectTimeout">Timeout for connect operation.</param>
public async Task<LockInfoTdo> ConnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout)
{
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
{
throw new System.Exception("Can not connect to lock by name. Platform must not be unknown and bluetooth code must be run on main thread.");
}
Log.ForContext<LockItByScanServiceBase>().Debug($"Request to connect to device {authInfo.Id}.");
var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id || x.Guid == authInfo.Guid);
if (lockIt != null && lockIt.GetDeviceState() == DeviceState.Connected)
{
// Nothing to do
Log.ForContext<LockItByScanServiceBase>().Debug($"Nothing to do. Lock is already connected.");
return await lockIt.GetLockStateAsync();
}
if (lockIt != null)
{
// Reconnect required
Log.ForContext<LockItByScanServiceBase>().Debug($"Device {lockIt.Name} has been already connected but needs reconnect.");
await lockIt.ReconnectAsync(authInfo, connectTimeout);
return await lockIt.GetLockStateAsync();
}
// Device has not yet been discovered.
Log.ForContext<LockItByScanServiceBase>().Verbose("Starting scan for new devices...");
var newlyDiscovertedDevice = await ScanForNewDevices(new List<int> { authInfo.Id }, connectTimeout);
var bleDevice = newlyDiscovertedDevice.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id);
if (bleDevice == null)
{
// Try to check why lock was not found.
var bluetoothState = await BluetoothService.GetBluetoothState();
switch (bluetoothState)
{
case BluetoothState.On:
break;
case BluetoothState.Off:
Log.ForContext<LockItByScanServiceBase>().Debug("Lock probable not found because bluetooth is off.");
throw new ConnectBluetoothNotOnException();
default:
Log.ForContext<LockItByScanServiceBase>().Debug("Lock probable not found because unexpected bluetooth state {state} detected.", bluetoothState);
throw new ConnectBluetoothNotOnException(bluetoothState);
}
if (await IsLocationPermissionMissingDelegate())
{
Log.ForContext<LockItByScanServiceBase>().Debug("Lock probable not found because missing location permission.");
throw new ConnectLocationPermissionMissingException();
}
if (IsLocationRequiredAndOffDelegate())
{
Log.ForContext<LockItByScanServiceBase>().Debug("Lock probable not found because location is off.");
throw new ConnectLocationOffException();
}
Log.ForContext<LockItByScanServiceBase>().Debug("Can not connect because device was not discovered. List of discovered devices {deviceList}.", String.Join(";", newlyDiscovertedDevice?.Select(x => x?.Id)));
throw new OutOfReachException();
}
var adapter = CrossBluetoothLE.Current.Adapter;
var cts = new CancellationTokenSource(connectTimeout);
// Connect to device and authenticate.
Log.ForContext<LockItByScanServiceBase>().Debug($"Device sucessfully discovered. Request connect to device {bleDevice?.Name}. Connect state is {bleDevice?.State}.");
try
{
await adapter.ConnectToDeviceAsync(
bleDevice,
new ConnectParameters(forceBleTransport: true /* Force BLE transport */),
cts.Token);
}
catch (System.Exception exception)
{
Log.ForContext<LockItByScanServiceBase>().Error("Can not connect to device by name. {Exception}", exception);
if (exception is TaskCanceledException)
{
// A timeout occurred.
throw new System.Exception($"Can not connect to lock by name.\r\nTimeout is {connectTimeout.TotalMilliseconds} [ms].\r\n{exception.Message}", exception);
}
Log.ForContext<LockItByScanServiceBase>().Error("Bluetooth connect to known device request failed. {Exception}", exception);
throw new System.Exception($"Can not connect to lock by name. {exception.Message}", exception);
}
lockIt = await AuthenticateDelegate(bleDevice, authInfo, CrossBluetoothLE.Current.Adapter);
if (lockIt == null)
{
await adapter.DisconnectDeviceAsync(bleDevice);
return await Task.FromResult<LockInfoTdo>(null);
}
DeviceList.Add(lockIt);
await lockIt.GetVersionInfoAsync();
return await lockIt.GetLockStateAsync();
}
/// <summary> Checks for locks which have not yet been discoverted and connects them. </summary>
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be connected. </remarks>
/// <param name="locksInfo">Locks to reconnect.</param>
/// <param name="connectTimeout">Timeout for connect operation of a single lock.</param>
protected override async Task<IEnumerable<ILockService>> CheckConnectMissing(IEnumerable<LockInfoAuthTdo> locksInfo, TimeSpan connectTimeout)
{
// Get list of target locks without invalid entries.
var validLocksInfo = locksInfo
.Where(x => x.IsIdValid)
.Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0)
.ToList();
// Get Locks to scan for
var locksInfoToScanFor = validLocksInfo.Where(x => !DeviceList.Any(y => y.Name.GetBluetoothLockId() == x.Id)).ToList();
// Check if bluetooth scan is required
if (locksInfoToScanFor.Count <= 0)
{
// No Scan required.
return new List<LockItBase>();
}
// Do bluetooth scan
var scanTargets = locksInfoToScanFor.Select(x => x.Id).ToList();
var newlyDiscovertedDevicesList = await ScanForNewDevices(scanTargets, TimeSpan.FromSeconds(connectTimeout.TotalSeconds * scanTargets.Count));
var locksList = new List<LockItBase>();
// Connect to newly discovered devices.
foreach (var lockInfo in locksInfoToScanFor)
{
var device = newlyDiscovertedDevicesList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == lockInfo.Id);
if (device == null)
{
// No device found for given lock.
continue;
}
Log.ForContext<LockItByScanServiceBase>().Debug($"Request connect to device {device?.Name}. Connect state is {device?.State}.");
var cts = new CancellationTokenSource(connectTimeout);
try
{
// Connect to device and authenticate.
await CrossBluetoothLE.Current.Adapter.ConnectToDeviceAsync(
device,
new ConnectParameters(forceBleTransport: true), // Force BLE transport
cts.Token);
}
catch (System.Exception exception)
{
Log.ForContext<LockItByScanServiceBase>().Error("Can not connect to lock. {Exception}", exception);
continue;
}
LockItBase lockIt = null;
try
{
lockIt = await AuthenticateDelegate(device, lockInfo, CrossBluetoothLE.Current.Adapter);
}
catch (System.Exception exception)
{
// Member is called for background update of missing devices.
// Do not display any error messages.
Log.ForContext<LockItByScanServiceBase>().Error($"Authentication failed. {exception.Message}");
continue;
}
if (lockIt == null)
{
// connectiong to device succeded.
continue;
}
Log.ForContext<LockItByScanServiceBase>().Debug($"Auth succeeded for device {device}.");
locksList.Add(lockIt);
await lockIt.GetVersionInfoAsync();
}
return locksList;
}
/// <summary> Scans for a given set of devices.</summary>
/// <param name="targetLocks">Locks to scan for.</param>
/// <returns></returns>
private static async Task<IEnumerable<IDevice>> ScanForNewDevices(
IEnumerable<int> targetLocks,
TimeSpan connectTimeout)
{
var adapter = CrossBluetoothLE.Current.Adapter;
var newlyDiscovertedDevicesList = new List<IDevice>();
var cts = new CancellationTokenSource(connectTimeout);
// Not all devices already connected.
// Start scanning
adapter.DeviceDiscovered += (s, a) =>
{
var name = a.Device.Name;
if (string.IsNullOrEmpty(name))
{
// Lock without name detected.
Log.ForContext<LockItByScanServiceBase>().Debug($"Ble decvice without name discovered. RSSI={a.Device.Rssi}, GUID={a.Device.Id}, State={a.Device.State}.");
return;
}
if (targetLocks.Where(x => x != TextToLockItTypeHelper.INVALIDLOCKID).Contains(name.GetBluetoothLockId()))
{
Log.ForContext<LockItByScanServiceBase>().Debug($"New LOCKIT device {name} discovered. RSSI={a.Device.Rssi}, GUID={a.Device.Id}, State={a.Device.State}.");
newlyDiscovertedDevicesList.Add(a.Device);
if (newlyDiscovertedDevicesList.Count() >= targetLocks.Count())
{
cts.Cancel();
}
return;
}
Log.ForContext<LockItByScanServiceBase>().Verbose($"Device of unknown advertisement name {name} discovered. RSSI={a.Device.Rssi}, GUID={a.Device.Id}, State={a.Device.State}.");
};
try
{
await adapter.StartScanningForDevicesAsync(cancellationToken: cts.Token);
}
catch (System.Exception exception)
{
Log.ForContext<LockItByScanServiceBase>().Error("Bluetooth scan failed. {Exception}", exception);
throw;
}
finally
{
try
{
await adapter.StopScanningForDevicesAsync();
}
catch (System.Exception exception)
{
Log.ForContext<LockItByScanServiceBase>().Error("Stop scanning failed. {Excpetion}", exception);
throw;
}
}
return newlyDiscovertedDevicesList;
}
}
}