using System; using System.Diagnostics; using System.Threading.Tasks; using Plugin.BLE.Abstractions.Contracts; using Serilog; using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock; using ShareeBike.Model.Device; using ShareeBike.Services.BluetoothLock.Exception; using ShareeBike.Services.BluetoothLock.Tdo; using Xamarin.Essentials; namespace ShareeBike.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 is LockitLockingState initialLockingState)) { // Device not reachable. Log.ForContext().Information("Can not open lock. Device is not reachable (get state)."); return await Task.FromResult((LockitLockingState?)null); } if (initialLockingState.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}."); // Send command to open to lock. 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(initialLockingState); } var subsequentLockingStateNullable = (await GetLockStateAsync()).State; if (subsequentLockingStateNullable == 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 (subsequentLockingStateNullable != null && subsequentLockingStateNullable != LockitLockingState.CouldntOpenBoltBlocked && subsequentLockingStateNullable != LockitLockingState.Open && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS)) { subsequentLockingStateNullable = (await GetLockStateAsync(true)).State; // While opening lock seems not always respond to reading operations. Log.ForContext().Debug($"Waiting for lock to open. Current lock state is {subsequentLockingStateNullable}."); } if (!(subsequentLockingStateNullable is LockitLockingState subsequentLockingState)) { Log.ForContext().Fatal($"Opening lock failed. State object is null."); return null; } switch (subsequentLockingState) { case LockitLockingState.Open: Log.ForContext().Information($"Lock was opened successfully."); return subsequentLockingState; case LockitLockingState.CouldntOpenBoltBlocked: // 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.CouldntCloseBoltBlocked (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 {subsequentLockingState.GetLockingState()} detected."); throw new CouldntOpenInconsistentStateExecption(subsequentLockingState.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 is LockitLockingState initialLockingState)) { // Device not reachable. Log.ForContext().Error("Can not close lock. Device is not reachable (get state)."); return await Task.FromResult((LockitLockingState?)null); } if (initialLockingState.GetLockingState() == LockingState.Closed) { // Lock is already closed. Log.ForContext().Error("No need to close lock. Lock is already closed."); return await Task.FromResult(initialLockingState); } Log.ForContext().Debug($"Request to close lock. Current locking state is {info}, counter value {ActivateLockWriteCounter}."); // Send command to close to lock. 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(initialLockingState); } // Get lock state until either lock state changes or until log gets unreachable. var subsequentLockingStateNullable = (await GetLockStateAsync()).State; if (subsequentLockingStateNullable == 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 (subsequentLockingStateNullable != null && subsequentLockingStateNullable != LockitLockingState.CouldntCloseMoving && subsequentLockingStateNullable != LockitLockingState.Closed && watch.Elapsed < TimeSpan.FromMilliseconds(OPEN_CLOSE_TIMEOUT_MS)) { subsequentLockingStateNullable = (await GetLockStateAsync(true)).State; ; // While closing lock seems not always respond to reading operations. if (subsequentLockingStateNullable == LockitLockingState.CouldntCloseBoltBlocked) { // Lock reported a blocked bold. hasBeenLocked = true; Log.ForContext().Debug($"Waiting for lock to close. Bold is blocked."); continue; } if (hasBeenLocked && subsequentLockingStateNullable == 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 CouldntCloseBoltBlockedException(LockingState.Open); } Log.ForContext().Debug($"Waiting for lock to close. Current lock state is {subsequentLockingStateNullable}."); } if (info == null) { Log.ForContext().Fatal($"Closing lock failed. State object is null."); return null; } if (!(subsequentLockingStateNullable is LockitLockingState subsequentLockingState)) { // Device not reachable. Log.ForContext().Information($"Lock state after close command unknown."); return await Task.FromResult((LockitLockingState?)null); } switch (subsequentLockingState) { case LockitLockingState.CouldntCloseBoltBlocked: // Expected error. ILockIt could not be closed (Spoke blocks lock, ....) Log.ForContext().Debug($"Closing lock failed. Bold is blocked."); throw new CouldntCloseBoltBlockedException(); 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 subsequentLockingState; default: // Comprises values // - LockitLockingState.Open // - LockitLockingState.Unknown // - LockitLockingState.CouldntOpenBoltBlocked // Internal error which should never occur. Lock refuses to close but connection is ok. Log.ForContext().Debug($"Closing lock failed. Unexpected lock state {subsequentLockingState.GetLockingState()} detected."); throw new CouldntCloseInconsistentStateExecption(subsequentLockingState.GetLockingState()); } } } }