using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Plugin.BLE.Abstractions; using Plugin.BLE.Abstractions.Contracts; using Plugin.BLE.Abstractions.Exceptions; using Polly; using Polly.Retry; using Serilog; using TINK.Model.Connector; using TINK.Model.Device; using TINK.Services.BluetoothLock.Crypto; using TINK.Services.BluetoothLock.Exception; using TINK.Services.BluetoothLock.Tdo; using Xamarin.Essentials; 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 AlarmSettingsCharacteristic { get; set; } private ICharacteristic AuthCharacteristic { get; set; } private ICharacteristic StateCharacteristic { get; set; } private ICharacteristic SoundCharacteristic { get; set; } private ICharacteristic BatteryCharacteristic { get; set; } private ICharacteristic FirmwareVersionCharacteristic { 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 alarm settings characteristic. private async Task GetAlarmSettingsCharacteristicAsync() { if (AlarmSettingsCharacteristic != null) return AlarmSettingsCharacteristic; AlarmSettingsCharacteristic = null; Log.ForContext().Debug("Request to get alarm settings characteristic."); try { AlarmSettingsCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BFFE-1212-efde-1523-785fef13d123")); } catch (System.Exception exception) { Log.ForContext().Error("Getting alarm settings charcteristic failed. {Exception}", exception); throw new System.Exception($"Can not get alarm settings characteristic. {exception.Message}", exception); } Log.ForContext().Debug("Get alarm settings characteristic retrieved successfully."); return AlarmSettingsCharacteristic; } /// 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; } /// Gets the versions info characteristic. protected async Task GetVersionsCharacteristicAsync() { if (FirmwareVersionCharacteristic != null) return FirmwareVersionCharacteristic ; FirmwareVersionCharacteristic = null; Log.ForContext().Debug("Request to get versions info characteristic."); try { FirmwareVersionCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baad-1212-efde-1523-785fef13d123")); } catch (System.Exception exception) { Log.ForContext().Error("Getting versions info charcteristic failed. {Exception}", exception); throw new System.Exception(string.Format("Can not get versions info characteristic. {0}", exception.Message), exception); } Log.ForContext().Debug("Get versions info characteristic retrieved successfully."); return FirmwareVersionCharacteristic ; } /// 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. Platform must not be unknown and bluetooth code must be run on main thread."); } DeviceState? state; Log.ForContext().Debug("Request to get connection state."); try { state = Device?.State.GetDeviceState() ?? throw new System.Exception("Can not get bluetooth device state. State must not be null."); } 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. Platform must not be unknown and 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; AlarmSettingsCharacteristic = 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. Platform must not be unknown and 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 = device?.State.GetDeviceState() ?? throw new System.Exception("Can not get bluetooth device state. State must not be null."); } 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 lock state like locking state (open/ close). /// True if to wait and retry in case of failures. /// /// Lock state is first byte of of value read from state characteristic ("0000baaa-1212-efde-1523-785fef13d123"). /// Values are as follows /// Open = 0x00, /// Closed = 0x01, /// Unknown = 0x02, /// CouldntCloseMoving = 0x03, /// CouldntOpenBoldBlocked = 0x04, /// CouldntCloseBoldBlocked = 0x05 /// TINK.Services.BluetoothLock.Tdo.LockitLockingState. /// /// Lock state. /// App is not connected to lock. /// Getting state characteristic to read from failed. /// /// Call not from main thread or unkonwn platform detected or /// query device state (connected, disconnected, ....) failed for an unknown reason or returned an unexpected value or /// reading state characteristic failed or reading from characteristic was empty. /// /// Exceptions thrown by PluginBle::ICharacteristic.ReasAsync. public async Task GetLockStateAsync(bool doWaitRetry = false) { if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) { throw new System.Exception("Can not get lock state. Platform must not be unknown and 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 = Device?.State.GetDeviceState() ?? throw new System.Exception("Can not get bluetooth device state. State must not be null."); } 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. Platform must not be unknown and 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 = Device?.State.GetDeviceState() ?? throw new System.Exception("Can not get bluetooth device state. State must not be null."); } 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[] percentage; var cts = new CancellationTokenSource(); cts.CancelAfter(READ_TIMEOUT_MS); try { percentage = 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 (percentage == null || percentage.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}", percentage[0], ToSerilogString(batteryCharacteristic), percentage); return percentage[0]; } /// Gets version info about the lock. /// /// Lock state is first byte of of value read from state characteristic ("0000baaa-1212-efde-1523-785fef13d123"). /// Values are as follows /// Byte number 0: firmware version, /// Byte number 1: lock version (2 – classic, 3 – plus, 4 – GPS) /// Byte number 2: hardware version, /// /// . public async Task GetVersionInfoAsync() { if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) { throw new System.Exception("Can not get versions info. Platform must not be unknown and bluetooth code must be run on main thread."); } DeviceState? deviceState; Log.ForContext().Debug("Request to get connection state in context of getting versions info."); try { deviceState = Device?.State.GetDeviceState() ?? throw new System.Exception("Can not get bluetooth device state. State must not be null."); } catch (System.Exception exception) { Log.ForContext().Error("Can not get versions info. Retrieving bluetooth state failed. {Exception}", exception); throw new System.Exception(string.Format("Can not get versions info. 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 versions info failed. Unexpected versions info {deviceState} detected."); throw new System.Exception(string.Format("Can not get versions info. Unexpected bluetooth state {0} detected.", deviceState)); } Log.ForContext().Debug($"Connection state is {deviceState}."); var firmwareVersionCharacteristic = await GetVersionsCharacteristicAsync(); if (firmwareVersionCharacteristic == null) { Log.ForContext().Error($"Can not get versions info. versions info characteristic is not available."); throw new CoundntGetCharacteristicException("Can not get versions info. versions info characteristic must not be null."); } byte[] version; var cts = new CancellationTokenSource(); cts.CancelAfter(READ_TIMEOUT_MS); try { version = await firmwareVersionCharacteristic.ReadAsync(cts.Token); } catch (System.Exception exception) { Log.ForContext().Error("Retrieving versions info (ReadAsync-call) failed inside delegate.{StateCharacteristic}{Exception}", ToSerilogString(firmwareVersionCharacteristic), exception); throw; } finally { cts.Dispose(); } if (version == null || version.Length <= 0) { Log.ForContext().Debug("Retrieving versions info (ReadAsync-call) failed. Data read is null or empty.{StateCharacteristic}", ToSerilogString(firmwareVersionCharacteristic)); throw new System.Exception("Can not get versions info. No data read"); } VersionInfo = new VersionInfoTdo.Builder { FirmwareVersion = version[0], LockVersion = version[1], HardwareVersion = version[2] }.Build(); Log.ForContext().Debug("Retrieving versions info (ReadAsync-call) succeeded. {@LockInfoTdo}{StateCharacteristic}{Reading}", VersionInfo, ToSerilogString(firmwareVersionCharacteristic), version); return VersionInfo; } public VersionInfoTdo VersionInfo { get; private set; } /// 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 OpenCloseLockAsync(bool open) { DeviceState deviceState; Log.ForContext().Debug(open ? "Request to get connection state in context of opening lock." : "Request to get connection state in context of closing lock."); try { deviceState = Device?.State.GetDeviceState() ?? throw new System.Exception(open ? "Can not open lock. Getting bluetooth device state failed. State must not be null." : "Can not close lock. Getting bluetooth device state failed. State must not be null."); } catch (System.Exception exception) { Log.ForContext().Error(open ? "Retrieving bluetooth state failed when opening lock failed. {Exception}" : "Retrieving bluetooth state failed when closing lock failed. {Exception}", exception); throw new System.Exception(open ? $"Can not open lock. Getting bluetooth failed. {exception.Message}" : $"Can not 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(open ? $"Can not open lock. Unexpected connection state detected {deviceState}." : $"Can not close lock. Unexpected connection state detected {deviceState}."); return false; } Log.ForContext().Debug(open ? $"Connection state before opening lock is {deviceState}." : $"Connection state before closing lock is {deviceState}."); var activateLockCharacteristic = await GetActivateLockCharacteristicAsync(); if (activateLockCharacteristic == null) { Log.ForContext().Debug(open ? "Can not open lock. Getting lock control service failed." : "Can not close lock. 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(open ? "Encypting command to open lock failed. {Exception}" : "Encypting command to close lock failed. {Exception}", exception); throw new System.Exception(open ? $"Can not open lock. Encrypting command to lock/ unlock failed. {exception.Message}" : $"Can not 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(open ? $"Can not open lock. Writing command failed. {exception.Message}" : $"Can not close lock. Writing command failed. {exception.Message}", exception); } Log.ForContext().Debug(open ? "Command to open lock written successfully.{ActivateLockCharacteristic}{CommandWritten}" : "Command to close lock written successfully.{ActivateLockCharacteristic}{CommandWritten}", ToSerilogString(activateLockCharacteristic), ToSerilogString(stateEnctryped)); return success; } /// Gets a value indicating whether alarm is on or off. public async Task GetIsAlarmOffAsync() { if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) { throw new System.Exception("Can not turn alarm off. Platform must not be unknown and bluetooth code must be run on main thread."); } var alarmCharacteristic = await GetAlarmCharacteristicAsync(); if (alarmCharacteristic == null) { Log.ForContext().Debug("Getting alarm characteristic failed."); throw new System.Exception($"Can not get alarm whether alarm is on or off. Getting alarm characteristic returned null."); } 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. Platform must not be unknown and 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("Alarm 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. Platform must not be unknown and 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 settings written successfully.{SoundCharacteristic}{CommandWritten}", ToSerilogString(soundCharacteristic), command); return success; } public async Task SetAlarmSettingsAsync(AlarmSettings settings) { if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) { throw new System.Exception("Can not set alarm settings. Platform must not be unknown and bluetooth code must be run on main thread."); } ICharacteristic alarmSettingsCharacteristic = await GetAlarmSettingsCharacteristicAsync(); if (alarmSettingsCharacteristic == null) { Log.ForContext().Debug("Getting alarm settings characteristic failed."); return false; } bool success; byte command = (byte)settings; try { success = await alarmSettingsCharacteristic.WriteAsync(new byte[] { command }); } catch (System.Exception exception) { Log.ForContext().Error("Writing alarm settings failed.{SoundCharacteristic}{CommandWritten}{Exception}", ToSerilogString(alarmSettingsCharacteristic), command, exception); throw new System.Exception($"Can not set alarm settings. {exception.Message}", exception); } Log.ForContext().Debug(success ? "Writing alarm settings failed.{SoundCharacteristic}{CommandWritten}" : "Alarm settings written successfully.{SoundCharacteristic}{CommandWritten}", ToSerilogString(alarmSettingsCharacteristic), 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; } /// 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. } }