Version 3.0.381

This commit is contained in:
Anja 2024-04-09 12:53:23 +02:00
parent f963c0a219
commit 3a363acf3a
1525 changed files with 60589 additions and 125098 deletions

View file

@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS;
using ShareeBike.Model.State;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class BikeInfo : BC.BikeInfo, IBikeInfo
{
/// <summary>
/// Constructs a bike info object for a available bike.
/// </summary>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="currentStationId">Id of station where bike is located.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
int lockId,
Guid lockGuid,
string currentStationId,
Uri operatorUri = null,
RentalDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(),
bike != null
? new Bike(
bike.Id,
LockModel.ILockIt /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(nameof(bike)),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid }.Build();
}
/// <summary>
/// Constructs a bike info object for a requested bike.
/// </summary>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="dateTimeProvider">Provider for current date time to calculate remaining time on demand for state of type reserved.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="requestedAt">Date time when bike was requested</param>
/// <param name="mailAddress">Mail address of user which requested bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="dateTimeProvider">Date time provider to calculate remaining time.</param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
int lockId,
Guid lockGuid,
byte[] userKey,
byte[] adminKey,
byte[] seed,
DateTime requestedAt,
string mailAddress,
string currentStationId,
Uri operatorUri,
RentalDescription tariffDescription,
Func<DateTime> dateTimeProvider,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(
dateTimeProvider,
requestedAt,
tariffDescription?.MaxReservationTimeSpan ?? StateRequestedInfo.UNKNOWNMAXRESERVATIONTIMESPAN,
mailAddress,
"" ), // BC code
bike != null
? new Bike(
bike.Id,
LockModel.ILockIt /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(nameof(bike)),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build();
}
/// <summary>
/// Constructs a bike info object for a booked bike.
/// </summary>
/// <param name="bike">Unique id of bike.</param>
/// <param name="dataSource">Specified the source of the data.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="bookedAt">Date time when bike was booked</param>
/// <param name="mailAddress">Mail address of user which booked bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="wheelType"></param>
public BikeInfo(
Bike bike,
DriveMutable drive,
DataSource dataSource,
int lockId,
Guid lockGuid,
byte[] userKey,
byte[] adminKey,
byte[] seed,
DateTime bookedAt,
string mailAddress,
string currentStationId,
Uri operatorUri,
RentalDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null) : base(
new StateInfo(
bookedAt,
mailAddress,
""),
bike != null
? new Bike(
bike.Id,
LockModel.ILockIt /* Ensure consistent lock model value */,
bike.WheelType,
bike.TypeOfBike,
bike.AaRideType,
bike.Description)
: throw new ArgumentNullException(),
drive,
dataSource,
isDemo,
group,
currentStationId,
operatorUri,
tariffDescription)
{
LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build();
}
public BikeInfo(BC.BikeInfo bikeInfo, LockInfo lockInfo) : base(
bikeInfo ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source bike info must not be null."))
{
LockInfo = lockInfo
?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source lock object must not be null.");
}
public LockInfo LockInfo { get; private set; }
}
}

View file

@ -0,0 +1,240 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Request;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.ViewModel;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class BikeInfoMutable : BC.BikeInfoMutable, IBikeInfoMutable
{
/// <summary> Provides a connector object.</summary>
private Func<bool, IConnector> ConnectorFactory { get; }
/// <summary> Provides geolocation information.</summary>
private IGeolocationService GeolocationService { get; }
/// <summary> Provides geolocation for end rental.</summary>
private LocationDto EndRentalLocation { get; }
/// <summary> Provides a connector object.</summary>
private ILocksService LockService { get; }
/// <summary> Delegate to retrieve connected state. </summary>
private Func<bool> IsConnectedDelegate { get; }
/// <summary>Object to manage update of view model objects from Copri.</summary>
protected Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Constructs a bike object from source. </summary>
/// <param name="viewUpdateManager">Manages update of view model objects from Copri.</param>
public BikeInfoMutable(
IGeolocationService geolocation,
ILocksService lockService,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
BikeInfo bike,
string stationName) : base(
bike != null
? bike.Bike
: throw new ArgumentNullException(nameof(bike)),
bike.Drive,
bike.DataSource,
bike.IsDemo,
bike.Group,
bike.StationId,
stationName,
bike.OperatorUri,
bike.TariffDescription,
() => DateTime.Now,
bike.State)
{
LockInfo = new LockInfoMutable(
bike.LockInfo.Id,
bike.LockInfo.Guid,
bike.LockInfo.UserKey,
bike.LockInfo.AdminKey,
bike.LockInfo.Seed,
bike.LockInfo.State);
GeolocationService = geolocation
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No geolocation object available.");
LockService = lockService
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No lock service object available.");
IsConnectedDelegate = isConnectedDelegate
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No is connected delegate available.");
ConnectorFactory = connectorFactory
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No connector available.");
ViewUpdateManager = viewUpdateManager
?? throw new ArgumentException($"Can not instantiate {nameof(BikeInfoMutable)}- object. No update manger available.");
}
public LockInfoMutable LockInfo { get; }
ILockInfoMutable IBikeInfoMutable.LockInfo => LockInfo;
/// <summary>
/// Connects the lock.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
public async Task ConnectAsync(
IConnectAndGetStateCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
LockService,
listener);
}
/// <summary>
/// Opens the lock and updates copri.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
/// <param name="stopPollingTask">Task which stops polling.</param>
public async Task OpenLockAsync(
IOpenCommandListener listener,
Task stopPollingTask)
{
await InvokeAsync<BikeInfoMutable>(
this,
LockService,
IsConnectedDelegate,
ConnectorFactory,
listener,
stopPollingTask);
}
/// <summary>
/// Closes the lock and updates copri.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
/// <param name="stopPollingTask">Task which stops polling.</param>
public async Task CloseLockAsync(
ICloseCommandListener listener,
Task stopPollingTask)
{
await InvokeAsync<BikeInfoMutable>(
this,
GeolocationService,
LockService,
IsConnectedDelegate,
ConnectorFactory,
listener,
stopPollingTask);
}
/// <summary>
/// Disconnects the lock.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
public async Task DisconnectAsync(
IDisconnectCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
LockService,
listener);
}
/// <summary>
/// Reserves the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task ReserveBikeAsync(
IStartReservationCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Cancels the reservation.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task CancelReservationAsync(
ICancelReservationCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Authenticates the user.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task AuthAsync(
IAuthCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Rents the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task RentBikeAsync(
IStartRentalCommandListener listener)
{
await InvokeAsync<BikeInfoMutable>(
this,
IsConnectedDelegate,
ConnectorFactory,
listener);
}
/// <summary>
/// Returns the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
public async Task<BookingFinishedModel> ReturnBikeAsync(
IEndRentalCommandListener listener)
{
var bookingFinished = await InvokeAsync<BikeInfoMutable>(
this,
GeolocationService,
LockService,
() => DateTime.Now,
IsConnectedDelegate,
ConnectorFactory,
listener);
return bookingFinished;
}
public override string ToString()
{
return $"Id={Id}{(TypeOfBike != null ? $";type={TypeOfBike}" : "")};state={State.ToString()};Lock id={LockInfo.Id}";
}
}
}

View file

@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Exception;
using ShareeBike.Repository.Request;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.BluetoothLock.Exception;
using ShareeBike.Services.BluetoothLock.Tdo;
using ShareeBike.Services.Geolocation;
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class CloseCommand
{
/// <summary>
/// Possible steps of closing a lock.
/// </summary>
public enum Step
{
StartStopingPolling,
StartingQueryingLocation,
ClosingLock,
WaitStopPollingQueryLocation,
/// <summary>
/// Notifies back end about modified lock state.
/// </summary>
UpdateLockingState,
QueryLocationTerminated
}
/// <summary>
/// Possible states of closing a lock.
/// </summary>
public enum State
{
OutOfReachError,
CouldntCloseMovingError,
CouldntCloseBoltBlockedError,
GeneralCloseError,
StartGeolocationException,
WaitGeolocationException,
WebConnectFailed,
ResponseIsInvalid,
BackendUpdateFailed
}
/// <summary>
/// Interface to notify view model about steps/ state changes of closing process.
/// </summary>
public interface ICloseCommandListener
{
/// <summary>
/// Reports current step.
/// </summary>
/// <param name="currentStep">Current step to report.</param>
void ReportStep(Step currentStep);
/// <summary>
/// Reports current state.
/// </summary>
/// <param name="currentState">Current state to report.</param>
/// <param name="message">Message describing the current state.</param>
/// <returns></returns>
Task ReportStateAsync(State currentState, string message);
}
/// <summary>
/// Closes the lock and updates copri.
/// </summary>
/// <param name="listener">Interface to notify view model about steps/ state changes of closing process.</param>
/// <param name="stopPolling">Task which stops polling.</param>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
IGeolocationService geolocation,
ILocksService lockService,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
ICloseCommandListener listener,
Task stopPollingTask)
{
// Invokes member to notify about step being started.
void InvokeCurrentStep(Step step)
{
if (listener == null)
return;
try
{
listener.ReportStep(step);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
}
}
// Invokes member to notify about state change.
async Task InvokeCurrentStateAsync(State state, string message)
{
if (listener == null)
return;
try
{
await listener.ReportStateAsync(state, message);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
// Wait for geolocation and polling task to stop (on finished or on canceled).
async Task<IGeolocation> WaitForPendingTasks(Task<IGeolocation> locationTask)
{
// Step: Wait until getting geolocation has completed.
InvokeCurrentStep(Step.WaitStopPollingQueryLocation);
Log.ForContext<T>().Information($"Waiting on steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} to finish...");
try
{
await Task.WhenAll(new List<Task> { locationTask, stopPollingTask ?? Task.CompletedTask });
}
catch (Exception exception)
{
// No location information available.
Log.ForContext<T>().Information("Canceling query location/ wait for polling task to finish failed. {@exception}", exception);
await InvokeCurrentStateAsync(State.WaitGeolocationException, exception.Message);
InvokeCurrentStep(Step.QueryLocationTerminated);
return null;
}
Log.ForContext<T>().Information($"Steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} finished.");
InvokeCurrentStep(Step.QueryLocationTerminated);
return locationTask.Result;
}
// Updates locking state
async Task UpdateLockingState(IGeolocation location, DateTime timeStamp)
{
// Step: Update backend.
InvokeCurrentStep(Step.UpdateLockingState);
try
{
await connectorFactory(true).Command.UpdateLockingStateAsync(
bike,
location != null
? new LocationDto.Builder
{
Latitude = location.Latitude,
Longitude = location.Longitude,
Accuracy = location.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(location.Timestamp.DateTime),
}.Build()
: null);
Log.ForContext<T>().Information("Backend updated for bike {bikeId} successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Information("Updating backend for bike {bikeId} failed.", bike.Id);
//BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<T>().Debug("Copri server not reachable.");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
return;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<T>().Debug("Message: {Message} Details: {Details}", copriException.Message, copriException.Response);
await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message);
return;
}
else
{
Log.ForContext<T>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", bike.Id, exception);
await InvokeCurrentStateAsync(State.BackendUpdateFailed, exception.Message);
return;
}
}
}
//// Start Action
//// Step: Start query geolocation data.
Log.ForContext<T>().Debug($"Starting step {Step.StartingQueryingLocation}...");
InvokeCurrentStep(Step.StartingQueryingLocation);
var ctsLocation = new CancellationTokenSource();
Task<IGeolocation> currentLocationTask = Task.FromResult<IGeolocation>(null);
var timeStampNow = DateTime.Now;
try
{
currentLocationTask = geolocation.GetAsync(ctsLocation.Token, timeStampNow);
Log.ForContext<T>().Information("Starting query location successful.");
}
catch (Exception exception)
{
// No location information available.
Log.ForContext<T>().Information("Starting query location failed. {@exception}", exception);
await InvokeCurrentStateAsync(State.StartGeolocationException, exception.Message);
}
//// Step: Close lock.
IGeolocation currentLocation;
Log.ForContext<T>().Information($"Starting step {Step.ClosingLock}...");
InvokeCurrentStep(Step.ClosingLock);
LockitLockingState? lockingState;
try
{
lockingState = await lockService[bike.LockInfo.Id].CloseAsync();
Log.ForContext<T>().Information("Lock of bike {bikeId} closed successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be closed.", bike.Id);
if (exception is OutOfReachException)
{
Log.ForContext<T>().Debug("Lock is out of reach");
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else if (exception is CouldntCloseMovingException)
{
Log.ForContext<T>().Debug("Lock is moving.");
await InvokeCurrentStateAsync(State.CouldntCloseMovingError, exception.Message);
}
else if (exception is CouldntCloseBoltBlockedException)
{
Log.ForContext<T>().Debug("Bold is blocked.}");
await InvokeCurrentStateAsync(State.CouldntCloseBoltBlockedError, exception.Message);
}
else
{
Log.ForContext<T>().Debug("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralCloseError, exception.Message);
}
// Signal cts to cancel getting geolocation.
ctsLocation.Cancel();
// Wait until getting geolocation and stop polling has completed.
currentLocation = await WaitForPendingTasks(currentLocationTask);
// Update current state from exception
bike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.UnknownDisconnected;
if (!isConnectedDelegate())
{
// Lock state can not be updated because there is no connected.
throw;
}
if (exception is OutOfReachException)
{
// Locking state can not be updated because lock is not connected.
throw;
}
// Update backend.
// Do this even if current lock state is open (lock state must not necessarily be open before try to open, i.e. something undefined between open and closed).
await UpdateLockingState(currentLocation, timeStampNow);
throw;
}
//// Step: Wait until getting geolocation and stop polling has completed.
currentLocation = await WaitForPendingTasks(currentLocationTask);
bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected;
// Keep geolocation where closing action occurred.
bike.LockInfo.Location = currentLocation;
if (!isConnectedDelegate())
{
return;
}
//// Step: Update backend.
await UpdateLockingState(currentLocation, timeStampNow);
}
}
}

View file

@ -0,0 +1,183 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.BluetoothLock.Exception;
using ShareeBike.Services.BluetoothLock.Tdo;
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
using ShareeBike.ViewModel.Bikes;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class ConnectAndGetStateCommand
{
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum Step
{
ConnectLock,
GetLockingState,
}
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum State
{
BluetoothOff,
NoLocationPermission,
LocationServicesOff,
OutOfReachError,
GeneralConnectLockError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of connecting or disconnecting lock process.
/// </summary>
public interface IConnectAndGetStateCommandListener
{
/// <summary>
/// Reports current step.
/// </summary>
/// <param name="currentStep">Current step to report.</param>
void ReportStep(Step currentStep);
/// <summary>
/// Reports current state.
/// </summary>
/// <param name="currentState">Current state to report.</param>
/// <param name="message">Message describing the current state.</param>
/// <returns></returns>
Task ReportStateAsync(State currentState, string message);
}
/// <summary>
/// Connect or disconnect lock.
/// </summary>
/// <param name="listener"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
ILocksService lockService,
IConnectAndGetStateCommandListener listener = null)
{
// Invokes member to notify about step being started.
void InvokeCurrentStep(Step step)
{
if (listener == null)
return;
try
{
listener.ReportStep(step);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
}
}
// Invokes member to notify about state change.
async Task InvokeCurrentStateAsync(State state, string message)
{
if (listener == null)
return;
try
{
await listener.ReportStateAsync(state, message);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
// Connect lock
InvokeCurrentStep(Step.ConnectLock);
LockInfoTdo result = null;
var continueConnect = true;
var retryCount = 1;
while (continueConnect && result == null)
{
try
{
result = await lockService.ConnectAsync(
new LockInfoAuthTdo.Builder { Id = bike.LockInfo.Id, Guid = bike.LockInfo.Guid, K_seed = bike.LockInfo.Seed, K_u = bike.LockInfo.UserKey }.Build(),
lockService.TimeOut.GetSingleConnect(retryCount));
Log.ForContext<T>().Information("Connected to lock of bike {bikeId} successfully. Value is {lockState}.", bike.Id, bike.LockInfo.State);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Connection to lock of bike {bikeId} failed. ", bike.Id);
if (exception is ConnectBluetoothNotOnException)
{
continueConnect = false;
Log.ForContext<T>().Error("Bluetooth not on.");
await InvokeCurrentStateAsync(State.BluetoothOff, exception.Message);
}
else if (exception is ConnectLocationPermissionMissingException)
{
continueConnect = false;
Log.ForContext<T>().Error("Location permission missing.");
await InvokeCurrentStateAsync(State.NoLocationPermission, exception.Message);
}
else if (exception is ConnectLocationOffException)
{
continueConnect = false;
Log.ForContext<T>().Error("Location services not on.");
await InvokeCurrentStateAsync(State.LocationServicesOff, exception.Message);
}
else if (exception is OutOfReachException)
{
continueConnect = false;
Log.ForContext<T>().Error("Lock is out of reach.");
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else
{
Log.ForContext<T>().Error("{@exception}", exception);
string message;
if (retryCount < 2)
{
message = AppResources.ErrorConnectLock;
}
else if (retryCount < 3)
{
message = AppResources.ErrorConnectLockEscalationLevel1;
}
else
{
message = AppResources.ErrorConnectLockEscalationLevel2;
continueConnect = false;
}
await InvokeCurrentStateAsync(State.GeneralConnectLockError, message);
}
if (continueConnect)
{
retryCount++;
continue;
}
throw;
}
}
// Get locking state
InvokeCurrentStep(Step.GetLockingState);
bike.LockInfo.State = result?.State?.GetLockingState() ?? LockingState.UnknownDisconnected;
if (bike.LockInfo.State == LockingState.UnknownDisconnected)
{
// Do not display any messages here, because search is implicit.
Log.ForContext<T>().Error("Lock is still not connected.");
}
bike.LockInfo.Guid = result?.Guid ?? new Guid();
}
}
}

View file

@ -0,0 +1,106 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Services.BluetoothLock;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class DisconnectCommand
{
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum Step
{
DisconnectLock,
}
/// <summary>
/// Possible steps of connecting or disconnecting a lock.
/// </summary>
public enum State
{
GeneralDisconnectError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of connecting or disconnecting lock process.
/// </summary>
public interface IDisconnectCommandListener
{
/// <summary>
/// Reports current step.
/// </summary>
/// <param name="currentStep">Current step to report.</param>
void ReportStep(Step currentStep);
/// <summary>
/// Reports current state.
/// </summary>
/// <param name="currentState">Current state to report.</param>
/// <param name="message">Message describing the current state.</param>
/// <returns></returns>
Task ReportStateAsync(State currentState, string message);
}
/// <summary>
/// Connect or disconnect lock.
/// </summary>
/// <param name="listener"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
ILocksService lockService,
IDisconnectCommandListener listener = null)
{
// Invokes member to notify about step being started.
void InvokeCurrentStep(Step step)
{
if (listener == null)
return;
try
{
listener.ReportStep(step);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
}
}
// Invokes member to notify about state change.
async Task InvokeCurrentStateAsync(State state, string message)
{
if (listener == null)
return;
try
{
await listener.ReportStateAsync(state, message);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
// Disconnect lock
InvokeCurrentStep(Step.DisconnectLock);
if (bike.LockInfo.State != LockingState.UnknownDisconnected)
{
try
{
bike.LockInfo.State = await lockService.DisconnectAsync(bike.LockInfo.Id, bike.LockInfo.Guid);
Log.ForContext<T>().Information("Lock from bike {bikeId} disconnected successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock from bike {bikeId} can not be disconnected. {@exception}", bike.Id, exception);
await InvokeCurrentStateAsync(State.GeneralDisconnectError, exception.Message);
}
}
}
}
}

View file

@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Connector;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.BluetoothLock.Exception;
using ShareeBike.Services.BluetoothLock.Tdo;
using ShareeBike.Services.Geolocation;
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command
{
public static class OpenCommand
{
/// <summary>
/// Possible steps of opening a lock.
/// </summary>
public enum Step
{
OpeningLock,
WaitStopPolling,
GetLockInfos,
UpdateLockingState,
}
/// <summary>
/// Possible states of opening a lock.
/// </summary>
public enum State
{
StopPollingFailed,
OutOfReachError,
CouldntOpenBoldStatusIsUnknownError,
CouldntOpenBoldIsBlockedError,
CouldntOpenInconsistentStateError,
GeneralOpenError,
WebConnectFailed,
ResponseIsInvalid,
BackendUpdateFailed
}
/// <summary>
/// Interface to notify view model about steps/ state changes of opening process.
/// </summary>
public interface IOpenCommandListener
{
/// <summary>
/// Reports current step.
/// </summary>
/// <param name="currentStep">Current step to report.</param>
void ReportStep(Step currentStep);
/// <summary>
/// Reports current state.
/// </summary>
/// <param name="currentState">Current state to report.</param>
/// <param name="message">Message describing the current state.</param>
/// <returns></returns>
Task ReportStateAsync(State currentState, string message);
}
/// <summary>
/// Opens the lock and updates copri.
/// </summary>
/// <param name="listener">Interface to notify view model about steps/ state changes of opening process.</param>
/// <param name="stopPolling">Task which stops polling.</param>
public static async Task InvokeAsync<T>(
IBikeInfoMutable bike,
ILocksService lockService,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IOpenCommandListener listener,
Task stopPollingTask)
{
// Invokes member to notify about step being started.
void InvokeCurrentStep(Step step)
{
if (listener == null)
return;
try
{
listener.ReportStep(step);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
}
}
// Invokes member to notify about state change.
async Task InvokeCurrentStateAsync(State state, string message)
{
if (listener == null)
return;
try
{
await listener.ReportStateAsync(state, message);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
// Wait polling task to stop (on finished or on canceled).
async Task WaitForPendingTasks()
{
// Step: Wait until getting geolocation has completed.
InvokeCurrentStep(Step.WaitStopPolling);
Log.ForContext<T>().Information($"Waiting on stop polling to finish...");
try
{
await Task.WhenAll(new List<Task> { stopPollingTask ?? Task.CompletedTask });
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Wait for polling task to finish failed. {@exception}", exception);
await InvokeCurrentStateAsync(State.StopPollingFailed, exception.Message);
return;
}
Log.ForContext<T>().Information($"Stop polling finished.");
return;
}
// Get lock infos
async Task GetLockInfos()
{
Log.ForContext<T>().Debug($"Starting step {Step.GetLockInfos}...");
InvokeCurrentStep(Step.GetLockInfos);
// get current charging level
try
{
bike.LockInfo.BatteryPercentage = await lockService[bike.LockInfo.Id].GetBatteryPercentageAsync();
Log.ForContext<T>().Information("Lock infos of bike {bikeId} read successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock infos could not be read.", bike.Id);
if (exception is OutOfReachException)
{
Log.ForContext<T>().Debug("Lock is out of reach");
}
else
{
Log.ForContext<T>().Debug("{@exception}", exception);
}
}
// get version infos.
var versionTdo = lockService[bike.LockInfo.Id].VersionInfo;
if (versionTdo != null)
{
bike.LockInfo.VersionInfo = new VersionInfo.Builder
{
FirmwareVersion = versionTdo.FirmwareVersion,
HardwareVersion = versionTdo.HardwareVersion,
LockVersion = versionTdo.LockVersion,
}.Build();
}
}
// Updates locking state
async Task UpdateLockingState()
{
// Step: Update backend.
InvokeCurrentStep(Step.UpdateLockingState);
try
{
await connectorFactory(true).Command.UpdateLockingStateAsync(bike);
Log.ForContext<T>().Information("Backend updated for bike {bikeId} successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Information("Updating backend for bike {bikeId} failed.", bike.Id);
//BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<T>().Debug("Copri server not reachable.");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
return;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<T>().Debug("Message: {Message} Details: {Details}", copriException.Message, copriException.Response);
await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message);
return;
}
else
{
Log.ForContext<T>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", bike.Id, exception);
await InvokeCurrentStateAsync(State.BackendUpdateFailed, exception.Message);
return;
}
}
}
//// Start Action
//// Step: Open lock.
Log.ForContext<T>().Information($"Starting step {Step.OpeningLock}...");
InvokeCurrentStep(Step.OpeningLock);
LockitLockingState? lockingState;
try
{
lockingState = await lockService[bike.LockInfo.Id].OpenAsync();
Log.ForContext<T>().Information("Lock of bike {bikeId} opened successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be opened.", bike.Id);
if (exception is OutOfReachException)
{
Log.ForContext<T>().Debug("Lock is out of reach");
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
}
else if (exception is CouldntOpenBoldStatusIsUnknownException)
{
Log.ForContext<T>().Debug("Lock status is unknown.");
await InvokeCurrentStateAsync(State.CouldntOpenBoldStatusIsUnknownError, exception.Message);
}
else if (exception is CouldntOpenBoldIsBlockedException)
{
Log.ForContext<T>().Debug("Bold is blocked.}");
await InvokeCurrentStateAsync(State.CouldntOpenBoldIsBlockedError, exception.Message);
}
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
&& inconsistentState.State == LockingState.Closed)
{
Log.ForContext<T>().Debug("Lock reports that it is still closed.}");
await InvokeCurrentStateAsync(State.CouldntOpenInconsistentStateError, exception.Message);
}
else
{
Log.ForContext<T>().Debug("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralOpenError, exception.Message);
}
// Update current state from exception
bike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.UnknownDisconnected;
if (!isConnectedDelegate())
{
// Lock state can not be updated because there is no connected.
throw;
}
if (exception is OutOfReachException)
{
// Locking state can not be updated because lock is not connected.
throw;
}
//// Step: Get Lock Battery and Version info
await GetLockInfos();
//// Step: Wait for stop polling finished
await WaitForPendingTasks();
//// Step: Update backend.
//Do this even if current lock state is closed (lock state must not necessarily be closed before try to close, i.e. something undefined between open and opened).
await UpdateLockingState();
throw;
}
bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected;
if (!isConnectedDelegate())
{
return;
}
//// Step: Get Lock Battery and Version info
await GetLockInfos();
//// Step: Wait for stop polling finished
await WaitForPendingTasks();
//// Step: Update backend.
await UpdateLockingState();
}
}
}

View file

@ -0,0 +1,6 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface IBikeInfo : BC.IBikeInfo
{
}
}

View file

@ -0,0 +1,76 @@
using System.Threading.Tasks;
using ShareeBike.Repository.Request;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface IBikeInfoMutable : BC.IBikeInfoMutable
{
ILockInfoMutable LockInfo { get; }
/// <summary>
/// Connects the lock.
/// </summary>
/// <param name="listener">View model to process connect notifications.</param>
Task ConnectAsync(IConnectAndGetStateCommandListener listener);
/// <summary>
/// Disconnects the lock.
/// </summary>
/// <param name="listener">View model to process disconnect notifications.</param>
Task DisconnectAsync(IDisconnectCommandListener listener);
/// <summary>
/// Opens the lock.
/// </summary>
/// <param name="listener">View model to process opening notifications.</param>
/// <param name="stopPollingTask">Task which stops polling to be awaited before updating bike object and copri communication.</param>
Task OpenLockAsync(IOpenCommandListener listener, Task stopPollingTask);
/// <summary>
/// Closes the lock.
/// </summary>
/// <param name="listener">View model to process closing notifications.</param>
/// <param name="stopPollingTask">Task which stops polling to be awaited before updating bike object and copri communication.</param>
Task CloseLockAsync(ICloseCommandListener listener, Task stopPollingTask);
/// <summary>
/// Reserves the bike.
/// </summary>
/// <param name="listener">View model to process reservation notifications.</param>
Task ReserveBikeAsync(IStartReservationCommandListener listener);
/// <summary>
/// Cancels the reservation.
/// </summary>
/// <param name="listener">View model to process reservation notifications.</param>
Task CancelReservationAsync(ICancelReservationCommandListener listener);
/// <summary>
/// Authenticates the user.
/// </summary>
/// <param name="listener">View model to process reservation notifications.</param>
Task AuthAsync(IAuthCommandListener listener);
/// <summary>
/// Rents the bike.
/// </summary>
/// <param name="listener">View model to process renting notifications.</param>
Task RentBikeAsync(IStartRentalCommandListener listener);
/// <summary>
/// Returns the bike.
/// </summary>
/// <param name="listener">View model to process notifications.</param>
/// <returns></returns>
Task<BookingFinishedModel> ReturnBikeAsync(IEndRentalCommandListener listener);
}
}

View file

@ -0,0 +1,37 @@
using System;
using ShareeBike.Services.Geolocation;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface ILockInfoMutable
{
/// <summary> Identification number of bluetooth lock.</summary>
int Id { get; }
/// <summary> Gets the user key.</summary>
byte[] UserKey { get; }
LockingState State { get; set; }
/// <summary> Holds the percentage of lock battery.</summary>
double BatteryPercentage { get; set; }
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
Guid Guid { get; set; }
byte[] Seed { get; }
/// <summary> Timestamp of the last locking state change.</summary>
DateTime? LastLockingStateChange { get; }
/// <summary>
/// Gets or sets the current location of the bike, null if location is unknown.
/// </summary>
IGeolocation Location { get; set; }
/// <summary>
/// Gets the version info of the locks.
/// </summary>
IVersionInfo VersionInfo { get; set; }
}
}

View file

@ -0,0 +1,20 @@
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public interface IVersionInfo
{
/// <summary>
/// Holds the firmware version of the lock.
/// </summary>
int FirmwareVersion { get; }
/// <summary>
/// Holds the hardware version (revision) of the lock.
/// </summary>
int HardwareVersion { get; }
/// <summary>
/// Holds lock version (2 classic, 3 plus, 4 GPS).
/// </summary>
int LockVersion { get; }
}
}

View file

@ -0,0 +1,89 @@
using System;
using ShareeBike.Services.Geolocation;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class LockInfoMutable : ILockInfoMutable
{
/// <summary> Lock info object. </summary>
private LockInfo LockInfo { get; set; }
/// <summary>
/// Delegate to create time stamp.
/// </summary>
private Func<DateTime> _nowDelegate;
/// <summary> Constructs a bluetooth lock info object. </summary>
/// <param name="id">Id of lock must always been known when constructing an lock info object.</param>
/// <param name="nowDelegate">Delegate to create time stamp if null DateTime.Now is used.</param>
public LockInfoMutable(
int id,
Guid guid,
byte[] userKey,
byte[] adminKey,
byte[] seed,
LockingState state,
Func<DateTime> nowDelegate = null)
{
_nowDelegate = nowDelegate ?? (() => DateTime.Now);
LockInfo = new LockInfo.Builder() { Id = id, Guid = guid, UserKey = userKey, AdminKey = adminKey, Seed = seed, State = state }.Build();
}
public int Id => LockInfo.Id;
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
public Guid Guid
{
get => LockInfo.Guid;
set => LockInfo = new LockInfo.Builder(LockInfo) { Guid = value }.Build();
}
public byte[] Seed => LockInfo.Seed;
public byte[] UserKey => LockInfo.UserKey;
public byte[] AdminKey => LockInfo.AdminKey;
/// <summary>
/// Gets or sets the locking state.
/// </summary>
public LockingState State
{
get => LockInfo.State;
set
{
if (LockInfo.State == value)
{
// State does not change, nothing to do.
return;
}
Location = null; // Invalidate location.
LastLockingStateChange = _nowDelegate(); // Get time stamp when state change happened.
LockInfo = new LockInfo.Builder(LockInfo) { State = value }.Build();
}
}
/// <summary> Gets the time stamp of the last locking state change.</summary>
public DateTime? LastLockingStateChange { get; private set; }
/// <summary>
/// Gets or sets the current location of the bike, null if location is unknown.
/// </summary>
public IGeolocation Location { get; set; }
/// <summary> Holds the percentage of lock battery.</summary>
public double BatteryPercentage { get; set; } = double.NaN;
/// <summary>
/// Gets the version info of the lock.
/// </summary>
public IVersionInfo VersionInfo { get; set; } = new VersionInfo.Builder().Build();
/// <summary> Loads lock info object from values. </summary>
public void Load(int id, Guid guid, byte[] seed, byte[] userKey, byte[] adminKey)
{
LockInfo = new LockInfo.Builder(LockInfo) { Id = id, Guid = guid, Seed = seed, UserKey = userKey, AdminKey = adminKey }.Build();
}
}
}

View file

@ -0,0 +1,69 @@
using Newtonsoft.Json;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock
{
public class VersionInfo : IVersionInfo
{
/// <summary>
/// Holds info about firmware- and hardware version of a lock and the type of lock (lock version).
/// </summary>
private VersionInfo() { }
/// <summary>
/// Holds the firmware version of the lock.
/// </summary>
public int FirmwareVersion { get; private set; } = 0;
/// <summary>
/// Holds the hardware version (revision) of the lock.
/// </summary>
public int HardwareVersion { get; private set; } = 0;
/// <summary>
/// Holds lock version (2 classic, 3 plus, 4 GPS).
/// </summary>
public int LockVersion { get; private set; } = 0;
public override bool Equals(object obj)
=> Equals(obj as VersionInfo);
public bool Equals(VersionInfo other)
{
if (ReferenceEquals(other, null)) return false;
if (ReferenceEquals(this, other)) return true;
if (GetType() != other.GetType()) return false;
return ToString() == other.ToString();
}
public override int GetHashCode() => ToString().GetHashCode();
public override string ToString() => JsonConvert.SerializeObject(this);
public static bool operator ==(VersionInfo lhs, VersionInfo rhs)
{
if (ReferenceEquals(lhs, null))
return ReferenceEquals(rhs, null) ? true /*null == null = true*/: false;
return lhs.Equals(rhs);
}
public static bool operator !=(VersionInfo lhs, VersionInfo rhs)
=> !(lhs == rhs);
public class Builder
{
private VersionInfo versionInfo = new VersionInfo();
public int FirmwareVersion { get => versionInfo.FirmwareVersion; set => versionInfo.FirmwareVersion = value; }
public int HardwareVersion { get => versionInfo.HardwareVersion; set => versionInfo.HardwareVersion = value; }
public int LockVersion { get => versionInfo.LockVersion; set => versionInfo.LockVersion = value; }
public VersionInfo Build()
{
return versionInfo;
}
}
}
}