mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-12-22 06:56:25 +01:00
Initial version.
This commit is contained in:
parent
6bab491a21
commit
193aaa1a56
17 changed files with 3142 additions and 0 deletions
19
LockItBLE/LockItBLE.csproj
Normal file
19
LockItBLE/LockItBLE.csproj
Normal file
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<RootNamespace>TINK</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Plugin.BLE" Version="2.1.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.1" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LockItShared\LockItShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
31
LockItBLE/LockItBLE.sln
Normal file
31
LockItBLE/LockItBLE.sln
Normal file
|
@ -0,0 +1,31 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31229.75
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LockItBLE", "LockItBLE.csproj", "{1F3B7642-9F98-48FB-9F3A-423C6EC06F45}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LockItShared", "..\LockItShared\LockItShared.csproj", "{EC2289B8-8758-46E1-B813-89EF79662CE8}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1F3B7642-9F98-48FB-9F3A-423C6EC06F45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1F3B7642-9F98-48FB-9F3A-423C6EC06F45}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1F3B7642-9F98-48FB-9F3A-423C6EC06F45}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1F3B7642-9F98-48FB-9F3A-423C6EC06F45}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EC2289B8-8758-46E1-B813-89EF79662CE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EC2289B8-8758-46E1-B813-89EF79662CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EC2289B8-8758-46E1-B813-89EF79662CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC2289B8-8758-46E1-B813-89EF79662CE8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {EB92666E-7E16-4AEB-9DA8-919E409198AF}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
1045
LockItBLE/Services/BluetoothLock/BLE/LockItBase.cs
Normal file
1045
LockItBLE/Services/BluetoothLock/BLE/LockItBase.cs
Normal file
File diff suppressed because it is too large
Load diff
162
LockItBLE/Services/BluetoothLock/BLE/LockItByGuidService.cs
Normal file
162
LockItBLE/Services/BluetoothLock/BLE/LockItByGuidService.cs
Normal file
|
@ -0,0 +1,162 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Plugin.BLE;
|
||||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using Plugin.BLE.Abstractions;
|
||||
using System.Linq;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
using TINK.Model.Connector;
|
||||
using Serilog;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using TINK.Services.BluetoothLock.Exception;
|
||||
using Xamarin.Essentials;
|
||||
using TINK.Model.Device;
|
||||
using LockItShared.Services.BluetoothLock;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class LockItByGuidService : LockItServiceBase, ILocksService
|
||||
{
|
||||
/// <summary> Constructs a LockItByGuidService object.</summary>
|
||||
/// <param name="cipher">Encrpyting/ decrypting object.</param>
|
||||
public LockItByGuidService(ICipher cipher) : base(cipher)
|
||||
{
|
||||
}
|
||||
|
||||
/// <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. </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)
|
||||
{
|
||||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||||
{
|
||||
throw new System.Exception("Can not connect to locks by guid. Bluetooth code must be run on main thread");
|
||||
}
|
||||
|
||||
// Get list of target locks without invalid entries.
|
||||
var validLocksInfo = locksInfo
|
||||
.Where(x => x.IsGuidValid)
|
||||
.Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0)
|
||||
.ToList();
|
||||
|
||||
var locksList = new List<ILockService>();
|
||||
|
||||
// Connect to
|
||||
foreach (var lockInfo in validLocksInfo)
|
||||
{
|
||||
if (DeviceList.Any(x => x.Name.GetBluetoothLockId() == lockInfo.Id || x.Guid == lockInfo.Guid))
|
||||
{
|
||||
// Device is already connected.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Connect to device and authenticate.
|
||||
ILockService lockIt = null;
|
||||
try
|
||||
{
|
||||
lockIt = await ConnectByGuid(lockInfo, connectTimeout);
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
// Member is called for background update of missing devices.
|
||||
// Do not display any error messages.
|
||||
Log.ForContext<LockItByGuidService>().Error($"Authentication failed. {exception.Message}");
|
||||
continue;
|
||||
}
|
||||
if (lockIt == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
locksList.Add(lockIt);
|
||||
}
|
||||
|
||||
return locksList;
|
||||
}
|
||||
|
||||
/// <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 guid. Bluetooth code must be run on main thread");
|
||||
}
|
||||
|
||||
// Connect to device and authenticate.
|
||||
var lockIt = await ConnectByGuid(authInfo, connectTimeout);
|
||||
|
||||
if (lockIt == null)
|
||||
{
|
||||
return new LockInfoTdo.Builder { Id = authInfo.Id, Guid = authInfo.Guid, State = null }.Build();
|
||||
}
|
||||
|
||||
DeviceList.Add(lockIt);
|
||||
|
||||
return await lockIt.GetLockStateAsync();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private async Task<ILockService> ConnectByGuid(LockInfoAuthTdo authInfo, TimeSpan connectTimeout)
|
||||
{
|
||||
if (authInfo.Guid == TextToLockItTypeHelper.INVALIDLOCKGUID)
|
||||
{
|
||||
Log.ForContext<LockItByGuidService>().Error($"Can not connect to lock {authInfo.Id}. Guid is unknown.");
|
||||
throw new GuidUnknownException();
|
||||
}
|
||||
|
||||
var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id || x.Guid == authInfo.Guid);
|
||||
if (lockIt != null && lockIt.GetDeviceState() == DeviceState.Connected)
|
||||
{
|
||||
// Device is already connected.
|
||||
return lockIt;
|
||||
}
|
||||
|
||||
var adapter = CrossBluetoothLE.Current.Adapter;
|
||||
Log.ForContext<LockItByGuidService>().Debug($"Request connect to device {authInfo.Id}.");
|
||||
|
||||
if (LockItByGuidServiceHelper.DevelGuids.ContainsKey(authInfo.Id) && LockItByGuidServiceHelper.DevelGuids[authInfo.Id] != authInfo.Guid)
|
||||
throw new System.Exception($"Invalid Guid {authInfo.Guid} for lock with id {authInfo.Id} detected. Guid should be {LockItByGuidServiceHelper.DevelGuids[authInfo.Id]}.");
|
||||
|
||||
IDevice device;
|
||||
var cts = new CancellationTokenSource(connectTimeout);
|
||||
// Step 1: Perform bluetooth connect.
|
||||
try
|
||||
{
|
||||
device = await adapter.ConnectToKnownDeviceAsync(
|
||||
authInfo.Guid,
|
||||
new ConnectParameters(forceBleTransport: true), // Force BLE transport
|
||||
cts.Token);
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
if (exception is TaskCanceledException)
|
||||
{
|
||||
// A timeout occurred.
|
||||
throw new System.Exception($"Can not connect to lock by guid.\r\nTimeout of {connectTimeout.TotalMilliseconds} [ms] elapsed.", exception);
|
||||
}
|
||||
|
||||
Log.ForContext<LockItByGuidService>().Error("Bluetooth connect to known device request failed. {Exception}", exception);
|
||||
throw new System.Exception($"Can not connect to lock by guid.\r\n{exception.Message}", exception);
|
||||
}
|
||||
|
||||
// Step 2: Authenticate.
|
||||
lockIt = await LockItEventBased.Authenticate(device, authInfo, CrossBluetoothLE.Current.Adapter, Cipher);
|
||||
|
||||
if (lockIt == null)
|
||||
{
|
||||
Log.ForContext<LockItByGuidService>().Error("Connect to device failed.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return lockIt;
|
||||
}
|
||||
}
|
||||
}
|
256
LockItBLE/Services/BluetoothLock/BLE/LockItByScanService.cs
Normal file
256
LockItBLE/Services/BluetoothLock/BLE/LockItByScanService.cs
Normal file
|
@ -0,0 +1,256 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Plugin.BLE;
|
||||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using System.Linq;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
using Serilog;
|
||||
using Plugin.BLE.Abstractions;
|
||||
using TINK.Model.Connector;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using Xamarin.Essentials;
|
||||
using TINK.Services.BluetoothLock.Exception;
|
||||
using TINK.Model.Device;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
/// <summary> Manages ILockIt- Locks.</summary>
|
||||
public abstract class LockItByScanServiceBase : LockItServiceBase
|
||||
{
|
||||
private Func<IDevice, LockInfoAuthTdo, IAdapter, Task<LockItBase>> AuthenticateDelegate { get; set; }
|
||||
|
||||
public LockItByScanServiceBase(
|
||||
ICipher cipher,
|
||||
Func<IDevice, LockInfoAuthTdo, IAdapter, Task<LockItBase>> authenticateDelegate) : base(cipher)
|
||||
{
|
||||
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. 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
|
||||
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.
|
||||
var newlyDiscovertedDevice = await ScanForNewDevices(new List<int> { authInfo.Id }, connectTimeout);
|
||||
|
||||
var bleDevice = newlyDiscovertedDevice.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id);
|
||||
|
||||
if (bleDevice == null)
|
||||
{
|
||||
|
||||
Log.ForContext<LockItByScanServiceBase>().Debug("Can not connect because device was not discovered.");
|
||||
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<LockItBase>().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<LockItBase>().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($"Lock without name discovered.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetLocks.Where(x => x != TextToLockItTypeHelper.INVALIDLOCKID).Contains(name.GetBluetoothLockId()))
|
||||
{
|
||||
Log.ForContext<LockItByScanServiceBase>().Debug($"New LOCKIT device {name} discovered.");
|
||||
|
||||
newlyDiscovertedDevicesList.Add(a.Device);
|
||||
|
||||
if (newlyDiscovertedDevicesList.Count() >= targetLocks.Count())
|
||||
{
|
||||
cts.Cancel();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Log.ForContext<LockItByScanServiceBase>().Verbose($"Device of unknown advertisement name {name} discovered.");
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using TINK.Model.Device;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class LockItByScanServiceEventBased : LockItByScanServiceBase, ILocksService
|
||||
{
|
||||
public LockItByScanServiceEventBased(ICipher cipher) : base(
|
||||
cipher,
|
||||
(bleDevice, authInfo, adapter) => LockItEventBased.Authenticate(bleDevice, authInfo, adapter, cipher)) { }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using TINK.Model.Device;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class LockItByScanServicePolling : LockItByScanServiceBase, ILocksService
|
||||
{
|
||||
public LockItByScanServicePolling(ICipher cipher) : base(
|
||||
cipher,
|
||||
(bleDevice, authInfo, adapter) => LockItPolling.Authenticate(bleDevice, authInfo, adapter, cipher)) { }
|
||||
}
|
||||
}
|
308
LockItBLE/Services/BluetoothLock/BLE/LockItEventBased.cs
Normal file
308
LockItBLE/Services/BluetoothLock/BLE/LockItEventBased.cs
Normal file
|
@ -0,0 +1,308 @@
|
|||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using Plugin.BLE.Abstractions.EventArgs;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TINK.Model.Bike.BluetoothLock;
|
||||
using TINK.Model.Device;
|
||||
using TINK.Services.BluetoothLock.Exception;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class LockItEventBased : LockItBase
|
||||
{
|
||||
private LockItEventBased(IDevice device, IAdapter adapter, ICipher cipher) : base(device, adapter, cipher)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary> Reconnects to device. </summary>
|
||||
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
|
||||
/// <param name="authInfo">Info required to connect.</param>
|
||||
/// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param>
|
||||
/// <returns>True if connecting succeeded, false if not.</returns>
|
||||
public async override Task ReconnectAsync(
|
||||
LockInfoAuthTdo authInfo,
|
||||
TimeSpan connectTimeout)
|
||||
=> await ReconnectAsync(
|
||||
authInfo,
|
||||
connectTimeout,
|
||||
() => new LockItEventBased(Device, Adapter, Cipher));
|
||||
|
||||
/// <summary> Connects to device. </summary>
|
||||
/// <param name="authInfo">Info required to connect.</param>
|
||||
/// <param name="device">Device with must be connected.</param>
|
||||
/// <returns>True if connecting succeeded, false if not.</returns>
|
||||
public static async Task<LockItBase> Authenticate(
|
||||
IDevice device,
|
||||
LockInfoAuthTdo authInfo,
|
||||
IAdapter adapter,
|
||||
ICipher cipher)
|
||||
=> await Authenticate(
|
||||
device,
|
||||
authInfo,
|
||||
adapter,
|
||||
cipher,
|
||||
() => new LockItEventBased(device, adapter, cipher));
|
||||
|
||||
/// <summary> Opens lock. </summary>
|
||||
/// <returns> Locking state.</returns>
|
||||
public async override Task<LockitLockingState?> OpenAsync()
|
||||
{
|
||||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||||
{
|
||||
throw new System.Exception("Can not open lock. Bluetooth code must be run on main thread");
|
||||
}
|
||||
|
||||
LockitLockingState? lockingState = (await GetLockStateAsync())?.State;
|
||||
if (lockingState == null)
|
||||
{
|
||||
// Device not reachable.
|
||||
Log.ForContext<LockItEventBased>().Information("Can not open lock. Device is not reachable (get state).");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
if (lockingState.Value.GetLockingState() == LockingState.Open)
|
||||
{
|
||||
// Lock is already open.
|
||||
Log.ForContext<LockItEventBased>().Information("No need to open lock. Lock is already open.");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
Log.ForContext<LockItEventBased>().Debug($"Request to closed lock. Current lockikng state is {lockingState}, counter value {ActivateLockWriteCounter}.");
|
||||
|
||||
double batteryPercentage = double.NaN;
|
||||
|
||||
var lockStateCharacteristic = await GetStateCharacteristicAsync();
|
||||
var batteryStateCharacteristic = await GetBatteryCharacteristicAsync();
|
||||
|
||||
TaskCompletionSource<LockitLockingState?> tcs = new TaskCompletionSource<LockitLockingState?>();
|
||||
var cts = new CancellationTokenSource(OPEN_CLOSE_TIMEOUT_MS);
|
||||
cts.Token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false);
|
||||
|
||||
EventHandler<CharacteristicUpdatedEventArgs> lockStateCharcteristicChanged = (sender, args) => GetStateValue(tcs, args.Characteristic.Value);
|
||||
EventHandler<CharacteristicUpdatedEventArgs> batteryStateCharcteristicChanged = (sender, args) => batteryPercentage = GetChargeValue(args.Characteristic.Value);
|
||||
try
|
||||
{
|
||||
lockStateCharacteristic.ValueUpdated += lockStateCharcteristicChanged;
|
||||
batteryStateCharacteristic.ValueUpdated += batteryStateCharcteristicChanged;
|
||||
} catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Subscribing to events when opening lock failed.{Exception}", ex);
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await lockStateCharacteristic.StartUpdatesAsync();
|
||||
await batteryStateCharacteristic.StartUpdatesAsync();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Starting updates wen opening lock failed.{Exception}", ex);
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await OpenCloseLock(true); // Close lock;
|
||||
|
||||
if (!result)
|
||||
{
|
||||
// State did not change. Return previous state.
|
||||
Log.ForContext<LockItEventBased>().Information($"Opening lock failed.");
|
||||
return await Task.FromResult(lockingState.Value);
|
||||
}
|
||||
|
||||
// Wait until event has been received.
|
||||
lockingState = await tcs.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await lockStateCharacteristic.StopUpdatesAsync();
|
||||
await batteryStateCharacteristic.StopUpdatesAsync();
|
||||
|
||||
lockStateCharacteristic.ValueUpdated -= lockStateCharcteristicChanged;
|
||||
batteryStateCharacteristic.ValueUpdated -= batteryStateCharcteristicChanged;
|
||||
} catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Sopping updates/ unsubscribing from events when opening lock failed.{Exception}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (lockingState == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (lockingState.Value)
|
||||
{
|
||||
case LockitLockingState.CouldntOpenBoldBlocked:
|
||||
// Expected error. ILockIt count not be opened (Spoke blocks lock, ....)
|
||||
throw new CouldntOpenBoldBlockedException();
|
||||
|
||||
case LockitLockingState.Open:
|
||||
return lockingState.Value;
|
||||
|
||||
default:
|
||||
// Comprises values
|
||||
// - LockitLockingState.Closed
|
||||
// - LockitLockingState.Unknown
|
||||
// - LockitLockingState.CouldntOpenBoldBlocked
|
||||
// Internal error which sould never occure. Lock refuses to open but connection is ok.
|
||||
throw new CouldntOpenInconsistentStateExecption(lockingState.Value.GetLockingState());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Close the lock.</summary>
|
||||
/// <returns>Locking state.</returns>
|
||||
public async override Task<LockitLockingState?> CloseAsync()
|
||||
{
|
||||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||||
{
|
||||
throw new System.Exception("Can not close lock. Bluetooth code must be run on main thread");
|
||||
}
|
||||
|
||||
// Get current state
|
||||
LockitLockingState? lockingState = (await GetLockStateAsync()).State;
|
||||
if (lockingState == null)
|
||||
{
|
||||
// Device not reachable.
|
||||
Log.ForContext<LockItEventBased>().Error("Can not close lock. Device is not reachable (get state).");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
if (lockingState.Value.GetLockingState() == LockingState.Closed)
|
||||
{
|
||||
// Lock is already closed.
|
||||
Log.ForContext<LockItEventBased>().Error("No need to close lock. Lock is already closed.");
|
||||
return await Task.FromResult(lockingState.Value);
|
||||
}
|
||||
|
||||
lockingState = null;
|
||||
var lockStateCharacteristic = await GetStateCharacteristicAsync();
|
||||
|
||||
TaskCompletionSource<LockitLockingState?> tcs = new TaskCompletionSource<LockitLockingState?>();
|
||||
var cts = new CancellationTokenSource(OPEN_CLOSE_TIMEOUT_MS);
|
||||
cts.Token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false);
|
||||
|
||||
EventHandler<CharacteristicUpdatedEventArgs> lockStateCharcteristicChanged = (sender, args) => GetStateValue(tcs, args.Characteristic.Value);
|
||||
|
||||
try
|
||||
{
|
||||
lockStateCharacteristic.ValueUpdated += lockStateCharcteristicChanged;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Subscribing to events when closing lock failed.{Exception}", ex);
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
try
|
||||
{
|
||||
await lockStateCharacteristic.StartUpdatesAsync();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Starting update when closing lock failed.{Exception}", ex);
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Debug($"Request to closed lock. Current state is {lockingState}, counter value {ActivateLockWriteCounter}.");
|
||||
var result = await OpenCloseLock(false); // Close lock
|
||||
if (!result)
|
||||
{
|
||||
// State did not change. Return previous state.
|
||||
Log.ForContext<LockItEventBased>().Information($"Closing lock failed.");
|
||||
return await Task.FromResult(lockingState.Value);
|
||||
}
|
||||
|
||||
// Wait until event has been received.
|
||||
lockingState = await tcs.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
await lockStateCharacteristic.StopUpdatesAsync();
|
||||
|
||||
lockStateCharacteristic.ValueUpdated -= lockStateCharcteristicChanged;
|
||||
} catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Sopping update/ unsubscribing from events when closing lock failed.{Exception}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (lockingState == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (lockingState.Value)
|
||||
{
|
||||
case LockitLockingState.CouldntCloseBoldBlocked:
|
||||
// Expected error. ILockIt could not be closed (Spoke blocks lock, ....)
|
||||
throw new CouldntCloseBoldBlockedException();
|
||||
|
||||
case LockitLockingState.CouldntCloseMoving:
|
||||
// Expected error. ILockIt could not be closed (bike is moving)
|
||||
throw new CounldntCloseMovingException();
|
||||
|
||||
case LockitLockingState.Closed:
|
||||
return lockingState;
|
||||
|
||||
default:
|
||||
// Comprises values
|
||||
// - LockitLockingState.Open
|
||||
// - LockitLockingState.Unknown
|
||||
// - LockitLockingState.CouldntOpenBoldBlocked
|
||||
// Internal error which sould never occurre. Lock refuses to close but connection is ok.
|
||||
throw new CouldntCloseInconsistentStateExecption(lockingState.Value.GetLockingState());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets the locking state from event argument. </summary>
|
||||
Action<TaskCompletionSource<LockitLockingState?>, byte[]> GetStateValue = (tcs, value) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (value?.Length <= 0)
|
||||
{
|
||||
tcs.TrySetResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
tcs.TrySetResult((LockitLockingState?)value[0]);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Error on sinking lock state characteristic on opening/closing .{Exception}", ex);
|
||||
tcs.TrySetResult(null);
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary> Gets the battery state from lock.</summary>
|
||||
Func<byte[], double> GetChargeValue = (value) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (value.Length <= 0)
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
return value[0];
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Log.ForContext<LockItEventBased>().Error("Error on sinking battery state characteristic on opening.{Exception}", ex);
|
||||
return double.NaN;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
215
LockItBLE/Services/BluetoothLock/BLE/LockItPolling.cs
Normal file
215
LockItBLE/Services/BluetoothLock/BLE/LockItPolling.cs
Normal file
|
@ -0,0 +1,215 @@
|
|||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using TINK.Model.Bike.BluetoothLock;
|
||||
using TINK.Model.Device;
|
||||
using TINK.Services.BluetoothLock.Exception;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
using Xamarin.Essentials;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class LockItPolling : LockItBase
|
||||
{
|
||||
public LockItPolling(IDevice device, IAdapter adapter, ICipher cipher) : base(device, adapter, cipher)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary> Reconnects to device. </summary>
|
||||
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
|
||||
/// <param name="authInfo">Info required to connect.</param>
|
||||
/// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param>
|
||||
/// <returns>True if connecting succeeded, false if not.</returns>
|
||||
public override async Task ReconnectAsync(
|
||||
LockInfoAuthTdo authInfo,
|
||||
TimeSpan connectTimeout)
|
||||
=> await ReconnectAsync(
|
||||
authInfo,
|
||||
connectTimeout,
|
||||
() => new LockItPolling(Device, Adapter, Cipher));
|
||||
|
||||
/// <summary> Connects to device. </summary>
|
||||
/// <param name="authInfo">Info required to connect.</param>
|
||||
/// <param name="device">Device with must be connected.</param>
|
||||
/// <returns>True if connecting succeeded, false if not.</returns>
|
||||
public static async Task<LockItBase> Authenticate(
|
||||
IDevice device,
|
||||
LockInfoAuthTdo authInfo,
|
||||
IAdapter adapter,
|
||||
ICipher cipher)
|
||||
=> await Authenticate(
|
||||
device,
|
||||
authInfo,
|
||||
adapter,
|
||||
cipher,
|
||||
() => new LockItPolling(device, adapter, cipher));
|
||||
|
||||
/// <summary> Opens lock. </summary>
|
||||
/// <returns> Locking state.</returns>
|
||||
public async override Task<LockitLockingState?> OpenAsync()
|
||||
{
|
||||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||||
{
|
||||
throw new System.Exception("Can not open lock. Bluetooth code must be run on main thread");
|
||||
}
|
||||
|
||||
var info = await GetLockStateAsync();
|
||||
if (info?.State == null)
|
||||
{
|
||||
// Device not reachable.
|
||||
Log.ForContext<LockItPolling>().Information("Can not open lock. Device is not reachable (get state).");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
if (info.State.Value.GetLockingState() == LockingState.Open)
|
||||
{
|
||||
// Lock is already open.
|
||||
Log.ForContext<LockItPolling>().Information("No need to open lock. Lock is already open.");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
Log.ForContext<LockItPolling>().Debug($"Request to closed lock. Current lockikng state is {info}, counter value {ActivateLockWriteCounter}.");
|
||||
|
||||
var result = await OpenCloseLock(
|
||||
true); // Close lock);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
// State did not change. Return previous state.
|
||||
Log.ForContext<LockItPolling>().Information($"Opening lock failed.");
|
||||
return await Task.FromResult(info.State.Value);
|
||||
}
|
||||
|
||||
info = await GetLockStateAsync();
|
||||
if (info?.State == null)
|
||||
{
|
||||
// Device not reachable.
|
||||
Log.ForContext<LockItPolling>().Information($"State after open command unknown. Device is not reachable (get state).");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
|
||||
while (info?.State != null
|
||||
&& info.State.Value != LockitLockingState.CouldntOpenBoldBlocked
|
||||
&& info.State.Value != LockitLockingState.Open
|
||||
&& watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS))
|
||||
{
|
||||
info = await GetLockStateAsync(true); // While opening lock seems not always respond to reading operations.
|
||||
Log.ForContext<LockItPolling>().Information($"Current lock state is {info?.State.Value}.");
|
||||
}
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (info.State.Value)
|
||||
{
|
||||
case LockitLockingState.CouldntOpenBoldBlocked:
|
||||
// Expected error. ILockIt count not be opened (Spoke blocks lock, ....)
|
||||
throw new CouldntOpenBoldBlockedException();
|
||||
|
||||
case LockitLockingState.Open:
|
||||
return info.State.Value;
|
||||
|
||||
default:
|
||||
// Comprises values
|
||||
// - LockitLockingState.Closed
|
||||
// - LockitLockingState.Unknown
|
||||
// - LockitLockingState.CouldntOpenBoldBlocked
|
||||
// Internal error which sould never occure. Lock refuses to open but connection is ok.
|
||||
throw new CouldntOpenInconsistentStateExecption(info.State.Value.GetLockingState());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Close the lock.</summary>
|
||||
/// <returns>Locking state.</returns>
|
||||
public async override Task<LockitLockingState?> CloseAsync()
|
||||
{
|
||||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||||
{
|
||||
throw new System.Exception("Can not close lock. Bluetooth code must be run on main thread");
|
||||
}
|
||||
|
||||
// Get current state
|
||||
var info = await GetLockStateAsync();
|
||||
if (info?.State == null)
|
||||
{
|
||||
// Device not reachable.
|
||||
Log.ForContext<LockItPolling>().Error("Can not close lock. Device is not reachable (get state).");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
if (info.State.Value.GetLockingState() == LockingState.Closed)
|
||||
{
|
||||
// Lock is already closed.
|
||||
Log.ForContext<LockItPolling>().Error("No need to close lock. Lock is already closed.");
|
||||
return await Task.FromResult(info.State.Value);
|
||||
}
|
||||
|
||||
Log.ForContext<LockItPolling>().Debug($"Request to closed lock. Current state is {info}, counter value {ActivateLockWriteCounter}.");
|
||||
|
||||
var result = await OpenCloseLock(false); // Close lock
|
||||
if (!result)
|
||||
{
|
||||
// State did not change. Return previous state.
|
||||
Log.ForContext<LockItPolling>().Information($"Closing lock failed.");
|
||||
return await Task.FromResult(info.State.Value);
|
||||
}
|
||||
|
||||
// Get lock state until either lock state chaneges or until log gets unreachable.
|
||||
info = await GetLockStateAsync();
|
||||
if (info?.State == null)
|
||||
{
|
||||
// Device not reachable.
|
||||
Log.ForContext<LockItPolling>().Information($"Lock state after close command unknown.");
|
||||
return await Task.FromResult((LockitLockingState?)null);
|
||||
}
|
||||
|
||||
var watch = new Stopwatch();
|
||||
watch.Start();
|
||||
|
||||
while (info.State != null
|
||||
&& info.State.Value != LockitLockingState.CouldntCloseBoldBlocked
|
||||
&& info.State.Value != LockitLockingState.CouldntCloseMoving
|
||||
&& info.State.Value != LockitLockingState.Closed
|
||||
&& watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS))
|
||||
{
|
||||
info = await GetLockStateAsync(true); ; // While closing lock seems not always respond to reading operations.
|
||||
Log.ForContext<LockItPolling>().Information($"Current lock state is {info?.State.Value}.");
|
||||
}
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (info.State.Value)
|
||||
{
|
||||
case LockitLockingState.CouldntCloseBoldBlocked:
|
||||
// Expected error. ILockIt could not be closed (Spoke blocks lock, ....)
|
||||
throw new CouldntCloseBoldBlockedException();
|
||||
|
||||
case LockitLockingState.CouldntCloseMoving:
|
||||
// Expected error. ILockIt could not be closed (bike is moving)
|
||||
throw new CounldntCloseMovingException();
|
||||
|
||||
case LockitLockingState.Closed:
|
||||
// Everything is ok.
|
||||
return info.State.Value;
|
||||
|
||||
default:
|
||||
// Comprises values
|
||||
// - LockitLockingState.Open
|
||||
// - LockitLockingState.Unknown
|
||||
// - LockitLockingState.CouldntOpenBoldBlocked
|
||||
// Internal error which sould never occurre. Lock refuses to close but connection is ok.
|
||||
throw new CouldntCloseInconsistentStateExecption(info.State.Value.GetLockingState());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
182
LockItBLE/Services/BluetoothLock/BLE/LockItServiceBase.cs
Normal file
182
LockItBLE/Services/BluetoothLock/BLE/LockItServiceBase.cs
Normal file
|
@ -0,0 +1,182 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
using TINK.Model.Connector;
|
||||
using System;
|
||||
using Serilog;
|
||||
using TINK.Model.Device;
|
||||
using Xamarin.Essentials;
|
||||
using TINK.Model.Bike.BluetoothLock;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
public abstract class LockItServiceBase
|
||||
{
|
||||
/// <summary>Constructs base object.</summary>
|
||||
/// <param name="cipher">Encrpyting/ decrypting object.</param> /// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param>
|
||||
public LockItServiceBase(ICipher cipher)
|
||||
{
|
||||
Cipher = cipher;
|
||||
}
|
||||
|
||||
/// <summary> Holds timeout values for series of connecting attemps to a lock or multiple locks. </summary>
|
||||
public ITimeOutProvider TimeOut { get; set; }
|
||||
|
||||
protected ICipher Cipher { get; }
|
||||
|
||||
/// <summary> List of available or connected bluetooth devices. </summary>
|
||||
protected List<ILockService> DeviceList = new List<ILockService>();
|
||||
|
||||
|
||||
/// <summary> Reconnect locks of interest if required. </summary>
|
||||
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be reconnected. </remarks>
|
||||
/// <param name="locksInfo">Locks to reconnect.</param>
|
||||
/// <param name="connectTimeout">Timeout for connect operation.</param>
|
||||
public static async Task CheckReconnect(
|
||||
IEnumerable<ILockService> deviceList,
|
||||
IEnumerable<LockInfoAuthTdo> locksInfo,
|
||||
TimeSpan connectTimeout)
|
||||
{
|
||||
// Get list of target locks without invalid entries.
|
||||
var validLocksInfo = locksInfo
|
||||
.Where(x => x.IsIdValid || x.IsGuidValid)
|
||||
.Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0)
|
||||
.ToList();
|
||||
|
||||
foreach (var device in deviceList)
|
||||
{
|
||||
if (device.GetDeviceState() == DeviceState.Connected)
|
||||
{
|
||||
// No need to reconnect device because device is already connected.
|
||||
continue;
|
||||
}
|
||||
|
||||
var lockInfo = validLocksInfo.FirstOrDefault(x => x.Id == device. Name.GetBluetoothLockId() || x.Guid == device.Guid);
|
||||
|
||||
if (lockInfo == null)
|
||||
{
|
||||
// Current lock from deviceList is not of interest detected, no need to reconnect.
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await device.ReconnectAsync(lockInfo, connectTimeout);
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
// Member is called for background update of missing devices.
|
||||
// Do not display any error messages.
|
||||
Log.ForContext<LockItServiceBase>().Error($"Reconnect failed. {exception.Message}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Gets the state for locks of interest.</summary>
|
||||
/// <remarks>
|
||||
/// Might require a
|
||||
/// - connect to lock
|
||||
/// - reconnect to lock
|
||||
/// </remarks>
|
||||
/// <param name="locksInfo">Locks toget info for.</param>
|
||||
/// <param name="connectTimeout">Timeout for connect operation of a single lock.</param>
|
||||
public async Task<IEnumerable<LockInfoTdo>> GetLocksStateAsync(
|
||||
IEnumerable<LockInfoAuthTdo> locksInfo,
|
||||
TimeSpan connectTimeout)
|
||||
{
|
||||
if (locksInfo.Count() == 0)
|
||||
{
|
||||
// Nothing to do.
|
||||
return new List<LockInfoTdo>();
|
||||
}
|
||||
|
||||
// Reconnect locks.
|
||||
await CheckReconnect(DeviceList, locksInfo, connectTimeout);
|
||||
|
||||
// Connect to locks which were not yet discovered.
|
||||
DeviceList.AddRange(await CheckConnectMissing(locksInfo, connectTimeout));
|
||||
|
||||
// Get devices for which to update state.
|
||||
var locksInfoState = new List<LockInfoTdo>();
|
||||
|
||||
foreach (var device in DeviceList)
|
||||
{
|
||||
var lockInfoAuth = locksInfo.FirstOrDefault(x => x.Guid == device.Guid || x.Id == device.Id);
|
||||
if (lockInfoAuth == null)
|
||||
{
|
||||
// Device is not of interest.
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = await device.GetLockStateAsync();
|
||||
locksInfoState.Add(new LockInfoTdo.Builder
|
||||
{
|
||||
Id = lockInfoAuth.Id,
|
||||
Guid = lockInfoAuth.IsGuidValid ? lockInfoAuth.Guid : device.Guid,
|
||||
State = state?.State
|
||||
}.Build());
|
||||
}
|
||||
|
||||
return locksInfoState;
|
||||
}
|
||||
|
||||
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
|
||||
/// <param name="locksInfo">Locks to reconnect.</param>
|
||||
/// <param name="connectTimeout">Timeout for connect operation of a single lock.</param>
|
||||
protected abstract Task<IEnumerable<ILockService>> CheckConnectMissing(
|
||||
IEnumerable<LockInfoAuthTdo> locksInfo,
|
||||
TimeSpan connectTimeout);
|
||||
|
||||
/// <summary>Gets a lock by bike Id.</summary>
|
||||
/// <param name="bikeId"></param>
|
||||
/// <returns>Lock object</returns>
|
||||
public ILockService this[int bikeId]
|
||||
{
|
||||
get
|
||||
{
|
||||
var device = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == bikeId);
|
||||
if (device == null)
|
||||
{
|
||||
return new NullLock(bikeId);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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<LockingState> DisconnectAsync(int bikeId, Guid bikeGuid)
|
||||
{
|
||||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||||
{
|
||||
throw new System.Exception("Can not disconnect from lock. Bluetooth code must be run on main thread");
|
||||
}
|
||||
|
||||
Log.ForContext<LockItByScanServiceBase>().Debug($"Request to disconnect from device {bikeId}/ {bikeGuid}.");
|
||||
|
||||
var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == bikeId || x.Guid == bikeGuid);
|
||||
|
||||
if (lockIt == null)
|
||||
{
|
||||
// Nothing to do
|
||||
return LockingState.Disconnected;
|
||||
}
|
||||
|
||||
DeviceList.Remove(lockIt);
|
||||
|
||||
if (lockIt.GetDeviceState() == DeviceState.Disconnected)
|
||||
{
|
||||
// Nothing to do
|
||||
return LockingState.Disconnected;
|
||||
}
|
||||
|
||||
await lockIt.Disconnect();
|
||||
return LockingState.Disconnected;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using TINK.Model.Connector;
|
||||
|
||||
namespace TINK.Services.BluetoothLock.BLE
|
||||
{
|
||||
public static class LockItServiceHelper
|
||||
{
|
||||
public static string Name { get => TextToLockItTypeHelper.ISHAREITADVERTISMENTTITLE; }
|
||||
}
|
||||
}
|
188
TestLockItBLE/Services/BluetoothLock/BLE/TestLockItBase.cs
Normal file
188
TestLockItBLE/Services/BluetoothLock/BLE/TestLockItBase.cs
Normal file
|
@ -0,0 +1,188 @@
|
|||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Plugin.BLE.Abstractions;
|
||||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TINK.Model.Device;
|
||||
using TINK.Services.BluetoothLock.BLE;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
|
||||
namespace TestLockItBLE.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class TestLockItBase
|
||||
{
|
||||
|
||||
[Test]
|
||||
public void TestName_Null()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'X', (byte)'D', (byte)'G', (byte)'x', (byte)'q', (byte)'M', (byte)'f', (byte)'A', (byte)'F', (byte)'q', (byte)'g', (byte)'N', (byte)'V', (byte)'r', (byte)'N', (byte)'Y' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(Arg.Any<Guid>()).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns((string)null);
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItBaseTest.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.AreEqual(string.Empty, lockIt.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestName()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'m', (byte)'x', (byte)'p', (byte)'J', (byte)'g', (byte)'D', (byte)'D', (byte)'i', (byte)'o', (byte)'T', (byte)'F', (byte)'M', (byte)'S', (byte)'E', (byte)'m', (byte)'E' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(Arg.Any<Guid>()).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItBaseTest.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.AreEqual("ISHAREIT+123", lockIt.Name);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestId()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'l', (byte)'x', (byte)'p', (byte)'J', (byte)'g', (byte)'D', (byte)'D', (byte)'i', (byte)'o', (byte)'T', (byte)'F', (byte)'M', (byte)'S', (byte)'E', (byte)'m', (byte)'E' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(Arg.Any<Guid>()).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItBaseTest.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.AreEqual(123, lockIt.Id);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGuid()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'l', (byte)'x', (byte)'p', (byte)'J', (byte)'g', (byte)'D', (byte)'D', (byte)'i', (byte)'o', (byte)'T', (byte)'F', (byte)'M', (byte)'S', (byte)'E', (byte)'m', (byte)'E' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("72bdc44f-c588-44f3-b6df-9aace7daafdd"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(Arg.Any<Guid>()).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItBaseTest.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.AreEqual(new Guid("72bdc44f-c588-44f3-b6df-9aace7daafdd"), lockIt.Guid);
|
||||
}
|
||||
|
||||
/// <summary> Derives from LockItBase class for testing purposes. </summary>
|
||||
|
||||
private class LockItBaseTest : LockItBase
|
||||
{
|
||||
private LockItBaseTest(IDevice device, IAdapter adapter, ICipher cipher) : base(device, adapter, cipher)
|
||||
{
|
||||
}
|
||||
/// <summary> Connects to device. </summary>
|
||||
/// <param name="authInfo">Info required to connect.</param>
|
||||
/// <param name="device">Device with must be connected.</param>
|
||||
/// <returns>True if connecting succeeded, false if not.</returns>
|
||||
public static async Task<LockItBase> Authenticate(
|
||||
IDevice device,
|
||||
LockInfoAuthTdo authInfo,
|
||||
IAdapter adapter,
|
||||
ICipher cipher)
|
||||
=> await Authenticate(
|
||||
device,
|
||||
authInfo,
|
||||
adapter,
|
||||
cipher,
|
||||
() => new LockItBaseTest(device, adapter, cipher));
|
||||
|
||||
public override Task<LockitLockingState?> CloseAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<LockitLockingState?> OpenAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task ReconnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
315
TestLockItBLE/Services/BluetoothLock/BLE/TestLockItEventBased.cs
Normal file
315
TestLockItBLE/Services/BluetoothLock/BLE/TestLockItEventBased.cs
Normal file
|
@ -0,0 +1,315 @@
|
|||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
using TINK.Services.BluetoothLock.Exception;
|
||||
using TINK.Services.BluetoothLock.BLE;
|
||||
using DeviceState = Plugin.BLE.Abstractions.DeviceState;
|
||||
using Plugin.BLE.Abstractions.EventArgs;
|
||||
using System.Threading;
|
||||
|
||||
namespace TestLockItBLE
|
||||
{
|
||||
public class Tests
|
||||
{
|
||||
[Test]
|
||||
public void TestOpen()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var stateCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateCharacteristic = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'p', (byte)'a', (byte)'w', (byte)'m', (byte)'X', (byte)'8', (byte)'T', (byte)'X', (byte)'Q', (byte)'Z', (byte)'d', (byte)'l', (byte)'k', (byte)'3', (byte)'e', (byte)'V', },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to open functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* State read before open action: closed */ }));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateCharacteristic));
|
||||
activateCharacteristic.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Open /* State passed as event argument after opening. */});
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
var lockState = lockIt.OpenAsync();
|
||||
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
|
||||
Assert.That(lockState.Result, Is.EqualTo(LockitLockingState.Open));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpen_ThrowsCouldntOpenInconsistentStateExecption()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var stateCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'m', (byte)'l', (byte)'Q', (byte)'I', (byte)'S', (byte)'z', (byte)'p', (byte)'H', (byte)'m', (byte)'n', (byte)'V', (byte)'n', (byte)'7', (byte)'f', (byte)'i', (byte)'3' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to open functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* State read before open action: closed */ }));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Closed /* State passed as event argument after opening. */});
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.That(async () =>
|
||||
{
|
||||
var lockState = lockIt.OpenAsync();
|
||||
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
|
||||
await lockState;
|
||||
},
|
||||
Throws.InstanceOf<CouldntOpenInconsistentStateExecption>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpen_ThrowsCouldntOpenBoldBlockedException()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
var stateCharacteristic = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'i', (byte)'r', (byte)'F', (byte)'h', (byte)'G', (byte)'T', (byte)'z', (byte)'P', (byte)'F', (byte)'Z', (byte)'n', (byte)'z', (byte)'Y', (byte)'B', (byte)'s', (byte)'9' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to open functionality.
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* closed */}), Task.FromResult(new byte[] { 4 /* bold blocked */ }));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.CouldntOpenBoldBlocked /* State passed as event argument after opening. */});
|
||||
|
||||
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
Assert.That(async () =>
|
||||
{
|
||||
var lockState = lockIt.OpenAsync();
|
||||
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
|
||||
await lockState;
|
||||
},
|
||||
Throws.InstanceOf<CouldntOpenBoldBlockedException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClose()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
var stateCharacteristic = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'I', (byte)'7', (byte)'y', (byte)'B', (byte)'f', (byte)'s', (byte)'v', (byte)'L', (byte)'G', (byte)'L', (byte)'7', (byte)'b', (byte)'7', (byte)'X', (byte)'z', (byte)'t' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to close functionality.
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* open */ }), Task.FromResult(new byte[] { 1 /* closed */}));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Closed /* State passed as event argument after closing. */});
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
|
||||
var lockState = lockIt.CloseAsync();
|
||||
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
|
||||
Assert.That(lockState.Result, Is.EqualTo(LockitLockingState.Closed));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClose_ThrowsCouldntCloseInconsistentStateExecption()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
var stateCharacteristic = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'8', (byte)'q', (byte)'3', (byte)'9', (byte)'i', (byte)'6', (byte)'c', (byte)'g', (byte)'9', (byte)'L', (byte)'V', (byte)'7', (byte)'T', (byte)'G', (byte)'l', (byte)'f' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to close functionality.
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* opened */}));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Open /* State passed as event argument after closing. */});
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
Assert.That(async () =>
|
||||
{
|
||||
var lockState = lockIt.CloseAsync();
|
||||
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
|
||||
await lockState;
|
||||
},
|
||||
Throws.InstanceOf<CouldntCloseInconsistentStateExecption>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClose_ThrowsCouldntCloseBoldBlockedException()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var lockControl = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
var stateCharacteristic = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'v', (byte)'f', (byte)'u', (byte)'v', (byte)'j', (byte)'E', (byte)'b', (byte)'x', (byte)'p', (byte)'z', (byte)'a', (byte)'h', (byte)'V', (byte)'5', (byte)'9', (byte)'i' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to Close functionality.
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* open */}));
|
||||
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.CouldntCloseBoldBlocked /* State passed as event argument after opening. */});
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
Assert.That(async () =>
|
||||
{
|
||||
var lockState = lockIt.CloseAsync();
|
||||
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
|
||||
await lockState;
|
||||
},
|
||||
Throws.InstanceOf<CouldntCloseBoldBlockedException>());
|
||||
}
|
||||
}
|
||||
}
|
270
TestLockItBLE/Services/BluetoothLock/BLE/TestLockItPolling.cs
Normal file
270
TestLockItBLE/Services/BluetoothLock/BLE/TestLockItPolling.cs
Normal file
|
@ -0,0 +1,270 @@
|
|||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
using TINK.Services.BluetoothLock.Exception;
|
||||
using TINK.Services.BluetoothLock.BLE;
|
||||
using DeviceState = Plugin.BLE.Abstractions.DeviceState;
|
||||
using System.Threading;
|
||||
|
||||
namespace TestLockItBLE.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class TestLockItPolling
|
||||
{
|
||||
[Test]
|
||||
public void TestOpen()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'o', (byte)'a', (byte)'w', (byte)'m', (byte)'X', (byte)'8', (byte)'T', (byte)'X', (byte)'Q', (byte)'Z', (byte)'d', (byte)'l', (byte)'k', (byte)'3', (byte)'e', (byte)'V', },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to open functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* state read after sending open commant: closed */ }), Task.FromResult(new byte[] { 0 /* state read after sending open commant: open */}));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItPolling.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.That(lockIt.OpenAsync().Result, Is.EqualTo(LockitLockingState.Open));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpen_ThrowsCouldntOpenInconsistentStateExecption()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'n', (byte)'l', (byte)'Q', (byte)'I', (byte)'S', (byte)'z', (byte)'p', (byte)'H', (byte)'m', (byte)'n', (byte)'V', (byte)'n', (byte)'7', (byte)'f', (byte)'i', (byte)'3' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to open functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* closed */})); // Closed is returned twice => Inconsistent state ....
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItPolling.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.That(async () => { var result = await lockIt.OpenAsync(); }, Throws.InstanceOf<CouldntOpenInconsistentStateExecption>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpen_ThrowsCouldntOpenBoldBlockedException()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'h', (byte)'r', (byte)'F', (byte)'h', (byte)'G', (byte)'T', (byte)'z', (byte)'P', (byte)'F', (byte)'Z', (byte)'n', (byte)'z', (byte)'Y', (byte)'B', (byte)'s', (byte)'9' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to open functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 1 /* closed */}), Task.FromResult(new byte[] { 4 /* bold blocked */ }));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItPolling.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.That(async () => { var result = await lockIt.OpenAsync(); }, Throws.InstanceOf<CouldntOpenBoldBlockedException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClose()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'H', (byte)'7', (byte)'y', (byte)'B', (byte)'f', (byte)'s', (byte)'v', (byte)'L', (byte)'G', (byte)'L', (byte)'7', (byte)'b', (byte)'7', (byte)'X', (byte)'z', (byte)'t' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to close functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* open */ }), Task.FromResult(new byte[] { 1 /* closed */}));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItPolling.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.That(lockIt.CloseAsync().Result, Is.EqualTo(LockitLockingState.Closed));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClose_ThrowsCouldntOpenInconsistentStateExecption()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'7', (byte)'q', (byte)'3', (byte)'9', (byte)'i', (byte)'6', (byte)'c', (byte)'g', (byte)'9', (byte)'L', (byte)'V', (byte)'7', (byte)'T', (byte)'G', (byte)'l', (byte)'f' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to close functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* opened */}));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItPolling.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.That(async () => { var result = await lockIt.CloseAsync(); }, Throws.InstanceOf<CouldntCloseInconsistentStateExecption>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClose_ThrowsCouldntOpenBoldBlockedException()
|
||||
{
|
||||
var device = Substitute.For<IDevice>();
|
||||
var adapter = Substitute.For<IAdapter>();
|
||||
var cipher = Substitute.For<TINK.Model.Device.ICipher>();
|
||||
var auth = Substitute.For<ICharacteristic>();
|
||||
var controlService = Substitute.For<IService>();
|
||||
var controlCharacteristic = Substitute.For<ICharacteristic>();
|
||||
var activateLock = Substitute.For<ICharacteristic>();
|
||||
|
||||
var authTdo = new LockInfoAuthTdo.Builder
|
||||
{
|
||||
Id = 12,
|
||||
K_seed = new byte[] { (byte)'u', (byte)'f', (byte)'u', (byte)'v', (byte)'j', (byte)'E', (byte)'b', (byte)'x', (byte)'p', (byte)'z', (byte)'a', (byte)'h', (byte)'V', (byte)'5', (byte)'9', (byte)'i' },
|
||||
K_u = new byte[16]
|
||||
}.Build();
|
||||
|
||||
// Calls related to Authenticate functionality.
|
||||
device.State.Returns(DeviceState.Connected);
|
||||
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
|
||||
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[8]));
|
||||
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
|
||||
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
|
||||
auth.WriteAsync(Arg.Any<byte[]>()).Returns(true);
|
||||
|
||||
device.Name.Returns("ISHAREIT+123");
|
||||
|
||||
// Calls related to Close functionality.
|
||||
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
|
||||
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult(new byte[] { 0 /* open */}), Task.FromResult(new byte[] { 5 /* bold blocked */ }));
|
||||
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
|
||||
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(true));
|
||||
|
||||
|
||||
// Use factory to create LockIt-object.
|
||||
var lockIt = LockItPolling.Authenticate(device, authTdo, adapter, cipher).Result;
|
||||
Assert.That(async () => { var result = await lockIt.CloseAsync(); }, Throws.InstanceOf<CouldntCloseBoldBlockedException>());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using TINK.Services.BluetoothLock;
|
||||
using TINK.Services.BluetoothLock.BLE;
|
||||
using TINK.Services.BluetoothLock.Tdo;
|
||||
|
||||
namespace TestLockItBLE.Services.BluetoothLock.BLE
|
||||
{
|
||||
public class TestLockItServiceBase
|
||||
{
|
||||
[Test]
|
||||
public async Task TestCheckReconnect_EmptyList()
|
||||
{
|
||||
var disconnectedDevice = Substitute.For<ILockService>();
|
||||
disconnectedDevice.GetDeviceState().Returns(DeviceState.Disconnected);
|
||||
var devices = new List<ILockService> { disconnectedDevice };
|
||||
var locksInfo = new List<LockInfoAuthTdo>();
|
||||
|
||||
await LockItServiceBase.CheckReconnect(
|
||||
devices,
|
||||
locksInfo,
|
||||
TimeSpan.FromSeconds(0));
|
||||
|
||||
await disconnectedDevice.DidNotReceive().ReconnectAsync(Arg.Any<LockInfoAuthTdo>(), Arg.Any<TimeSpan>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestCheckReconnect_NoMatchingIdEntry()
|
||||
{
|
||||
var disconnectedDevice = Substitute.For<ILockService>();
|
||||
disconnectedDevice.GetDeviceState().Returns(DeviceState.Disconnected);
|
||||
disconnectedDevice.Name.Returns("ISHAREIT+334");
|
||||
disconnectedDevice.Guid.Returns(new Guid("00000000-0000-0000-0000-000000000001"));
|
||||
|
||||
var devices = new List<ILockService> { disconnectedDevice };
|
||||
var locksInfo = new List<LockInfoAuthTdo> { new LockInfoAuthTdo.Builder { Id = 992, Guid = new Guid("00000000-0000-0000-0000-000000000002"), K_seed = new byte[] { 2 }, K_u = new byte[] { 3 } }.Build() };
|
||||
|
||||
await LockItServiceBase.CheckReconnect(
|
||||
devices,
|
||||
locksInfo,
|
||||
TimeSpan.FromSeconds(0));
|
||||
|
||||
await disconnectedDevice.DidNotReceive().ReconnectAsync(Arg.Any<LockInfoAuthTdo>(), Arg.Any<TimeSpan>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestCheckReconnect()
|
||||
{
|
||||
var disconnectedDevice = Substitute.For<ILockService>();
|
||||
disconnectedDevice.GetDeviceState().Returns(DeviceState.Disconnected);
|
||||
disconnectedDevice.Name.Returns("ISHAREIT+992");
|
||||
disconnectedDevice.Guid.Returns(new Guid("00000000-0000-0000-0000-000000000001"));
|
||||
|
||||
var devices = new List<ILockService> { disconnectedDevice };
|
||||
var locksInfo = new List<LockInfoAuthTdo> { new LockInfoAuthTdo.Builder { Id = 992, Guid = new Guid("00000000-0000-0000-0000-000000000002"), K_seed = new byte[] { 2 }, K_u = new byte[] { 3 }}.Build() };
|
||||
|
||||
await LockItServiceBase.CheckReconnect(
|
||||
devices,
|
||||
locksInfo,
|
||||
TimeSpan.FromSeconds(0));
|
||||
|
||||
await disconnectedDevice.Received().ReconnectAsync(Arg.Any<LockInfoAuthTdo>(), Arg.Any<TimeSpan>());
|
||||
}
|
||||
}
|
||||
}
|
20
TestLockItBLE/TestLockItBLE.csproj
Normal file
20
TestLockItBLE/TestLockItBLE.csproj
Normal file
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LockItBLE\LockItBLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
31
TestLockItBLE/TestLockItBLE.sln
Normal file
31
TestLockItBLE/TestLockItBLE.sln
Normal file
|
@ -0,0 +1,31 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.31229.75
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestLockItBLE", "TestLockItBLE.csproj", "{B8B4495E-AFE4-4072-B13C-954955CFB45E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LockItBLE", "..\LockItBLE\LockItBLE.csproj", "{828D1E68-1DCC-45B1-B212-40AFDAE7A1D7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{B8B4495E-AFE4-4072-B13C-954955CFB45E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B8B4495E-AFE4-4072-B13C-954955CFB45E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B8B4495E-AFE4-4072-B13C-954955CFB45E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B8B4495E-AFE4-4072-B13C-954955CFB45E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{828D1E68-1DCC-45B1-B212-40AFDAE7A1D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{828D1E68-1DCC-45B1-B212-40AFDAE7A1D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{828D1E68-1DCC-45B1-B212-40AFDAE7A1D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{828D1E68-1DCC-45B1-B212-40AFDAE7A1D7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {3434CB05-4558-4AE4-A733-D5BE55DA0F12}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
Loading…
Reference in a new issue