using System; using System.Threading; using System.Threading.Tasks; using Plugin.BLE.Abstractions.Contracts; using Plugin.BLE.Abstractions.EventArgs; 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 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. Platform must not be unknown and 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 open lock. Current locking 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 when opening lock failed.{Exception}", ex); return await Task.FromResult((LockitLockingState?)null); } try { 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(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.Open: Log.ForContext().Information($"Lock was opened successfully."); return lockingState.Value; 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.Unknown // - LockitLockingState.CouldntOpenBoltBlocked // Internal error which should never occur. Lock refuses to open but connection is ok. Log.ForContext().Debug($"Opening lock failed. Unexpected lock state {lockingState.Value.GetLockingState()} detected."); 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. Platform must not be unknown and 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 close lock. Current state is {lockingState}, 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(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.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: Log.ForContext().Information($"Lock was closed successfully."); return lockingState; case LockitLockingState.Open: // Expected error. ILockIt could not be closed. Bolt was blocked but was opened again. Log.ForContext().Debug($"Closing lock failed. Bold is blocked but was reopened again."); throw new CouldntCloseBoltBlockedException(LockingState.Open); 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($"Opening lock failed. Unexpected lock state {lockingState.Value.GetLockingState()} detected."); 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; } }; } }