using System; using System.Diagnostics; using System.Threading.Tasks; using Plugin.BLE.Abstractions.Contracts; using Serilog; using TINK.Model.Bikes.BikeInfoNS.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 the lock. public async override Task OpenAsync() { if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) { throw new System.Exception("Can not open lock. Platform must not be unknown and 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 open lock. Current locking state is {info}, counter value {ActivateLockWriteCounter}."); var result = await OpenCloseLockAsync( 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().Debug($"Waiting for lock to open. Current lock state is {info?.State.Value}."); } if (info == null) { Log.ForContext().Fatal($"Opening lock failed. State object is null."); return null; } switch (info.State.Value) { case LockitLockingState.Open: Log.ForContext().Information($"Lock was opened successfully."); return info.State.Value; case LockitLockingState.CouldntOpenBoldBlocked: // Expected error. ILockIt count not be opened (Spoke blocks lock, ....) Log.ForContext().Debug($"Opening lock failed. Bold is blocked."); throw new CouldntOpenBoldIsBlockedException(); case LockitLockingState.Unknown: // Expected error. ILockIt count not be opened (Spoke has blocked/ blocks lock, ....) Log.ForContext().Debug($"Opening lock failed. Bold status is unknown."); throw new CouldntOpenBoldStatusIsUnknownException(); default: // Comprises values // - LockitLockingState.Closed // - LockitLockingState.CouldntCloseMoving (should never happen because command open was send) // - LockitLockingState.CouldntCloseBoldBlocked (should never happen because command open was send) // Internal error which should never occur. Lock refuses to open but connection is ok. Log.ForContext().Debug($"Opening lock failed. Unexpected lock state {info.State.Value.GetLockingState()} detected."); 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. Platform must not be unknown and 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 close lock. Current locking state is {info}, counter value {ActivateLockWriteCounter}."); var result = await OpenCloseLockAsync(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(); var hasBeenLocked = false; while (info.State != null && 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. if (info.State.Value == LockitLockingState.CouldntCloseBoldBlocked) { // Lock reported a blocked bold. hasBeenLocked = true; Log.ForContext().Debug($"Waiting for lock to close. Bold is blocked."); continue; } if (hasBeenLocked && info.State.Value == LockitLockingState.Open) { // ILockIt could not be closed. Bolt was blocked but was opened again. Log.ForContext().Debug($"Closing lock failed. Bold was blocked and lock was reopened."); throw new CouldntCloseBoldBlockedException(LockingState.Open); } Log.ForContext().Debug($"Waiting for lock to close. Current lock state is {info?.State.Value}."); } if (info == null) { Log.ForContext().Fatal($"Closing lock failed. State object is null."); return null; } switch (info.State.Value) { case LockitLockingState.CouldntCloseBoldBlocked: // Expected error. ILockIt could not be closed (Spoke blocks lock, ....) Log.ForContext().Debug($"Closing lock failed. Bold is blocked."); throw new CouldntCloseBoldBlockedException(); case LockitLockingState.CouldntCloseMoving: // Expected error. ILockIt could not be closed (bike is moving) Log.ForContext().Debug($"Closing lock failed. Bike is moving."); throw new CouldntCloseMovingException(); case LockitLockingState.Closed: // Everything is ok. Log.ForContext().Information($"Lock was closed successfully."); return info.State.Value; default: // Comprises values // - LockitLockingState.Open // - LockitLockingState.Unknown // - LockitLockingState.CouldntOpenBoldBlocked // Internal error which should never occur. Lock refuses to close but connection is ok. Log.ForContext().Debug($"Closing lock failed. Unexpected lock state {info.State.Value.GetLockingState()} detected."); throw new CouldntCloseInconsistentStateExecption(info.State.Value.GetLockingState()); } } } }