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) { } /// <summary> Reconnects to device. </summary> /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks> /// <param name="authInfo">Info required to connect.</param> /// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param> /// <returns>True if connecting succeeded, false if not.</returns> public override async Task ReconnectAsync( LockInfoAuthTdo authInfo, TimeSpan connectTimeout) => await ReconnectAsync( authInfo, connectTimeout, () => new LockItPolling(Device, Adapter, Cipher)); /// <summary> Connects to device. </summary> /// <param name="authInfo">Info required to connect.</param> /// <param name="device">Device with must be connected.</param> /// <returns>True if connecting succeeded, false if not.</returns> public static async Task<LockItBase> Authenticate( IDevice device, LockInfoAuthTdo authInfo, IAdapter adapter, ICipher cipher) => await Authenticate( device, authInfo, adapter, cipher, () => new LockItPolling(device, adapter, cipher)); /// <summary> Opens lock. </summary> /// <returns> Locking state.</returns> public async override Task<LockitLockingState?> 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<LockItPolling>().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<LockItPolling>().Information("No need to open lock. Lock is already open."); return await Task.FromResult((LockitLockingState?)null); } Log.ForContext<LockItPolling>().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<LockItPolling>().Information($"Opening lock failed."); return await Task.FromResult(info.State.Value); } info = await GetLockStateAsync(); if (info?.State == null) { // Device not reachable. Log.ForContext<LockItPolling>().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<LockItPolling>().Information($"Current lock state is {info?.State.Value}."); } if (info == null) { return null; } switch (info.State.Value) { case LockitLockingState.Open: return info.State.Value; case LockitLockingState.CouldntOpenBoldBlocked: // Expected error. ILockIt count not be opened (Spoke blocks lock, ....) throw new CouldntOpenBoldIsBlockedException(); case LockitLockingState.Unknown: // Expected error. ILockIt count not be opened (Spoke has blocked/ blocks lock, ....) throw new CouldntOpenBoldWasBlockedException(); 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 sould never occure. Lock refuses to open but connection is ok. throw new CouldntOpenInconsistentStateExecption(info.State.Value.GetLockingState()); } } /// <summary> Close the lock.</summary> /// <returns>Locking state.</returns> public async override Task<LockitLockingState?> 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<LockItPolling>().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<LockItPolling>().Error("No need to close lock. Lock is already closed."); return await Task.FromResult(info.State.Value); } Log.ForContext<LockItPolling>().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<LockItPolling>().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<LockItPolling>().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<LockItPolling>().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()); } } } }