mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-11-01 00:46:33 +01:00
305 lines
11 KiB
C#
305 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);
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|