diff --git a/LockItBLE/LockItBLE.csproj b/LockItBLE/LockItBLE.csproj new file mode 100644 index 0000000..6b5f44e --- /dev/null +++ b/LockItBLE/LockItBLE.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + TINK + + + + + + + + + + + + + + diff --git a/LockItBLE/LockItBLE.sln b/LockItBLE/LockItBLE.sln new file mode 100644 index 0000000..f41c647 --- /dev/null +++ b/LockItBLE/LockItBLE.sln @@ -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 diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItBase.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItBase.cs new file mode 100644 index 0000000..e2c65f0 --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItBase.cs @@ -0,0 +1,1045 @@ +using Plugin.BLE.Abstractions; +using Plugin.BLE.Abstractions.Contracts; +using Plugin.BLE.Abstractions.Exceptions; +using Serilog; +using System; +using System.Threading.Tasks; +using TINK.Services.BluetoothLock.Crypto; +using TINK.Services.BluetoothLock.Tdo; +using System.Threading; +using System.Collections.Generic; +using TINK.Services.BluetoothLock.Exception; +using Xamarin.Essentials; +using TINK.Model.Connector; +using TINK.Model.Device; +using Polly.Retry; +using Polly; + +namespace TINK.Services.BluetoothLock.BLE +{ + /// Manages a single lock. + public abstract class LockItBase : ILockService + { + /// Lenght of seed in bytes. + private const int SEEDLENGTH = 16; + + + /// Timeout for open/ close operations. + protected const int OPEN_CLOSE_TIMEOUT_MS = 30000; + + /// Timeout for get service operations. + private const int GETSERVICE_TIMEOUT_MS = 3000; + + /// Timeout for read operations. + private const int READ_TIMEOUT_MS = 3000; + + protected LockItBase(IDevice device, IAdapter adapter, ICipher cipher) + { + Device = device ?? throw new ArgumentException(nameof(device)); + Cipher = cipher ?? throw new ArgumentException(nameof(cipher)); + Adapter = adapter ?? throw new ArgumentException(nameof(adapter)); + + _retryPollicy = Policy + .Handle() + .WaitAndRetryAsync(2, index => TimeSpan.FromMilliseconds(100)); + + GetGuid(); + GetName(); + + if (InvalidatedSeed.ContainsKey(Guid)) + { + // Lock was already connected. No need to add entry. + return; + } + + InvalidatedSeed.Add(Guid, new List()); + } + + private static readonly Dictionary> InvalidatedSeed = new Dictionary>(); + + /// Count of write- actions to activate lock characteristic.. + protected int ActivateLockWriteCounter { get; private set; } + + protected ICipher Cipher { get; } + + protected IAdapter Adapter { get; } + + protected IDevice Device { get; } + + private IService LockControl { get; set; } + + private IService BatteryControl { get; set; } + + private ICharacteristic ActivateLockCharacteristic { get; set; } + + private ICharacteristic AlarmCharacteristic { get; set; } + + private ICharacteristic AuthCharacteristic { get; set; } + + private ICharacteristic StateCharacteristic { get; set; } + + private ICharacteristic SoundCharacteristic { get; set; } + + private ICharacteristic BatteryCharacteristic { get; set; } + + private readonly AsyncRetryPolicy _retryPollicy; + + /// Gets the lock control service. + private async Task GetLockControlService() + { + if (LockControl != null) return LockControl; + + LockControl = null; + + Log.ForContext().Debug("Request to get lock control service."); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(GETSERVICE_TIMEOUT_MS); + try + { + LockControl = await Device.GetServiceAsync(new Guid("0000f00d-1212-efde-1523-785fef13d123"), cts.Token); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting lock control service failed. {Exception}", exception); + throw new System.Exception($"Can not get lock control service. {exception.Message}", exception); + } + finally + { + cts.Dispose(); + } + + Log.ForContext().Debug("Getting lock control service succeeded."); + return LockControl; + } + + /// Gets battery service. + private async Task GetBatteryService() + { + if (BatteryControl != null) return BatteryControl; + + BatteryControl = null; + + Log.ForContext().Debug("Request to get battery control service."); + + var cts = new CancellationTokenSource(); + cts.CancelAfter(GETSERVICE_TIMEOUT_MS); + try + { + BatteryControl = await Device.GetServiceAsync(new Guid("0000180F-0000-1000-8000-00805f9b34fb"), cts.Token); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting battery service failed. {Exception}", exception); + throw new System.Exception($"Can not get battery service. {exception.Message}", exception); + } + finally + { + cts.Dispose(); + } + + Log.ForContext().Debug("Getting battery service succeeded."); + return BatteryControl; + } + + /// Gets the state characteristic. + private async Task GetActivateLockCharacteristicAsync() + { + if (ActivateLockCharacteristic != null) return ActivateLockCharacteristic; + + ActivateLockCharacteristic = null; + + Log.ForContext().Debug("Request to get activate lock characteristic."); + + try + { + ActivateLockCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting activate lock charcteristic failed. {Exception}", exception); + throw new System.Exception($"Can not get activate characteristic. {exception.Message}", exception); + } + + Log.ForContext().Debug("Activate lock characteristic retrieved successfully."); + + return ActivateLockCharacteristic; + } + /// Gets the alarm characteristic. + private async Task GetAlarmCharacteristicAsync() + { + if (AlarmCharacteristic != null) return AlarmCharacteristic; + + AlarmCharacteristic = null; + + Log.ForContext().Debug("Request to get alarm characteristic."); + + try + { + AlarmCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BFFF-1212-efde-1523-785fef13d123")); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting alarm-charcteristic failed. {Exception}", exception); + throw new System.Exception($"Can not get alarm characteristic. {exception.Message}", exception); + } + + Log.ForContext().Debug("Get alarm characteristic retrieved successfully."); + return AlarmCharacteristic; + } + + /// Gets the auth characteristic. + private async Task GetAuthCharacteristicAsync() + { + if (AuthCharacteristic != null) return AuthCharacteristic; + + AuthCharacteristic = null; + + Log.ForContext().Debug("Request to get auth characteristic."); + try + { + AuthCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting auth-charcteristic failed. {Exception}", exception); + throw new System.Exception(string.Format("Can not get auth characteristic. {0}", exception.Message), exception); + } + + Log.ForContext().Debug("Get auth characteristic retrieved successfully."); + return AuthCharacteristic; + } + + /// Gets the state characteristic. + protected async Task GetStateCharacteristicAsync() + { + if (StateCharacteristic != null) return StateCharacteristic; + + StateCharacteristic = null; + + Log.ForContext().Debug("Request to get lock state characteristic."); + try + { + StateCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting state charcteristic failed. {Exception}", exception); + throw new System.Exception(string.Format("Can not get state characteristic. {0}", exception.Message), exception); + } + + Log.ForContext().Debug("Get state characteristic retrieved successfully."); + return StateCharacteristic; + } + + /// Gets the sound characteristic. + private async Task GetSoundCharacteristicAsync() + { + if (SoundCharacteristic != null) return SoundCharacteristic; + + SoundCharacteristic = null; + + Log.ForContext().Debug("Request to get sound characteristic."); + try + { + SoundCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BAAE-1212-efde-1523-785fef13d123")); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting sound charcteristic failed. {Exception}", exception); + throw new System.Exception($"Can not get sound characteristic. {exception.Message}", exception); + } + + Log.ForContext().Debug("Get sound characteristic retrieved successfully."); + return SoundCharacteristic; + } + + /// Gets the battery characteristic. + protected async Task GetBatteryCharacteristicAsync() + { + if (BatteryCharacteristic != null) return BatteryCharacteristic; + + BatteryCharacteristic = null; + + Log.ForContext().Debug("Request to get battery characteristic."); + try + { + BatteryCharacteristic = await (await GetBatteryService())?.GetCharacteristicAsync(new Guid("00002a19-0000-1000-8000-00805f9b34fb")); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Getting battery charcteristic failed. {Exception}", exception); + throw new System.Exception($"Can not get battery characteristic. {exception.Message}", exception); + } + + Log.ForContext().Debug("Get battery characteristic retrieved successfully."); + return BatteryCharacteristic; + } + + /// Query name of lock. + private void GetName() + { + if (!string.IsNullOrEmpty(Name)) + { + // Prevent valid name to be queried more than twice because Name does not change. + return; + } + + Log.ForContext().Debug("Query name of lock."); + try + { + Name = Device.Name ?? string.Empty; + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving bluetooth name failed . {Exception}", exception); + throw new System.Exception($"Can not get name of lock. {exception.Message}", exception); + } + + Log.ForContext().Debug($"Lock name is {Name}."); + Id = Name.GetBluetoothLockId(); + return; + } + + /// Full idvertisement name. + public string Name { get; private set; } = string.Empty; + + /// Id part of idvertisement name. + public int Id { get; private set; } + + /// Query GUID of lock. + private void GetGuid() + { + if (Guid != TextToLockItTypeHelper.INVALIDLOCKGUID) + { + // Prevent valid GUID to be queried more than twice because GUID does not change. + } + + Log.ForContext().Debug("Query name of lock."); + try + { + Guid = Device.Id; + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving bluetooth guid failed. {Exception}", exception); + throw new System.Exception($"Can not get guid of lock. {exception.Message}", exception); + } + + Log.ForContext().Debug($"Lock GUID is {Guid}."); + } + + /// GUID. + public Guid Guid { get; private set; } = TextToLockItTypeHelper.INVALIDLOCKGUID; + + private byte[] CopriKey { get; set; } = new byte[0]; + + /// Gets the device state. + public DeviceState? GetDeviceState() + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not get device state. Bluetooth code must be run on main thread"); + } + + DeviceState? state; + Log.ForContext().Debug("Request to get connection state."); + try + { + state = GetDeviceState(Device.State); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving bluetooth state failed. {Exception}", exception); + throw new System.Exception($"Can not get bluetooth state. {exception.Message}", exception); + } + + Log.ForContext().Debug($"Connection state is {state}."); + return state; + } + + /// Reconnects to device. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect. + /// Timeout to apply when connecting to bluetooth lock. + /// True if connecting succeeded, false if not. + public abstract Task ReconnectAsync( + LockInfoAuthTdo authInfo, + TimeSpan connectTimeout); + + /// Reconnects to device. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect. + /// Timeout to apply when connecting to bluetooth lock. + /// True if connecting succeeded, false if not. + protected async Task ReconnectAsync( + LockInfoAuthTdo authInfo, + TimeSpan connectTimeout, + Func factory) + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not reconnect to lock. Bluetooth code must be run on main thread"); + } + + // Check if key is available. + if (authInfo == null) + { + Log.ForContext().Error($"Can not authenticate. No auth info available."); + throw new AuthKeyException($"Can not authenticate. No auth info available."); + } + + if (authInfo.K_seed.Length != SEEDLENGTH + || authInfo.K_u.Length <= 0) + { + throw new AuthKeyException($"Can not authenticate. Invalid seed-/ k-user-lenght {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected."); + } + + if (Device.State == Plugin.BLE.Abstractions.DeviceState.Connected) + { + throw new AlreadyConnectedException(); + } + + // Reset all references to characteristics. + LockControl = null; + ActivateLockCharacteristic = null; + AlarmCharacteristic = null; + AuthCharacteristic = null; + StateCharacteristic = null; + SoundCharacteristic = null; + ActivateLockWriteCounter = 0; + + var cts = new CancellationTokenSource(connectTimeout); + + // Connect to device and authenticate. + Log.ForContext().Debug($"Request connect to device {Device.Name}. Connect state is {Device.State}."); + try + { + await Adapter.ConnectToDeviceAsync( + Device, + 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 reconnect.\r\nTimeout of {connectTimeout.TotalMilliseconds} [ms] elapsed.", exception); + } + + Log.ForContext().Error("Can not reconnect. {Exception}", exception); + throw new System.Exception($"Can not Reconnect. {exception.Message}", exception); + } + + Log.ForContext().Debug($"Connecting to device succeeded. Starting auth sequence..."); + + var lockIt = await Authenticate(Device, authInfo, Adapter, Cipher, factory); + CopriKey = lockIt.CopriKey; + } + + /// Connects to device. + /// Info required to connect. + /// Device with must be connected. + /// True if connecting succeeded, false if not. + protected static async Task Authenticate( + IDevice device, + LockInfoAuthTdo authInfo, + IAdapter adapter, + ICipher cipher, + Func factory) + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not authenticate. Bluetooth code must be run on main thread"); + } + + if (device == null) + throw new ArgumentException(nameof(device)); + + if (cipher == null) + throw new ArgumentException(nameof(cipher)); + + if (adapter == null) + throw new ArgumentException(nameof(adapter)); + + // Check if connect is required. + DeviceState deviceState; + Log.ForContext().Debug("Retrieving connection state is in context of auth."); + try + { + deviceState = GetDeviceState(device.State); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Can not authenticate. Retrieving bluetooth state failed. {Exception}", exception); + throw new System.Exception(string.Format("Can not authenticate. Getting bluetooth state failed. {0}", exception.Message), exception); + } + + switch (deviceState) + { + case DeviceState.Disconnected: + throw new BluetoothDisconnectedException(); + + case DeviceState.Connected: + break; + + default: + // Can not get state if device is not connected. + Log.ForContext().Error($"Can not authenticate. Unexpected lock state {deviceState} detected."); + throw new System.Exception(string.Format("Can not authenticate. Unexpected bluetooth state {0} detected.", deviceState)); + } + + // Check if key is available. + if (authInfo == null) + { + Log.ForContext().Error($"Can not authenticate. No auth info available."); + throw new AuthKeyException($"Can not authenticate. No auth info available."); + } + + if (authInfo.K_seed.Length != SEEDLENGTH + || authInfo.K_u.Length <= 0) + { + throw new AuthKeyException($"Can not authenticate. Invalid seed-/ k-user-lenght {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected."); + } + + Log.ForContext().Debug($"Connection state is {deviceState} in context of auth."); + + var lockIt = factory(); + + if (InvalidatedSeed[lockIt.Guid].Contains(string.Join(",", authInfo.K_seed))) + { + throw new AuthKeyException($"Can not authenticate. Seed {string.Join(",", authInfo.K_seed)} was already used."); + } + + InvalidatedSeed[lockIt.Guid].Add(string.Join(",", authInfo.K_seed)); + + // Connect to device and authenticate. + try + { + await AuthenticateAsync( + lockIt, + authInfo, + lockIt.Cipher); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Authentication failed. {Exception}", exception); + try + { + // Disconnect from device if auth did not succeed. + await lockIt.Adapter.DisconnectDeviceAsync(lockIt.Device); + } + catch (System.Exception exceptionInner) + { + Log.ForContext().Error("Authentication failed. Disconnect throw an exception. {Exception}", exceptionInner); + } + + Log.ForContext().Error($"Auth failed for device name={lockIt.Name}, GUID={lockIt.Guid}."); + + throw; + } + + lockIt.CopriKey = authInfo.K_u; + + Log.ForContext().Debug($"Auth succeeded for device name={lockIt.Name}, GUID={lockIt.Guid}, state={lockIt.GetDeviceState()}."); + return lockIt; + } + + /// Performs an authentication. + private static async Task AuthenticateAsync( + LockItBase lockIt, + LockInfoAuthTdo lockInfo, + ICipher cipher) + { + Log.ForContext().Debug($"Request to autenticate for {lockIt.Name}."); + + var authCharacteristic = await lockIt.GetAuthCharacteristicAsync(); + if (authCharacteristic == null) + { + Log.ForContext().Debug("Getting auth-charcteristic failed."); + throw new CoundntGetCharacteristicException("Authentication failed. Auth characteristic must not be null."); + } + + bool success; + + try + { + success = await authCharacteristic.WriteAsync(lockInfo.K_seed); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Writing copri seed failed.{AuthCharacteristic}{CommandWritten}{Exception}", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed), exception); + throw new System.Exception(string.Format("Can not authenticate. Writing copri seed failed. {0}", exception.Message), exception); + } + if (!success) + { + Log.ForContext().Debug("Writing copri seed failed.{AuthCharacteristic}{CommandWritten}", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed)); + throw new System.Exception("Can not authenticate. Writing copri seed did not succeed."); + } + + Log.ForContext().Debug("Copri seed written successfully.{AuthCharacteristic}{CommandWritten}.", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed)); + + byte[] seedLockEncrypted; + var cts = new CancellationTokenSource(); + cts.CancelAfter(READ_TIMEOUT_MS); + try + { + seedLockEncrypted = await authCharacteristic.ReadAsync(cts.Token); // encrypted seed value (random value) from lock to decypt using k_user + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieveing encrypted random value from lock (seed)(ReadAsync-call) failed.{ReadCharacteristic}{Exception}", ToSerilogString(authCharacteristic), exception); + throw new System.Exception(string.Format("Can not authenticate. Reading encrypted seed failed. {0}", exception.Message), exception); + } + finally + { + cts.Dispose(); + } + + Log.ForContext().Debug("Retrieveing encrypted random value from lock (seed)(ReadAsync-call) succeeded.{ReadCharacteristic}{Reading}", ToSerilogString(authCharacteristic), "***"); + + var crypto = new AuthCryptoHelper( + seedLockEncrypted, + lockInfo.K_u, + cipher); + + byte[] accessKeyEncrypted; + try + { + accessKeyEncrypted = crypto.GetAccessKeyEncrypted(); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Error getting encrypted access key. {Exception}", exception); + throw new System.Exception(string.Format("Can not authenticate. Getting access key failed. {0}", exception.Message), exception); + } + + try + { + success = await authCharacteristic.WriteAsync(accessKeyEncrypted); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}{Exception}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), ToSerilogString(accessKeyEncrypted), exception); + throw new System.Exception(string.Format("Can not authenticate. Writing access key failed. {0}", exception.Message), exception); + } + if (!success) + { + Log.ForContext().Debug("Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), "***"); + throw new System.Exception(string.Format("Can not authenticate. Writing access key did not succeed.")); + } + + Log.ForContext().Debug("Encrypted access key written successfully.{Key}{Seed}{AuthCharacteristic}{CommandWritten}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), "***"); + } + + /// Gets the entire lock state, like locking state (open/ close) and GUID. + /// True if to wait and retry in case of failures. + public async Task GetLockStateAsync(bool doWaitRetry = false) + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not get lock state. Bluetooth code must be run on main thread"); + } + + DeviceState? deviceState; + Log.ForContext().Debug("Request to get connection state in context of getting locking state."); + try + { + deviceState = GetDeviceState(Device.State); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Can not get lock state. Retrieving bluetooth state failed. {Exception}", exception); + throw new System.Exception(string.Format("Can not get lock state. Getting bluetooth state failed. {0}", exception.Message), exception); + } + + switch (deviceState) + { + case DeviceState.Disconnected: + throw new BluetoothDisconnectedException(); + + case DeviceState.Connected: + break; + + default: + // Can not get state if device is not connected. + Log.ForContext().Error($"Getting lock state failed. Unexpected lock state {deviceState} detected."); + throw new System.Exception(string.Format("Can not get lock state. Unexpected bluetooth state {0} detected.", deviceState)); + } + + Log.ForContext().Debug($"Connection state is {deviceState}."); + + var stateCharacteristic = await GetStateCharacteristicAsync(); + if (stateCharacteristic == null) + { + Log.ForContext().Error($"Can not get lock state. State characteristic is not available."); + throw new CoundntGetCharacteristicException("Can not get lock state. State characteristic must not be null."); + } + + byte[] state; + async Task readAsyncDelegate() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(READ_TIMEOUT_MS); + try + { + return await stateCharacteristic.ReadAsync(cts.Token); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving lock state (ReadAsync-call) failed inside delegate.{StateCharacteristic}{Exception}", ToSerilogString(stateCharacteristic), exception); + throw; + } + finally + { + cts.Dispose(); + } + } + + try + { + state = doWaitRetry + ? await _retryPollicy.ExecuteAsync(async () => await readAsyncDelegate()) + : await readAsyncDelegate(); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving lock state (ReadAsync-call) failed.{StateCharacteristic}{Exception}", ToSerilogString(stateCharacteristic), exception); + throw new System.Exception(string.Format("Can not get lock state. Reading data failed. {0}", exception.Message), exception); + } + if (state == null || state.Length <= 0) + { + Log.ForContext().Debug("Retrieving lock state (ReadAsync-call) failed. Data read is null or empty.{StateCharacteristic}", ToSerilogString(stateCharacteristic)); + throw new System.Exception("Can not get lock state. No data read"); + } + + var lockInfoTdo = new LockInfoTdo.Builder + { + Id = Id, + Guid = Guid, + State = (LockitLockingState?)state[0] + }.Build(); + + Log.ForContext().Debug("Retrieving lock state (ReadAsync-call) succeeded.{@LockInfoTdo}{StateCharacteristic}{Reading}", lockInfoTdo, ToSerilogString(stateCharacteristic), state); + + return lockInfoTdo; + } + + /// Gets the battery percentage. + public async Task GetBatteryPercentageAsync() + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not get battery percentage. Bluetooth code must be run on main thread"); + } + + DeviceState? deviceState; + Log.ForContext().Debug("Request to get battery percentage in context of getting locking state."); + try + { + deviceState = GetDeviceState(Device.State); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Can not get battery percentage. Retrieving bluetooth state failed. {Exception}", exception); + throw new System.Exception(string.Format("Can not get battery percentage. Getting bluetooth state failed. {0}", exception.Message), exception); + } + + switch (deviceState) + { + case DeviceState.Disconnected: + throw new BluetoothDisconnectedException(); + + case DeviceState.Connected: + break; + + default: + // Can not get state if device is not connected. + Log.ForContext().Error($"Getting battery percentage failed. Unexpected battery percentage {deviceState} detected."); + throw new System.Exception(string.Format("Can not get battery percentage. Unexpected bluetooth state {0} detected.", deviceState)); + } + + Log.ForContext().Debug($"Connection state is {deviceState}."); + + var batteryCharacteristic = await GetBatteryCharacteristicAsync(); + if (batteryCharacteristic == null) + { + Log.ForContext().Error($"Can not get battery percentage. State characteristic is not available."); + throw new CoundntGetCharacteristicException("Can not get battery percentage. State characteristic must not be null."); + } + + byte[] state; + var cts = new CancellationTokenSource(); + cts.CancelAfter(READ_TIMEOUT_MS); + try + { + state = await batteryCharacteristic.ReadAsync(cts.Token); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving charging level (ReadAsync-call) failed. {BatteryCharacteristic}{Exception}", ToSerilogString(batteryCharacteristic), exception); + throw new System.Exception(string.Format("Can not get battery percentage. Reading data failed. {0}", exception.Message), exception); + } + finally + { + cts.Dispose(); + } + + if (state == null || state.Length <= 0) + { + Log.ForContext().Debug("Retrieving charging level (ReadAsync-call) failed. Data read is null or empty.{BatteryCharacteristic}", ToSerilogString(batteryCharacteristic)); + throw new System.Exception("Can not get battery percentage. No data read."); + } + + Log.ForContext().Debug("Retrieving charging level (ReadAsync-call) succeeded.{Level}{BatteryCharacteristic}{Reading}", state[0], ToSerilogString(batteryCharacteristic), state); + + return state[0]; + } + + /// Opens lock. + /// Locking state. + public abstract Task OpenAsync(); + + /// Close the lock. + /// Locking state. + public abstract Task CloseAsync(); + + /// Opens/ closes lock. + /// + /// + /// True if opening/ closing command could be written successfully. + protected async Task OpenCloseLock(bool open) + { + DeviceState deviceState; + Log.ForContext().Debug("Request to get connection state in context of opening/ closing lock."); + try + { + deviceState = GetDeviceState(Device.State); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving bluetooth state failed when opening/ closing lock failed. {Exception}", exception); + throw new System.Exception($"Can not Open/ Close lock. Getting bluetooth failed. {exception.Message}", exception); + } + + switch (deviceState) + { + case DeviceState.Disconnected: + throw new BluetoothDisconnectedException(); + + case DeviceState.Connected: + break; + + default: + // Can not open lock if bluetooth state is not connected. + Log.ForContext().Debug($"Can not open/ close lock. Unexpected connection state detected {deviceState}."); + return false; + } + + Log.ForContext().Debug($"Connection state before opening/ closing lock is {deviceState}."); + + var activateLockCharacteristic = await GetActivateLockCharacteristicAsync(); + if (activateLockCharacteristic == null) + { + Log.ForContext().Debug("Getting lock control service failed."); + return false; + } + + byte[] state = bitShift( + new byte[] { 0, 0, open ? (byte)1 : (byte)2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + ++ActivateLockWriteCounter); + + byte[] stateEnctryped; + try + { + stateEnctryped = Cipher.Encrypt(CopriKey, state); + } + catch (System.Exception exception) + { + ActivateLockWriteCounter--; + Log.ForContext().Error("Encypting command to open/ close lock failed. {Exception}", exception); + throw new System.Exception($"Can not open/ close lock. Encrypting command to lock/ unlock failed. {exception.Message}", exception); + } + + bool success; + try + { + success = await activateLockCharacteristic.WriteAsync(stateEnctryped); + } + catch (System.Exception exception) + { + ActivateLockWriteCounter--; + Log.ForContext().Error(open + ? "Writing data to open lock failed.{ActivateLockCharacteristic}{CommandWritten}{Exception}" + : "Writing data to close lock failed.{ActivateLockCharacteristic}{CommandWritten}{Exception}", + ToSerilogString(activateLockCharacteristic), ToSerilogString(stateEnctryped), exception); + throw new System.Exception($"Can not {(open ? "open" : "close")} lock. Writing command to lock/ unlock failed. {exception.Message}", exception); + } + + Log.ForContext().Debug(open + ? "Command to open lock written successfully.{ActivateLockCharacteristic}{CommandWritten}" + : "Command to open lock written successfully.{ActivateLockCharacteristic}{CommandWritten}", + ToSerilogString(activateLockCharacteristic), ToSerilogString(stateEnctryped)); + + return success; + } + + /// Turns off the alarm. + public async Task GetIsAlarmOffAsync() + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not turn alarm off. Bluetooth code must be run on main thread"); + } + + var alarmCharacteristic = await GetAlarmCharacteristicAsync(); + if (alarmCharacteristic == null) + { + Log.ForContext().Debug("Getting alarm characteristic failed."); + return false; + } + + byte[] alarmSettings; + var cts = new CancellationTokenSource(); + cts.CancelAfter(READ_TIMEOUT_MS); + try + { + alarmSettings = await alarmCharacteristic.ReadAsync(cts.Token); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{Exception}", ToSerilogString(alarmCharacteristic), exception); + throw new System.Exception($"Can not get whether alarm is off or on {exception.Message}."); + } + finally + { + cts.Dispose(); + } + + if (alarmSettings == null || alarmSettings.Length < 1) + { + Log.ForContext().Error("Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{Reading}", ToSerilogString(alarmCharacteristic)); + throw new System.Exception("Can not get whether alarm is off or on. Reading failed."); + } + + var isAlarmOff = alarmSettings[0] == 0; + Log.ForContext().Debug("Retrieving alarm settings (ReadAsync-call) succeeded.{IsArlarmOff}{AlarmCharacteristic}{Reading}", isAlarmOff, ToSerilogString(alarmCharacteristic), alarmSettings); + return isAlarmOff; + } + + /// Sets alarm on or off. + public async Task SetIsAlarmOffAsync(bool isActivated) + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not turn alarm off. Bluetooth code must be run on main thread"); + } + + ICharacteristic alarmCharacteristic = await GetAlarmCharacteristicAsync(); + if (alarmCharacteristic == null) + { + Log.ForContext().Debug("Getting alarm characteristic failed."); + throw new System.Exception($"Can not set alarm {isActivated}. Getting alarm characteristic returned null."); + } + + bool success; + var command = new byte[] { isActivated ? (byte)1 : (byte)0 }; + try + { + success = await alarmCharacteristic.WriteAsync(command); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}{Exception}", ToSerilogString(alarmCharacteristic), command[0], exception); + throw new System.Exception($"Can not set alarm settings. {exception.Message}", exception); + } + + if (!success) + { + Log.ForContext().Error("Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}", ToSerilogString(alarmCharacteristic), command[0]); + throw new System.Exception($"Can not set alarm settings. Writing settings did not succeed."); + } + + Log.ForContext().Debug("Sound settings written successfully.{AlarmCharacteristic}{CommandWritten}.", ToSerilogString(alarmCharacteristic), command[0]); + } + + public async Task SetSoundAsync(SoundSettings settings) + { + if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) + { + throw new System.Exception("Can not set sound settings. Bluetooth code must be run on main thread"); + } + + ICharacteristic soundCharacteristic = await GetSoundCharacteristicAsync(); + if (soundCharacteristic == null) + { + Log.ForContext().Debug("Getting sound characteristic failed."); + return false; + } + + bool success; + byte command = (byte)settings; + try + { + success = await soundCharacteristic.WriteAsync(new byte[] { command }); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Writing sound settings failed.{SoundCharacteristic}{CommandWritten}{Exception}", ToSerilogString(soundCharacteristic), command, exception); + throw new System.Exception($"Can not set sound settings. {exception.Message}", exception); + } + + Log.ForContext().Debug(success + ? "Writing sound settings failed.{SoundCharacteristic}{CommandWritten}" + : "Sound setting written successfully.{SoundCharacteristic}{CommandWritten}", + ToSerilogString(soundCharacteristic), command); + + return success; + } + + private static byte[] bitShift(byte[] data, int counter) + { + int mask = 0x000000FF; + data[0] = (byte)(counter & mask); + data[1] = (byte)(counter >> 8); + + return data; + } + + private static DeviceState GetDeviceState(Plugin.BLE.Abstractions.DeviceState state) + { + switch (state) + { + case Plugin.BLE.Abstractions.DeviceState.Disconnected: + return DeviceState.Disconnected; + + case Plugin.BLE.Abstractions.DeviceState.Connecting: + return DeviceState.Connecting; + + case Plugin.BLE.Abstractions.DeviceState.Connected: + return DeviceState.Connected; + + case Plugin.BLE.Abstractions.DeviceState.Limited: + return DeviceState.Limited; + + default: + throw new ArgumentException($"Can not convert state {state}"); + } + } + + /// Disconnect from bluetooth lock. + public async Task Disconnect() => await Adapter.DisconnectDeviceAsync(Device); + + /// + /// Don' t use .Destructure.ByTransforming(...) because this would introduce a dependency to Plugin.BLE in main project. + /// + /// + /// + private static string ToSerilogString(ICharacteristic charcteristic) + => charcteristic.Id.ToString(); + + private static string ToSerilogString(byte[] byteArray) + => "***"; // For debuging purposes it might be reqired to return string.Join(",", byteArray); Do not log any confidental value in production context. + } +} diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItByGuidService.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItByGuidService.cs new file mode 100644 index 0000000..ad0ad60 --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItByGuidService.cs @@ -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 + { + /// Constructs a LockItByGuidService object. + /// Encrpyting/ decrypting object. + public LockItByGuidService(ICipher cipher) : base(cipher) + { + } + + /// Checks for locks which have not yet been discoverted and connects them. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Locks to reconnect. + /// Timeout for connect operation of a single lock. + protected override async Task> CheckConnectMissing(IEnumerable 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(); + + // 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().Error($"Authentication failed. {exception.Message}"); + continue; + } + if (lockIt == null) + { + continue; + } + + locksList.Add(lockIt); + } + + return locksList; + } + + /// Connects to lock. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect to lock. + /// Timeout for connect operation. + public async Task 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(); + } + + /// Connects to lock. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect to lock. + /// Timeout for connect operation. + private async Task ConnectByGuid(LockInfoAuthTdo authInfo, TimeSpan connectTimeout) + { + if (authInfo.Guid == TextToLockItTypeHelper.INVALIDLOCKGUID) + { + Log.ForContext().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().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().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().Error("Connect to device failed."); + return null; + } + + return lockIt; + } + } +} diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItByScanService.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItByScanService.cs new file mode 100644 index 0000000..63e5d80 --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItByScanService.cs @@ -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 +{ + /// Manages ILockIt- Locks. + public abstract class LockItByScanServiceBase : LockItServiceBase + { + private Func> AuthenticateDelegate { get; set; } + + public LockItByScanServiceBase( + ICipher cipher, + Func> authenticateDelegate) : base(cipher) + { + AuthenticateDelegate = authenticateDelegate; + } + + /// Connects to lock. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect to lock. + /// Timeout for connect operation. + public async Task 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().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().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 { authInfo.Id }, connectTimeout); + + var bleDevice = newlyDiscovertedDevice.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id); + + if (bleDevice == null) + { + + Log.ForContext().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().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().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().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(null); + } + + DeviceList.Add(lockIt); + + return await lockIt.GetLockStateAsync(); + } + + /// Checks for locks which have not yet been discoverted and connects them. + /// Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be connected. + /// Locks to reconnect. + /// Timeout for connect operation of a single lock. + protected override async Task> CheckConnectMissing(IEnumerable 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(); + } + + // 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(); + + // 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().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().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().Error($"Authentication failed. {exception.Message}"); + continue; + } + if (lockIt == null) + { + // connectiong to device succeded. + continue; + } + + Log.ForContext().Debug($"Auth succeeded for device {device}."); + locksList.Add(lockIt); + } + + return locksList; + } + + /// Scans for a given set of devices. + /// Locks to scan for. + /// + private static async Task> ScanForNewDevices( + IEnumerable targetLocks, + TimeSpan connectTimeout) + { + var adapter = CrossBluetoothLE.Current.Adapter; + + var newlyDiscovertedDevicesList = new List(); + + 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().Debug($"Lock without name discovered."); + return; + } + + if (targetLocks.Where(x => x != TextToLockItTypeHelper.INVALIDLOCKID).Contains(name.GetBluetoothLockId())) + { + Log.ForContext().Debug($"New LOCKIT device {name} discovered."); + + newlyDiscovertedDevicesList.Add(a.Device); + + if (newlyDiscovertedDevicesList.Count() >= targetLocks.Count()) + { + cts.Cancel(); + } + + return; + } + + Log.ForContext().Verbose($"Device of unknown advertisement name {name} discovered."); + }; + + try + { + await adapter.StartScanningForDevicesAsync(cancellationToken: cts.Token); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Bluetooth scan failed. {Exception}", exception); + throw; + } + finally + { + try + { + await adapter.StopScanningForDevicesAsync(); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Stop scanning failed. {Excpetion}", exception); + throw; + } + } + + return newlyDiscovertedDevicesList; + } + } +} diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItByScanServiceEventBased.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItByScanServiceEventBased.cs new file mode 100644 index 0000000..6158a2a --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItByScanServiceEventBased.cs @@ -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)) { } + } +} diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItByScanServicePolling.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItByScanServicePolling.cs new file mode 100644 index 0000000..a9571c7 --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItByScanServicePolling.cs @@ -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)) { } + } +} diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItEventBased.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItEventBased.cs new file mode 100644 index 0000000..e776e2e --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItEventBased.cs @@ -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) + { + } + + /// Reconnects to device. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect. + /// Timeout to apply when connecting to bluetooth lock. + /// True if connecting succeeded, false if not. + public async override Task ReconnectAsync( + LockInfoAuthTdo authInfo, + TimeSpan connectTimeout) + => await ReconnectAsync( + authInfo, + connectTimeout, + () => new LockItEventBased(Device, Adapter, Cipher)); + + /// Connects to device. + /// Info required to connect. + /// Device with must be connected. + /// True if connecting succeeded, false if not. + public static async Task Authenticate( + IDevice device, + LockInfoAuthTdo authInfo, + IAdapter adapter, + ICipher cipher) + => await Authenticate( + device, + authInfo, + adapter, + cipher, + () => new LockItEventBased(device, adapter, cipher)); + + /// Opens lock. + /// Locking state. + public async override Task 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().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().Information("No need to open lock. Lock is already open."); + return await Task.FromResult((LockitLockingState?)null); + } + + Log.ForContext().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 tcs = new TaskCompletionSource(); + var cts = new CancellationTokenSource(OPEN_CLOSE_TIMEOUT_MS); + cts.Token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false); + + EventHandler lockStateCharcteristicChanged = (sender, args) => GetStateValue(tcs, args.Characteristic.Value); + EventHandler batteryStateCharcteristicChanged = (sender, args) => batteryPercentage = GetChargeValue(args.Characteristic.Value); + try + { + lockStateCharacteristic.ValueUpdated += lockStateCharcteristicChanged; + batteryStateCharacteristic.ValueUpdated += batteryStateCharcteristicChanged; + } catch (System.Exception ex) + { + Log.ForContext().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().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().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().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()); + } + } + + /// Close the lock. + /// Locking state. + public async override Task 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().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().Error("No need to close lock. Lock is already closed."); + return await Task.FromResult(lockingState.Value); + } + + lockingState = null; + var lockStateCharacteristic = await GetStateCharacteristicAsync(); + + TaskCompletionSource tcs = new TaskCompletionSource(); + var cts = new CancellationTokenSource(OPEN_CLOSE_TIMEOUT_MS); + cts.Token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false); + + EventHandler lockStateCharcteristicChanged = (sender, args) => GetStateValue(tcs, args.Characteristic.Value); + + try + { + lockStateCharacteristic.ValueUpdated += lockStateCharcteristicChanged; + } + catch (System.Exception ex) + { + Log.ForContext().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().Error("Starting update when closing lock failed.{Exception}", ex); + return await Task.FromResult((LockitLockingState?)null); + } + + try + { + Log.ForContext().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().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().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()); + } + } + + /// Gets the locking state from event argument. + Action, byte[]> GetStateValue = (tcs, value) => + { + try + { + if (value?.Length <= 0) + { + tcs.TrySetResult(null); + return; + } + + tcs.TrySetResult((LockitLockingState?)value[0]); + } + catch (System.Exception ex) + { + Log.ForContext().Error("Error on sinking lock state characteristic on opening/closing .{Exception}", ex); + tcs.TrySetResult(null); + } + }; + + /// Gets the battery state from lock. + Func GetChargeValue = (value) => + { + try + { + if (value.Length <= 0) + { + return double.NaN; + } + + return value[0]; + } + catch (System.Exception ex) + { + Log.ForContext().Error("Error on sinking battery state characteristic on opening.{Exception}", ex); + return double.NaN; + } + }; + } +} diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItPolling.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItPolling.cs new file mode 100644 index 0000000..8ec89ac --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItPolling.cs @@ -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) + { + } + + /// Reconnects to device. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect. + /// Timeout to apply when connecting to bluetooth lock. + /// True if connecting succeeded, false if not. + public override async Task ReconnectAsync( + LockInfoAuthTdo authInfo, + TimeSpan connectTimeout) + => await ReconnectAsync( + authInfo, + connectTimeout, + () => new LockItPolling(Device, Adapter, Cipher)); + + /// Connects to device. + /// Info required to connect. + /// Device with must be connected. + /// True if connecting succeeded, false if not. + public static async Task Authenticate( + IDevice device, + LockInfoAuthTdo authInfo, + IAdapter adapter, + ICipher cipher) + => await Authenticate( + device, + authInfo, + adapter, + cipher, + () => new LockItPolling(device, adapter, cipher)); + + /// Opens lock. + /// Locking state. + public async override Task 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().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().Information("No need to open lock. Lock is already open."); + return await Task.FromResult((LockitLockingState?)null); + } + + Log.ForContext().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().Information($"Opening lock failed."); + return await Task.FromResult(info.State.Value); + } + + info = await GetLockStateAsync(); + if (info?.State == null) + { + // Device not reachable. + Log.ForContext().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().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()); + } + } + + /// Close the lock. + /// Locking state. + public async override Task 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().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().Error("No need to close lock. Lock is already closed."); + return await Task.FromResult(info.State.Value); + } + + Log.ForContext().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().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().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().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()); + } + } + } +} diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItServiceBase.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItServiceBase.cs new file mode 100644 index 0000000..9450497 --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItServiceBase.cs @@ -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 + { + /// Constructs base object. + /// Encrpyting/ decrypting object. /// Timeout to apply when connecting to bluetooth lock. + public LockItServiceBase(ICipher cipher) + { + Cipher = cipher; + } + + /// Holds timeout values for series of connecting attemps to a lock or multiple locks. + public ITimeOutProvider TimeOut { get; set; } + + protected ICipher Cipher { get; } + + /// List of available or connected bluetooth devices. + protected List DeviceList = new List(); + + + /// Reconnect locks of interest if required. + /// Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be reconnected. + /// Locks to reconnect. + /// Timeout for connect operation. + public static async Task CheckReconnect( + IEnumerable deviceList, + IEnumerable 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().Error($"Reconnect failed. {exception.Message}"); + continue; + } + } + } + + /// Gets the state for locks of interest. + /// + /// Might require a + /// - connect to lock + /// - reconnect to lock + /// + /// Locks toget info for. + /// Timeout for connect operation of a single lock. + public async Task> GetLocksStateAsync( + IEnumerable locksInfo, + TimeSpan connectTimeout) + { + if (locksInfo.Count() == 0) + { + // Nothing to do. + return new List(); + } + + // 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(); + + 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; + } + + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Locks to reconnect. + /// Timeout for connect operation of a single lock. + protected abstract Task> CheckConnectMissing( + IEnumerable locksInfo, + TimeSpan connectTimeout); + + /// Gets a lock by bike Id. + /// + /// Lock object + public ILockService this[int bikeId] + { + get + { + var device = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == bikeId); + if (device == null) + { + return new NullLock(bikeId); + } + + return device; + } + } + + /// Connects to lock. + /// Consists of a bluetooth connect plus invocation of an authentication sequence. + /// Info required to connect to lock. + /// Timeout for connect operation. + public async Task 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().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; + } + } +} \ No newline at end of file diff --git a/LockItBLE/Services/BluetoothLock/BLE/LockItServiceHelper.cs b/LockItBLE/Services/BluetoothLock/BLE/LockItServiceHelper.cs new file mode 100644 index 0000000..656df42 --- /dev/null +++ b/LockItBLE/Services/BluetoothLock/BLE/LockItServiceHelper.cs @@ -0,0 +1,9 @@ +using TINK.Model.Connector; + +namespace TINK.Services.BluetoothLock.BLE +{ + public static class LockItServiceHelper + { + public static string Name { get => TextToLockItTypeHelper.ISHAREITADVERTISMENTTITLE; } + } +} diff --git a/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItBase.cs b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItBase.cs new file mode 100644 index 0000000..61aec97 --- /dev/null +++ b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItBase.cs @@ -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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(Arg.Any()).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(Arg.Any()).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(Arg.Any()).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(Arg.Any()).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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); + } + + /// Derives from LockItBase class for testing purposes. + + private class LockItBaseTest : LockItBase + { + private LockItBaseTest(IDevice device, IAdapter adapter, ICipher cipher) : base(device, adapter, cipher) + { + } + /// Connects to device. + /// Info required to connect. + /// Device with must be connected. + /// True if connecting succeeded, false if not. + public static async Task Authenticate( + IDevice device, + LockInfoAuthTdo authInfo, + IAdapter adapter, + ICipher cipher) + => await Authenticate( + device, + authInfo, + adapter, + cipher, + () => new LockItBaseTest(device, adapter, cipher)); + + public override Task CloseAsync() + { + throw new NotImplementedException(); + } + + public override Task OpenAsync() + { + throw new NotImplementedException(); + } + + public override Task ReconnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItEventBased.cs b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItEventBased.cs new file mode 100644 index 0000000..1711af5 --- /dev/null +++ b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItEventBased.cs @@ -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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var stateCharacteristic = Substitute.For(); + var activateCharacteristic = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var stateCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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()); + } + + [Test] + public void TestOpen_ThrowsCouldntOpenBoldBlockedException() + { + var device = Substitute.For(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + var stateCharacteristic = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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()); + } + + [Test] + public void TestClose() + { + var device = Substitute.For(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + var stateCharacteristic = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + var stateCharacteristic = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).Returns(Task.FromResult(new byte[] { 0 /* opened */})); + lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock)); + activateLock.WriteAsync(Arg.Any()).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()); + } + + [Test] + public void TestClose_ThrowsCouldntCloseBoldBlockedException() + { + var device = Substitute.For(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var lockControl = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + var stateCharacteristic = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(lockControl)); + lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).Returns(Task.FromResult(new byte[] { 0 /* open */})); + lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock)); + activateLock.WriteAsync(Arg.Any()).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()); + } + } +} \ No newline at end of file diff --git a/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItPolling.cs b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItPolling.cs new file mode 100644 index 0000000..867e3cd --- /dev/null +++ b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItPolling.cs @@ -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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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()); + } + + [Test] + public void TestOpen_ThrowsCouldntOpenBoldBlockedException() + { + var device = Substitute.For(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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()); + } + + [Test] + public void TestClose() + { + var device = Substitute.For(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).Returns(Task.FromResult(new byte[] { 0 /* opened */})); + controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock)); + activateLock.WriteAsync(Arg.Any()).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()); + } + + [Test] + public void TestClose_ThrowsCouldntOpenBoldBlockedException() + { + var device = Substitute.For(); + var adapter = Substitute.For(); + var cipher = Substitute.For(); + var auth = Substitute.For(); + var controlService = Substitute.For(); + var controlCharacteristic = Substitute.For(); + var activateLock = Substitute.For(); + + 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(), Arg.Any()).Returns(Task.FromResult(controlService)); + controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth)); + auth.WriteAsync(Arg.Any()).Returns(Task.FromResult(true)); + auth.ReadAsync(Arg.Any()).Returns(Task.FromResult(new byte[8])); + cipher.Decrypt(Arg.Any(), Arg.Any()).Returns(new byte[3]); + cipher.Encrypt(Arg.Any(), Arg.Any()).Returns(new byte[16]); + auth.WriteAsync(Arg.Any()).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()).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()).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()); + } + } +} diff --git a/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItServiceBase.cs b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItServiceBase.cs new file mode 100644 index 0000000..6673e87 --- /dev/null +++ b/TestLockItBLE/Services/BluetoothLock/BLE/TestLockItServiceBase.cs @@ -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(); + disconnectedDevice.GetDeviceState().Returns(DeviceState.Disconnected); + var devices = new List { disconnectedDevice }; + var locksInfo = new List(); + + await LockItServiceBase.CheckReconnect( + devices, + locksInfo, + TimeSpan.FromSeconds(0)); + + await disconnectedDevice.DidNotReceive().ReconnectAsync(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task TestCheckReconnect_NoMatchingIdEntry() + { + var disconnectedDevice = Substitute.For(); + disconnectedDevice.GetDeviceState().Returns(DeviceState.Disconnected); + disconnectedDevice.Name.Returns("ISHAREIT+334"); + disconnectedDevice.Guid.Returns(new Guid("00000000-0000-0000-0000-000000000001")); + + var devices = new List { disconnectedDevice }; + var locksInfo = new List { 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(), Arg.Any()); + } + + [Test] + public async Task TestCheckReconnect() + { + var disconnectedDevice = Substitute.For(); + disconnectedDevice.GetDeviceState().Returns(DeviceState.Disconnected); + disconnectedDevice.Name.Returns("ISHAREIT+992"); + disconnectedDevice.Guid.Returns(new Guid("00000000-0000-0000-0000-000000000001")); + + var devices = new List { disconnectedDevice }; + var locksInfo = new List { 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(), Arg.Any()); + } + } +} diff --git a/TestLockItBLE/TestLockItBLE.csproj b/TestLockItBLE/TestLockItBLE.csproj new file mode 100644 index 0000000..f66ed4a --- /dev/null +++ b/TestLockItBLE/TestLockItBLE.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + diff --git a/TestLockItBLE/TestLockItBLE.sln b/TestLockItBLE/TestLockItBLE.sln new file mode 100644 index 0000000..ad3ee03 --- /dev/null +++ b/TestLockItBLE/TestLockItBLE.sln @@ -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