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