mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-11-05 10:36:30 +01:00
1359 lines
50 KiB
C#
1359 lines
50 KiB
C#
// Ignore Spelling: "0000f00d-1212-efde-1523-785fef13d123"
|
||
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Plugin.BLE.Abstractions;
|
||
using Plugin.BLE.Abstractions.Contracts;
|
||
using Plugin.BLE.Abstractions.Exceptions;
|
||
using Polly;
|
||
using Polly.Retry;
|
||
using Serilog;
|
||
using ShareeBike.Model.Connector;
|
||
using ShareeBike.Model.Device;
|
||
using ShareeBike.Services.BluetoothLock.Crypto;
|
||
using ShareeBike.Services.BluetoothLock.Exception;
|
||
using ShareeBike.Services.BluetoothLock.Tdo;
|
||
using Xamarin.Essentials;
|
||
|
||
namespace ShareeBike.Services.BluetoothLock.BLE
|
||
{
|
||
/// <summary> Manages a single lock.</summary>
|
||
public abstract class LockItBase : ILockService
|
||
{
|
||
/// <summary> Length of seed in bytes.</summary>
|
||
private const int SEEDLENGTH = 16;
|
||
|
||
/// <summary> Timeout for open/ close operations.</summary>
|
||
protected const int OPEN_CLOSE_TIMEOUT_MS = 30000;
|
||
|
||
/// <summary> Timeout for get service operations.</summary>
|
||
private const int GETSERVICE_TIMEOUT_MS = 3000;
|
||
|
||
/// <summary> Timeout for read operations.</summary>
|
||
private const int READ_TIMEOUT_MS = 3000;
|
||
|
||
/// <summary> Return code which indicates success.</summary>
|
||
public const int ERRORCODE_SUCCESS = 0;
|
||
|
||
protected LockItBase(IDevice device, IAdapter adapter, ICipher cipher)
|
||
{
|
||
Device = device ?? throw new ArgumentException(nameof(device));
|
||
Cipher = cipher ?? throw new ArgumentException(nameof(cipher));
|
||
Adapter = adapter ?? throw new ArgumentException(nameof(adapter));
|
||
|
||
_retryPollicy = Policy<(byte[], int)>
|
||
.Handle<CharacteristicReadException>()
|
||
.WaitAndRetryAsync(2, index => TimeSpan.FromMilliseconds(100));
|
||
|
||
GetGuid();
|
||
GetName();
|
||
|
||
if (InvalidatedSeed.ContainsKey(Guid))
|
||
{
|
||
// Lock was already connected. No need to add entry.
|
||
return;
|
||
}
|
||
|
||
InvalidatedSeed.Add(Guid, new List<string>());
|
||
}
|
||
|
||
private static readonly Dictionary<Guid, List<string>> InvalidatedSeed = new Dictionary<Guid, List<string>>();
|
||
|
||
/// <summary> Count of write- actions to activate lock characteristic..</summary>
|
||
protected int ActivateLockWriteCounter { get; private set; }
|
||
|
||
protected ICipher Cipher { get; }
|
||
|
||
protected IAdapter Adapter { get; }
|
||
|
||
protected IDevice Device { get; }
|
||
|
||
private IService LockControl { get; set; }
|
||
|
||
private IService BatteryControl { get; set; }
|
||
|
||
private ICharacteristic ActivateLockCharacteristic { get; set; }
|
||
|
||
private ICharacteristic AlarmCharacteristic { get; set; }
|
||
|
||
private ICharacteristic AlarmSettingsCharacteristic { get; set; }
|
||
|
||
private ICharacteristic AuthCharacteristic { get; set; }
|
||
|
||
private ICharacteristic StateCharacteristic { get; set; }
|
||
|
||
private ICharacteristic SoundCharacteristic { get; set; }
|
||
|
||
private ICharacteristic BatteryCharacteristic { get; set; }
|
||
|
||
private ICharacteristic FirmwareVersionCharacteristic { get; set; }
|
||
|
||
private readonly AsyncRetryPolicy<(byte[], int)> _retryPollicy;
|
||
|
||
/// <summary> Gets the lock control service.</summary>
|
||
private async Task<IService> GetLockControlService()
|
||
{
|
||
if (LockControl != null) return LockControl;
|
||
|
||
LockControl = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get lock control service.");
|
||
|
||
var cts = new CancellationTokenSource();
|
||
cts.CancelAfter(GETSERVICE_TIMEOUT_MS);
|
||
try
|
||
{
|
||
LockControl = await Device.GetServiceAsync(new Guid("0000f00d-1212-efde-1523-785fef13d123"), cts.Token);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting lock control service failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get lock control service. {exception.Message}", exception);
|
||
}
|
||
finally
|
||
{
|
||
cts.Dispose();
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Getting lock control service succeeded.");
|
||
return LockControl;
|
||
}
|
||
|
||
/// <summary> Gets battery service.</summary>
|
||
private async Task<IService> GetBatteryService()
|
||
{
|
||
if (BatteryControl != null) return BatteryControl;
|
||
|
||
BatteryControl = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get battery control service.");
|
||
|
||
var cts = new CancellationTokenSource();
|
||
cts.CancelAfter(GETSERVICE_TIMEOUT_MS);
|
||
try
|
||
{
|
||
BatteryControl = await Device.GetServiceAsync(new Guid("0000180F-0000-1000-8000-00805f9b34fb"), cts.Token);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting battery service failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get battery service. {exception.Message}", exception);
|
||
}
|
||
finally
|
||
{
|
||
cts.Dispose();
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Getting battery service succeeded.");
|
||
return BatteryControl;
|
||
}
|
||
|
||
/// <summary> Gets the state characteristic.</summary>
|
||
private async Task<ICharacteristic> GetActivateLockCharacteristicAsync()
|
||
{
|
||
if (ActivateLockCharacteristic != null) return ActivateLockCharacteristic;
|
||
|
||
ActivateLockCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get activate lock characteristic.");
|
||
|
||
try
|
||
{
|
||
ActivateLockCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting activate lock characteristic failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get activate characteristic. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Activate lock characteristic retrieved successfully.");
|
||
|
||
return ActivateLockCharacteristic;
|
||
}
|
||
|
||
/// <summary> Gets the alarm characteristic.</summary>
|
||
private async Task<ICharacteristic> GetAlarmCharacteristicAsync()
|
||
{
|
||
if (AlarmCharacteristic != null) return AlarmCharacteristic;
|
||
|
||
AlarmCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get alarm characteristic.");
|
||
|
||
try
|
||
{
|
||
AlarmCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BFFF-1212-efde-1523-785fef13d123"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting alarm-characteristic failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get alarm characteristic. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Get alarm characteristic retrieved successfully.");
|
||
return AlarmCharacteristic;
|
||
}
|
||
|
||
/// <summary> Gets the alarm settings characteristic.</summary>
|
||
private async Task<ICharacteristic> GetAlarmSettingsCharacteristicAsync()
|
||
{
|
||
if (AlarmSettingsCharacteristic != null) return AlarmSettingsCharacteristic;
|
||
|
||
AlarmSettingsCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get alarm settings characteristic.");
|
||
|
||
try
|
||
{
|
||
AlarmSettingsCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BFFE-1212-efde-1523-785fef13d123"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting alarm settings characteristic failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get alarm settings characteristic. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Get alarm settings characteristic retrieved successfully.");
|
||
return AlarmSettingsCharacteristic;
|
||
}
|
||
|
||
/// <summary> Gets the auth characteristic.</summary>
|
||
private async Task<ICharacteristic> GetAuthCharacteristicAsync()
|
||
{
|
||
if (AuthCharacteristic != null) return AuthCharacteristic;
|
||
|
||
AuthCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get auth characteristic.");
|
||
try
|
||
{
|
||
AuthCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting auth-characteristic failed. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not get auth characteristic. {0}", exception.Message), exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Get auth characteristic retrieved successfully.");
|
||
return AuthCharacteristic;
|
||
}
|
||
|
||
/// <summary> Gets the state characteristic.</summary>
|
||
protected async Task<ICharacteristic> GetStateCharacteristicAsync()
|
||
{
|
||
if (StateCharacteristic != null) return StateCharacteristic;
|
||
|
||
StateCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get lock state characteristic.");
|
||
try
|
||
{
|
||
StateCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting state characteristic failed. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not get state characteristic. {0}", exception.Message), exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Get state characteristic retrieved successfully.");
|
||
return StateCharacteristic;
|
||
}
|
||
|
||
/// <summary> Gets the sound characteristic.</summary>
|
||
private async Task<ICharacteristic> GetSoundCharacteristicAsync()
|
||
{
|
||
if (SoundCharacteristic != null) return SoundCharacteristic;
|
||
|
||
SoundCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get sound characteristic.");
|
||
try
|
||
{
|
||
SoundCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000BAAE-1212-efde-1523-785fef13d123"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting sound characteristic failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get sound characteristic. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Get sound characteristic retrieved successfully.");
|
||
return SoundCharacteristic;
|
||
}
|
||
|
||
/// <summary> Gets the battery characteristic.</summary>
|
||
protected async Task<ICharacteristic> GetBatteryCharacteristicAsync()
|
||
{
|
||
if (BatteryCharacteristic != null) return BatteryCharacteristic;
|
||
|
||
BatteryCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get battery characteristic.");
|
||
try
|
||
{
|
||
BatteryCharacteristic = await (await GetBatteryService())?.GetCharacteristicAsync(new Guid("00002a19-0000-1000-8000-00805f9b34fb"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting battery characteristic failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get battery characteristic. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Get battery characteristic retrieved successfully.");
|
||
return BatteryCharacteristic;
|
||
}
|
||
|
||
/// <summary> Gets the versions info characteristic.</summary>
|
||
protected async Task<ICharacteristic> GetVersionsCharacteristicAsync()
|
||
{
|
||
if (FirmwareVersionCharacteristic != null) return FirmwareVersionCharacteristic ;
|
||
|
||
FirmwareVersionCharacteristic = null;
|
||
|
||
Log.ForContext<LockItBase>().Debug("Request to get versions info characteristic.");
|
||
try
|
||
{
|
||
FirmwareVersionCharacteristic = await (await GetLockControlService())?.GetCharacteristicAsync(new Guid("0000baad-1212-efde-1523-785fef13d123"));
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Getting versions info characteristic failed. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not get versions info characteristic. {0}", exception.Message), exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Get versions info characteristic retrieved successfully.");
|
||
return FirmwareVersionCharacteristic ;
|
||
}
|
||
/// <summary> Query name of lock.</summary>
|
||
private void GetName()
|
||
{
|
||
if (!string.IsNullOrEmpty(Name))
|
||
{
|
||
// Prevent valid name to be queried more than twice because Name does not change.
|
||
return;
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Query name of lock.");
|
||
try
|
||
{
|
||
Name = Device.Name ?? string.Empty;
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving bluetooth name failed . {Exception}", exception);
|
||
throw new System.Exception($"Can not get name of lock. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Lock name is {Name}.");
|
||
Id = Name.GetBluetoothLockId();
|
||
return;
|
||
}
|
||
|
||
/// <summary> Full advertisement name.</summary>
|
||
public string Name { get; private set; } = string.Empty;
|
||
|
||
/// <summary> Id part of advertisement name.</summary>
|
||
public int Id { get; private set; }
|
||
|
||
/// <summary> Query GUID of lock.</summary>
|
||
private void GetGuid()
|
||
{
|
||
if (Guid != TextToLockItTypeHelper.INVALIDLOCKGUID)
|
||
{
|
||
// Prevent valid GUID to be queried more than twice because GUID does not change.
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Query name of lock.");
|
||
try
|
||
{
|
||
Guid = Device.Id;
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving bluetooth guid failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get guid of lock. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Lock GUID is {Guid}.");
|
||
}
|
||
|
||
/// <summary> GUID.</summary>
|
||
public Guid Guid { get; private set; } = TextToLockItTypeHelper.INVALIDLOCKGUID;
|
||
|
||
private byte[] CopriKey { get; set; } = new byte[0];
|
||
|
||
/// <summary> Gets the device state.</summary>
|
||
public DeviceState? GetDeviceState()
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not get device state. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
DeviceState? state;
|
||
Log.ForContext<LockItBase>().Debug("Request to get connection state.");
|
||
try
|
||
{
|
||
state = Device?.State.GetDeviceState()
|
||
?? throw new System.Exception("Can not get bluetooth device state. State must not be null.");
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving bluetooth state failed. {Exception}", exception);
|
||
throw new System.Exception($"Can not get bluetooth state. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Connection state is {state}.");
|
||
return state;
|
||
}
|
||
|
||
/// <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 abstract Task ReconnectAsync(
|
||
LockInfoAuthTdo authInfo,
|
||
TimeSpan connectTimeout);
|
||
|
||
/// <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>
|
||
protected async Task ReconnectAsync(
|
||
LockInfoAuthTdo authInfo,
|
||
TimeSpan connectTimeout,
|
||
Func<LockItBase> factory)
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not reconnect to lock. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
// Check if key is available.
|
||
if (authInfo == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Error($"Can not authenticate. No auth info available.");
|
||
throw new AuthKeyException($"Can not authenticate. No auth info available.");
|
||
}
|
||
|
||
if (authInfo.K_seed.Length != SEEDLENGTH
|
||
|| authInfo.K_u.Length <= 0)
|
||
{
|
||
throw new AuthKeyException($"Can not authenticate. Invalid seed-/ k-user-length {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected.");
|
||
}
|
||
|
||
if (Device.State == Plugin.BLE.Abstractions.DeviceState.Connected)
|
||
{
|
||
throw new AlreadyConnectedException();
|
||
}
|
||
|
||
// Reset all references to characteristics.
|
||
LockControl = null;
|
||
ActivateLockCharacteristic = null;
|
||
AlarmCharacteristic = null;
|
||
AlarmSettingsCharacteristic = null;
|
||
AuthCharacteristic = null;
|
||
StateCharacteristic = null;
|
||
SoundCharacteristic = null;
|
||
ActivateLockWriteCounter = 0;
|
||
|
||
var cts = new CancellationTokenSource(connectTimeout);
|
||
|
||
// Connect to device and authenticate.
|
||
Log.ForContext<LockItBase>().Debug($"Request connect to device {Device.Name}. Connect state is {Device.State}.");
|
||
try
|
||
{
|
||
await Adapter.ConnectToDeviceAsync(
|
||
Device,
|
||
new ConnectParameters(forceBleTransport: true /* Force BLE transport */),
|
||
cts.Token);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
if (exception is TaskCanceledException)
|
||
{
|
||
// A timeout occurred.
|
||
throw new System.Exception($"Can not reconnect.\r\nTimeout of {connectTimeout.TotalMilliseconds} [ms] elapsed.", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Error("Can not reconnect. {Exception}", exception);
|
||
throw new System.Exception($"Can not Reconnect. {exception.Message}", exception);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Connecting to device succeeded. Starting auth sequence...");
|
||
|
||
var lockIt = await Authenticate(Device, authInfo, Adapter, Cipher, factory);
|
||
CopriKey = lockIt.CopriKey;
|
||
}
|
||
|
||
/// <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>
|
||
protected static async Task<LockItBase> Authenticate(
|
||
IDevice device,
|
||
LockInfoAuthTdo authInfo,
|
||
IAdapter adapter,
|
||
ICipher cipher,
|
||
Func<LockItBase> factory)
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not authenticate. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
if (device == null)
|
||
throw new ArgumentException(nameof(device));
|
||
|
||
if (cipher == null)
|
||
throw new ArgumentException(nameof(cipher));
|
||
|
||
if (adapter == null)
|
||
throw new ArgumentException(nameof(adapter));
|
||
|
||
// Check if connect is required.
|
||
DeviceState deviceState;
|
||
Log.ForContext<LockItBase>().Debug("Retrieving connection state is in context of auth.");
|
||
try
|
||
{
|
||
deviceState = device?.State.GetDeviceState()
|
||
?? throw new System.Exception("Can not get bluetooth device state. State must not be null.");
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Can not authenticate. Retrieving bluetooth state failed. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not authenticate. Getting bluetooth state failed. {0}", exception.Message), exception);
|
||
}
|
||
|
||
switch (deviceState)
|
||
{
|
||
case DeviceState.Disconnected:
|
||
throw new BluetoothDisconnectedException();
|
||
|
||
case DeviceState.Connected:
|
||
break;
|
||
|
||
default:
|
||
// Can not get state if device is not connected.
|
||
Log.ForContext<LockItBase>().Error($"Can not authenticate. Unexpected lock state {deviceState} detected.");
|
||
throw new System.Exception(string.Format("Can not authenticate. Unexpected bluetooth state {0} detected.", deviceState));
|
||
}
|
||
|
||
// Check if key is available.
|
||
if (authInfo == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Error($"Can not authenticate. No auth info available.");
|
||
throw new AuthKeyException($"Can not authenticate. No auth info available.");
|
||
}
|
||
|
||
if (authInfo.K_seed.Length != SEEDLENGTH
|
||
|| authInfo.K_u.Length <= 0)
|
||
{
|
||
throw new AuthKeyException($"Can not authenticate. Invalid seed-/ k-user-length {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected.");
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Connection state is {deviceState} in context of auth.");
|
||
|
||
var lockIt = factory();
|
||
|
||
if (InvalidatedSeed[lockIt.Guid].Contains(string.Join(",", authInfo.K_seed)))
|
||
{
|
||
throw new AuthKeyException($"Can not authenticate. Seed {string.Join(",", authInfo.K_seed)} was already used.");
|
||
}
|
||
|
||
InvalidatedSeed[lockIt.Guid].Add(string.Join(",", authInfo.K_seed));
|
||
|
||
// Connect to device and authenticate.
|
||
try
|
||
{
|
||
await AuthenticateAsync(
|
||
lockIt,
|
||
authInfo,
|
||
lockIt.Cipher);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Authentication failed. {Exception}", exception);
|
||
try
|
||
{
|
||
// Disconnect from device if auth did not succeed.
|
||
await lockIt.Adapter.DisconnectDeviceAsync(lockIt.Device);
|
||
}
|
||
catch (System.Exception exceptionInner)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Authentication failed. Disconnect throw an exception. {Exception}", exceptionInner);
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Error($"Auth failed for device name={lockIt.Name}, GUID={lockIt.Guid}.");
|
||
|
||
throw;
|
||
}
|
||
|
||
lockIt.CopriKey = authInfo.K_u;
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Auth succeeded for device name={lockIt.Name}, GUID={lockIt.Guid}, state={lockIt.GetDeviceState()}.");
|
||
return lockIt;
|
||
}
|
||
|
||
/// <summary> Performs an authentication.</summary>
|
||
private static async Task AuthenticateAsync(
|
||
LockItBase lockIt,
|
||
LockInfoAuthTdo lockInfo,
|
||
ICipher cipher)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug($"Request to authenticate for {lockIt.Name}.");
|
||
|
||
var authCharacteristic = await lockIt.GetAuthCharacteristicAsync();
|
||
if (authCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Getting auth-characteristic failed.");
|
||
throw new CoundntGetCharacteristicException("Authentication failed. Auth characteristic must not be null.");
|
||
}
|
||
|
||
int success;
|
||
|
||
try
|
||
{
|
||
success = await authCharacteristic.WriteAsync(lockInfo.K_seed);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing copri seed failed.{AuthCharacteristic}{CommandWritten}{Exception}", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed), exception);
|
||
throw new System.Exception(string.Format("Can not authenticate. Writing copri seed failed. {0}", exception.Message), exception);
|
||
}
|
||
if (success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing copri seed failed.{AuthCharacteristic}{CommandWritten}{ErrorCode}", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed), success);
|
||
throw new System.Exception("Can not authenticate. Writing copri seed did not succeed.");
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Copri seed written successfully.{AuthCharacteristic}{CommandWritten}.", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed));
|
||
|
||
(byte[] SeedLockEncrypted, int Success) readResult;
|
||
|
||
var cts = new CancellationTokenSource();
|
||
cts.CancelAfter(READ_TIMEOUT_MS);
|
||
try
|
||
{
|
||
readResult = await authCharacteristic.ReadAsync(cts.Token); // encrypted seed value (random value) from lock to decrypt using k_user
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving encrypted random value from lock (seed)(ReadAsync-call) failed.{ReadCharacteristic}{Exception}", ToSerilogString(authCharacteristic), exception);
|
||
throw new System.Exception(
|
||
string.Format("Can not authenticate. Reading encrypted seed failed. {0}", exception.Message),
|
||
exception);
|
||
}
|
||
finally
|
||
{
|
||
cts.Dispose();
|
||
}
|
||
|
||
if (readResult.Success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(
|
||
"Retrieving encrypted random value from lock (seed)(ReadAsync-call) failed.{ReadCharacteristic}{ErrorCode}",
|
||
ToSerilogString(authCharacteristic),
|
||
readResult.Success);
|
||
|
||
throw new BleReturnCodeException(readResult.Success, "Retrieving encrypted random value from lock failed.");
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Retrieving encrypted random value from lock (seed)(ReadAsync-call) succeeded.{ReadCharacteristic}{Reading}", ToSerilogString(authCharacteristic), "***");
|
||
|
||
var crypto = new AuthCryptoHelper(
|
||
readResult.SeedLockEncrypted,
|
||
lockInfo.K_u,
|
||
cipher);
|
||
|
||
byte[] accessKeyEncrypted;
|
||
try
|
||
{
|
||
accessKeyEncrypted = crypto.GetAccessKeyEncrypted();
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Error getting encrypted access key. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not authenticate. Getting access key failed. {0}", exception.Message), exception);
|
||
}
|
||
|
||
try
|
||
{
|
||
success = await authCharacteristic.WriteAsync(accessKeyEncrypted);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}{Exception}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), ToSerilogString(accessKeyEncrypted), exception);
|
||
throw new System.Exception(string.Format("Can not authenticate. Writing access key failed. {0}", exception.Message), exception);
|
||
}
|
||
if (success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}{ErrorCode}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), "***", success);
|
||
throw new System.Exception(string.Format("Can not authenticate. Writing access key did not succeed."));
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Encrypted access key written successfully.{Key}{Seed}{AuthCharacteristic}{CommandWritten}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), "***");
|
||
}
|
||
|
||
/// <summary> Gets the lock state like locking state (open/ close). </summary>
|
||
/// <param name="doWaitRetry">True if to wait and retry in case of failures. </param>
|
||
/// <remarks>
|
||
/// Lock state is first byte of value read from state characteristic ("0000baaa-1212-efde-1523-785fef13d123").
|
||
/// Values are as follows
|
||
/// Open = 0x00,
|
||
/// Closed = 0x01,
|
||
/// Unknown = 0x02,
|
||
/// CouldntCloseMoving = 0x03,
|
||
/// CouldntOpenBoltBlocked = 0x04,
|
||
/// CouldntCloseBoltBlocked = 0x05
|
||
/// ShareeBike.Services.BluetoothLock.Tdo.LockitLockingState.
|
||
/// </remarks>
|
||
/// <returns> Lock state.</returns>
|
||
/// <exception cref="BluetoothDisconnectedException">App is not connected to lock.</exception>
|
||
/// <exception cref="CoundntGetCharacteristicException">Getting state characteristic to read from failed.</exception>
|
||
/// <exception cref="Exception">
|
||
/// Call not from main thread or unknown platform detected or
|
||
/// query device state (connected, disconnected, ....) failed for an unknown reason or returned an unexpected value or
|
||
/// reading state characteristic failed or reading from characteristic was empty.
|
||
/// </exception>
|
||
/// <exception> Exceptions thrown by PluginBle::ICharacteristic.ReasAsync.</exception>
|
||
public async Task<LockInfoTdo> GetLockStateAsync(bool doWaitRetry = false)
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not get lock state. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
DeviceState? deviceState;
|
||
Log.ForContext<LockItBase>().Debug("Request to get connection state in context of getting locking state.");
|
||
try
|
||
{
|
||
deviceState = Device?.State.GetDeviceState()
|
||
?? throw new System.Exception("Can not get bluetooth device state. State must not be null.");
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Can not get lock state. Retrieving bluetooth state failed. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not get lock state. Getting bluetooth state failed. {0}", exception.Message), exception);
|
||
}
|
||
|
||
switch (deviceState)
|
||
{
|
||
case DeviceState.Disconnected:
|
||
throw new BluetoothDisconnectedException();
|
||
|
||
case DeviceState.Connected:
|
||
break;
|
||
|
||
default:
|
||
// Can not get state if device is not connected.
|
||
Log.ForContext<LockItBase>().Error($"Getting lock state failed. Unexpected lock state {deviceState} detected.");
|
||
throw new System.Exception(string.Format("Can not get lock state. Unexpected bluetooth state {0} detected.", deviceState));
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Connection state is {deviceState}.");
|
||
|
||
var stateCharacteristic = await GetStateCharacteristicAsync();
|
||
if (stateCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Error($"Can not get lock state. State characteristic is not available.");
|
||
throw new CoundntGetCharacteristicException("Can not get lock state. State characteristic must not be null.");
|
||
}
|
||
|
||
// Reads the lock state from characteristic
|
||
async Task<(byte[], int)> readAsyncDelegate()
|
||
{
|
||
var cts = new CancellationTokenSource();
|
||
(byte[] State, int Success) innerReadResult;
|
||
cts.CancelAfter(READ_TIMEOUT_MS);
|
||
try
|
||
{
|
||
innerReadResult = await stateCharacteristic.ReadAsync(cts.Token);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving lock state (ReadAsync-call) failed inside delegate.{StateCharacteristic}{Exception}", ToSerilogString(stateCharacteristic), exception);
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
cts.Dispose();
|
||
}
|
||
|
||
if (innerReadResult.Success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(
|
||
"Retrieving lock state (ReadAsync-call) failed inside delegate.{StateCharacteristic}{ErrorCode}",
|
||
ToSerilogString(stateCharacteristic),
|
||
innerReadResult.Success);
|
||
|
||
throw new BleReturnCodeException(innerReadResult.Success, "Retrieving lock state failed.");
|
||
}
|
||
|
||
return innerReadResult;
|
||
}
|
||
|
||
(byte[] State, int Success) readResult;
|
||
|
||
try
|
||
{
|
||
readResult = doWaitRetry
|
||
? await _retryPollicy.ExecuteAsync(async () => await readAsyncDelegate())
|
||
: await readAsyncDelegate();
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving lock state (ReadAsync-call) failed.{StateCharacteristic}{Exception}", ToSerilogString(stateCharacteristic), exception);
|
||
throw new System.Exception(string.Format("Can not get lock state. Reading data failed. {0}", exception.Message), exception);
|
||
}
|
||
|
||
if (readResult.State == null || readResult.State.Length <= 0)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Retrieving lock state (ReadAsync-call) failed. Data read is null or empty.{StateCharacteristic}", ToSerilogString(stateCharacteristic));
|
||
throw new System.Exception("Can not get lock state. No data read");
|
||
}
|
||
|
||
int lockingState = readResult.State[0];
|
||
var lockInfoTdo = new LockInfoTdo.Builder
|
||
{
|
||
Id = Id,
|
||
Guid = Guid,
|
||
State = Enum.IsDefined(typeof(LockitLockingState), lockingState) ? (LockitLockingState?) lockingState : null
|
||
}.Build();
|
||
|
||
Log.ForContext<LockItBase>().Debug("Retrieving lock state (ReadAsync-call) succeeded. {@LockInfoTdo}{StateCharacteristic}{Reading}",
|
||
lockInfoTdo,
|
||
ToSerilogString(stateCharacteristic),
|
||
readResult.State);
|
||
|
||
return lockInfoTdo;
|
||
}
|
||
|
||
/// <summary>Gets the battery percentage.</summary>
|
||
public async Task<double> GetBatteryPercentageAsync()
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not get battery percentage. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
DeviceState? deviceState;
|
||
Log.ForContext<LockItBase>().Debug("Request to get battery percentage in context of getting locking state.");
|
||
try
|
||
{
|
||
deviceState = Device?.State.GetDeviceState()
|
||
?? throw new System.Exception("Can not get bluetooth device state. State must not be null.");
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Can not get battery percentage. Retrieving bluetooth state failed. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not get battery percentage. Getting bluetooth state failed. {0}", exception.Message), exception);
|
||
}
|
||
|
||
switch (deviceState)
|
||
{
|
||
case DeviceState.Disconnected:
|
||
throw new BluetoothDisconnectedException();
|
||
|
||
case DeviceState.Connected:
|
||
break;
|
||
|
||
default:
|
||
// Can not get state if device is not connected.
|
||
Log.ForContext<LockItBase>().Error($"Getting battery percentage failed. Unexpected battery percentage {deviceState} detected.");
|
||
throw new System.Exception(string.Format("Can not get battery percentage. Unexpected bluetooth state {0} detected.", deviceState));
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Connection state is {deviceState}.");
|
||
|
||
var batteryCharacteristic = await GetBatteryCharacteristicAsync();
|
||
if (batteryCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Error($"Can not get battery percentage. State characteristic is not available.");
|
||
throw new CoundntGetCharacteristicException("Can not get battery percentage. State characteristic must not be null.");
|
||
}
|
||
|
||
(byte[] Percentage, int Success) readResult;
|
||
var cts = new CancellationTokenSource();
|
||
cts.CancelAfter(READ_TIMEOUT_MS);
|
||
try
|
||
{
|
||
readResult = await batteryCharacteristic.ReadAsync(cts.Token);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving charging level (ReadAsync-call) failed. {BatteryCharacteristic}{Exception}", ToSerilogString(batteryCharacteristic), exception);
|
||
throw new System.Exception(
|
||
string.Format("Can not get battery percentage. Reading data failed. {0}", exception.Message),
|
||
exception);
|
||
}
|
||
finally
|
||
{
|
||
cts.Dispose();
|
||
}
|
||
|
||
if (readResult.Success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(
|
||
"Retrieving charging level (ReadAsync-call) failed. {BatteryCharacteristic}{ErrorCode}",
|
||
ToSerilogString(batteryCharacteristic),
|
||
readResult.Success);
|
||
|
||
throw new BleReturnCodeException(readResult.Success, "Retrieving charging level failed.");
|
||
}
|
||
|
||
if (readResult.Percentage == null || readResult.Percentage.Length <= 0)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Retrieving charging level (ReadAsync-call) failed. Data read is null or empty.{BatteryCharacteristic}", ToSerilogString(batteryCharacteristic));
|
||
throw new System.Exception("Can not get battery percentage. No data read.");
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Retrieving charging level (ReadAsync-call) succeeded.{Level}{BatteryCharacteristic}{Reading}", readResult.Percentage[0], ToSerilogString(batteryCharacteristic), readResult.Percentage);
|
||
|
||
return readResult.Percentage[0];
|
||
}
|
||
|
||
/// <summary> Gets version info about the lock. </summary>
|
||
/// <remarks>
|
||
/// Lock state is first byte of value read from state characteristic ("0000baaa-1212-efde-1523-785fef13d123").
|
||
/// Values are as follows
|
||
/// Byte number 0: firmware version,
|
||
/// Byte number 1: lock version (2 – classic, 3 – plus, 4 – GPS)
|
||
/// Byte number 2: hardware version,
|
||
/// </remarks>
|
||
/// <returns> .</returns>
|
||
public async Task<VersionInfoTdo> GetVersionInfoAsync()
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not get versions info. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
DeviceState? deviceState;
|
||
Log.ForContext<LockItBase>().Debug("Request to get connection state in context of getting versions info.");
|
||
try
|
||
{
|
||
deviceState = Device?.State.GetDeviceState()
|
||
?? throw new System.Exception("Can not get bluetooth device state. State must not be null.");
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Can not get versions info. Retrieving bluetooth state failed. {Exception}", exception);
|
||
throw new System.Exception(string.Format("Can not get versions info. Getting bluetooth state failed. {0}", exception.Message), exception);
|
||
}
|
||
|
||
switch (deviceState)
|
||
{
|
||
case DeviceState.Disconnected:
|
||
throw new BluetoothDisconnectedException();
|
||
|
||
case DeviceState.Connected:
|
||
break;
|
||
|
||
default:
|
||
// Can not get state if device is not connected.
|
||
Log.ForContext<LockItBase>().Error($"Getting versions info failed. Unexpected versions info {deviceState} detected.");
|
||
throw new System.Exception(string.Format("Can not get versions info. Unexpected bluetooth state {0} detected.", deviceState));
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug($"Connection state is {deviceState}.");
|
||
|
||
var firmwareVersionCharacteristic = await GetVersionsCharacteristicAsync();
|
||
if (firmwareVersionCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Error($"Can not get versions info. versions info characteristic is not available.");
|
||
throw new CoundntGetCharacteristicException("Can not get versions info. versions info characteristic must not be null.");
|
||
}
|
||
|
||
(byte[] Version, int Success) readResult;
|
||
var cts = new CancellationTokenSource();
|
||
cts.CancelAfter(READ_TIMEOUT_MS);
|
||
try
|
||
{
|
||
readResult = await firmwareVersionCharacteristic.ReadAsync(cts.Token);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving versions info (ReadAsync-call) failed inside delegate.{StateCharacteristic}{Exception}", ToSerilogString(firmwareVersionCharacteristic), exception);
|
||
throw;
|
||
}
|
||
finally
|
||
{
|
||
cts.Dispose();
|
||
}
|
||
|
||
if (readResult.Success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(
|
||
"Retrieving versions info (ReadAsync-call) failed inside delegate.{StateCharacteristic}{ErrorCode}",
|
||
ToSerilogString(firmwareVersionCharacteristic),
|
||
readResult.Success);
|
||
|
||
throw new BleReturnCodeException(readResult.Success, "Retrieving versions info failed");
|
||
}
|
||
|
||
if (readResult.Version == null || readResult.Version.Length <= 0)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Retrieving versions info (ReadAsync-call) failed. Data read is null or empty.{StateCharacteristic}", ToSerilogString(firmwareVersionCharacteristic));
|
||
throw new System.Exception("Can not get versions info. No data read");
|
||
}
|
||
|
||
VersionInfo = new VersionInfoTdo.Builder
|
||
{
|
||
FirmwareVersion = readResult.Version[0],
|
||
LockVersion = readResult.Version[1],
|
||
HardwareVersion = readResult.Version[2]
|
||
}.Build();
|
||
|
||
Log.ForContext<LockItBase>().Debug("Retrieving versions info (ReadAsync-call) succeeded. {@LockInfoTdo}{StateCharacteristic}{Reading}",
|
||
VersionInfo,
|
||
ToSerilogString(firmwareVersionCharacteristic),
|
||
readResult.Version);
|
||
|
||
return VersionInfo;
|
||
}
|
||
|
||
public VersionInfoTdo VersionInfo { get; private set; }
|
||
|
||
/// <summary> Opens lock. </summary>
|
||
/// <returns> Locking state.</returns>
|
||
public abstract Task<LockitLockingState?> OpenAsync();
|
||
|
||
/// <summary> Close the lock.</summary>
|
||
/// <returns>Locking state.</returns>
|
||
public abstract Task<LockitLockingState?> CloseAsync();
|
||
|
||
/// <summary> Opens/ closes lock.</summary>
|
||
/// <param name="counter"></param>
|
||
/// <param name="open"></param>
|
||
/// <returns>True if opening/ closing command could be written successfully.</returns>
|
||
protected async Task<bool> OpenCloseLockAsync(bool open)
|
||
{
|
||
DeviceState deviceState;
|
||
Log.ForContext<LockItBase>().Debug(open
|
||
? "Request to get connection state in context of opening lock."
|
||
: "Request to get connection state in context of closing lock.");
|
||
try
|
||
{
|
||
deviceState = Device?.State.GetDeviceState()
|
||
?? throw new System.Exception(open
|
||
? "Can not open lock. Getting bluetooth device state failed. State must not be null."
|
||
: "Can not close lock. Getting bluetooth device state failed. State must not be null.");
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(open
|
||
? "Retrieving bluetooth state failed when opening lock failed. {Exception}"
|
||
: "Retrieving bluetooth state failed when closing lock failed. {Exception}",
|
||
exception);
|
||
throw new System.Exception(open
|
||
? $"Can not open lock. Getting bluetooth failed. {exception.Message}"
|
||
: $"Can not close lock. Getting bluetooth failed. {exception.Message}",
|
||
exception);
|
||
}
|
||
|
||
switch (deviceState)
|
||
{
|
||
case DeviceState.Disconnected:
|
||
throw new BluetoothDisconnectedException();
|
||
|
||
case DeviceState.Connected:
|
||
break;
|
||
|
||
default:
|
||
// Can not open lock if bluetooth state is not connected.
|
||
Log.ForContext<LockItBase>().Debug(open
|
||
? $"Can not open lock. Unexpected connection state detected {deviceState}."
|
||
: $"Can not close lock. Unexpected connection state detected {deviceState}.");
|
||
return false;
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug(open
|
||
? $"Connection state before opening lock is {deviceState}."
|
||
: $"Connection state before closing lock is {deviceState}.");
|
||
|
||
var activateLockCharacteristic = await GetActivateLockCharacteristicAsync();
|
||
if (activateLockCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug(open
|
||
? "Can not open lock. Getting lock control service failed."
|
||
: "Can not close lock. Getting lock control service failed.");
|
||
return false;
|
||
}
|
||
|
||
byte[] state = bitShift(
|
||
new byte[] { 0, 0, open ? (byte)1 : (byte)2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
|
||
++ActivateLockWriteCounter);
|
||
|
||
byte[] stateEnctryped;
|
||
try
|
||
{
|
||
stateEnctryped = Cipher.Encrypt(CopriKey, state);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
ActivateLockWriteCounter--;
|
||
Log.ForContext<LockItBase>().Error(open
|
||
? "Encrypting command to open lock failed. {Exception}"
|
||
: "Encrypting command to close lock failed. {Exception}",
|
||
exception);
|
||
throw new System.Exception(open
|
||
? $"Can not open lock. Encrypting command to lock/ unlock failed. {exception.Message}"
|
||
: $"Can not close lock. Encrypting command to lock/ unlock failed. {exception.Message}",
|
||
exception);
|
||
}
|
||
|
||
int success;
|
||
try
|
||
{
|
||
success = await activateLockCharacteristic.WriteAsync(stateEnctryped);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
ActivateLockWriteCounter--;
|
||
Log.ForContext<LockItBase>().Error(open
|
||
? "Writing data to open lock failed.{ActivateLockCharacteristic}{CommandWritten}{Exception}"
|
||
: "Writing data to close lock failed.{ActivateLockCharacteristic}{CommandWritten}{Exception}",
|
||
ToSerilogString(activateLockCharacteristic),
|
||
ToSerilogString(stateEnctryped),
|
||
exception);
|
||
throw new System.Exception(open
|
||
? $"Can not open lock. Writing command failed. {exception.Message}"
|
||
: $"Can not close lock. Writing command failed. {exception.Message}",
|
||
exception);
|
||
}
|
||
|
||
if (success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(open
|
||
? "Writing data to open lock failed.{ActivateLockCharacteristic}{CommandWritten}{ErrorCode}"
|
||
: "Writing data to close lock failed.{ActivateLockCharacteristic}{CommandWritten}{ErrorCode}",
|
||
ToSerilogString(activateLockCharacteristic),
|
||
ToSerilogString(stateEnctryped),
|
||
success);
|
||
|
||
return false;
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug(open
|
||
? "Command to open lock written successfully.{ActivateLockCharacteristic}{CommandWritten}"
|
||
: "Command to close lock written successfully.{ActivateLockCharacteristic}{CommandWritten}",
|
||
ToSerilogString(activateLockCharacteristic),
|
||
ToSerilogString(stateEnctryped));
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary> Gets a value indicating whether alarm is on or off.</summary>
|
||
public async Task<bool> GetIsAlarmOffAsync()
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not turn alarm off. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
var alarmCharacteristic = await GetAlarmCharacteristicAsync();
|
||
if (alarmCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Getting alarm characteristic failed.");
|
||
throw new System.Exception($"Can not get alarm whether alarm is on or off. Getting alarm characteristic returned null.");
|
||
}
|
||
|
||
(byte[] AlarmSettings, int Success) readResult;
|
||
var cts = new CancellationTokenSource();
|
||
cts.CancelAfter(READ_TIMEOUT_MS);
|
||
try
|
||
{
|
||
readResult = await alarmCharacteristic.ReadAsync(cts.Token);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{Exception}", ToSerilogString(alarmCharacteristic), exception);
|
||
throw new System.Exception(
|
||
$"Can not get whether alarm is off or on {exception.Message}.",
|
||
exception);
|
||
}
|
||
finally
|
||
{
|
||
cts.Dispose();
|
||
}
|
||
|
||
if (readResult.Success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(
|
||
"Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{ErrorCode}",
|
||
ToSerilogString(alarmCharacteristic),
|
||
readResult.Success);
|
||
|
||
throw new BleReturnCodeException(readResult.Success, "Retrieving alarm settings failed.");
|
||
}
|
||
|
||
if (readResult.AlarmSettings == null || readResult.AlarmSettings.Length < 1)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{Reading}", ToSerilogString(alarmCharacteristic));
|
||
throw new System.Exception("Can not get whether alarm is off or on. Reading failed.");
|
||
}
|
||
|
||
var isAlarmOff = readResult.AlarmSettings[0] == 0;
|
||
Log.ForContext<LockItBase>().Debug("Retrieving alarm settings (ReadAsync-call) succeeded.{IsArlarmOff}{AlarmCharacteristic}{Reading}", isAlarmOff, ToSerilogString(alarmCharacteristic), readResult.AlarmSettings);
|
||
return isAlarmOff;
|
||
}
|
||
|
||
/// <summary> Sets alarm on or off.</summary>
|
||
public async Task SetIsAlarmOffAsync(bool isActivated)
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not turn alarm off. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
ICharacteristic alarmCharacteristic = await GetAlarmCharacteristicAsync();
|
||
if (alarmCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Getting alarm characteristic failed.");
|
||
throw new System.Exception($"Can not set alarm {isActivated}. Getting alarm characteristic returned null.");
|
||
}
|
||
|
||
int success;
|
||
var command = new byte[] { isActivated ? (byte)1 : (byte)0 };
|
||
try
|
||
{
|
||
success = await alarmCharacteristic.WriteAsync(command);
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}{Exception}", ToSerilogString(alarmCharacteristic), command[0], exception);
|
||
throw new System.Exception($"Can not set alarm settings. {exception.Message}", exception);
|
||
}
|
||
|
||
if (success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}{ErrorCode}", ToSerilogString(alarmCharacteristic), command[0], success);
|
||
throw new System.Exception($"Can not set alarm settings. Writing settings did not succeed.");
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug("Alarm settings written successfully.{AlarmCharacteristic}{CommandWritten}.", ToSerilogString(alarmCharacteristic), command[0]);
|
||
}
|
||
|
||
public async Task<bool> SetSoundAsync(SoundSettings settings)
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not set sound settings. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
ICharacteristic soundCharacteristic = await GetSoundCharacteristicAsync();
|
||
if (soundCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Getting sound characteristic failed.");
|
||
return false;
|
||
}
|
||
|
||
int success;
|
||
byte command = (byte)settings;
|
||
try
|
||
{
|
||
success = await soundCharacteristic.WriteAsync(new byte[] { command });
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing sound settings failed.{SoundCharacteristic}{CommandWritten}{Exception}", ToSerilogString(soundCharacteristic), command, exception);
|
||
throw new System.Exception($"Can not set sound settings. {exception.Message}", exception);
|
||
}
|
||
|
||
if (success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(
|
||
"Writing sound settings failed.{SoundCharacteristic}{CommandWritten}{ErrorCode}",
|
||
ToSerilogString(soundCharacteristic),
|
||
command,
|
||
success);
|
||
|
||
return false;
|
||
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug(
|
||
"Sound settings written successfully.{SoundCharacteristic}{CommandWritten}",
|
||
ToSerilogString(soundCharacteristic),
|
||
command);
|
||
|
||
return true;
|
||
}
|
||
|
||
public async Task<bool> SetAlarmSettingsAsync(AlarmSettings settings)
|
||
{
|
||
if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false)
|
||
{
|
||
throw new System.Exception("Can not set alarm settings. Platform must not be unknown and bluetooth code must be run on main thread.");
|
||
}
|
||
|
||
ICharacteristic alarmSettingsCharacteristic = await GetAlarmSettingsCharacteristicAsync();
|
||
if (alarmSettingsCharacteristic == null)
|
||
{
|
||
Log.ForContext<LockItBase>().Debug("Getting alarm settings characteristic failed.");
|
||
return false;
|
||
}
|
||
|
||
int success;
|
||
byte command = (byte)settings;
|
||
try
|
||
{
|
||
success = await alarmSettingsCharacteristic.WriteAsync(new byte[] { command });
|
||
}
|
||
catch (System.Exception exception)
|
||
{
|
||
Log.ForContext<LockItBase>().Error("Writing alarm settings failed.{SoundCharacteristic}{CommandWritten}{Exception}", ToSerilogString(alarmSettingsCharacteristic), command, exception);
|
||
throw new System.Exception($"Can not set alarm settings. {exception.Message}", exception);
|
||
}
|
||
|
||
if (success != ERRORCODE_SUCCESS)
|
||
{
|
||
Log.ForContext<LockItBase>().Error(
|
||
"Writing alarm settings failed.{SoundCharacteristic}{CommandWritten}{ErrorCode}",
|
||
ToSerilogString(alarmSettingsCharacteristic),
|
||
command,
|
||
success);
|
||
return false;
|
||
}
|
||
|
||
Log.ForContext<LockItBase>().Debug(
|
||
"Alarm settings written successfully.{SoundCharacteristic}{CommandWritten}",
|
||
ToSerilogString(alarmSettingsCharacteristic),
|
||
command);
|
||
|
||
return true;
|
||
}
|
||
|
||
private static byte[] bitShift(byte[] data, int counter)
|
||
{
|
||
int mask = 0x000000FF;
|
||
data[0] = (byte)(counter & mask);
|
||
data[1] = (byte)(counter >> 8);
|
||
|
||
return data;
|
||
}
|
||
|
||
|
||
/// <summary> Disconnect from bluetooth lock. </summary>
|
||
public async Task Disconnect() => await Adapter.DisconnectDeviceAsync(Device);
|
||
|
||
/// <summary>
|
||
/// Don' t use .Destructure.ByTransforming<ICharacteristic>(...) because this would introduce a dependency to Plugin.BLE in main project.
|
||
/// </summary>
|
||
/// <param name="charcteristic"></param>
|
||
/// <returns></returns>
|
||
private static string ToSerilogString(ICharacteristic charcteristic)
|
||
=> charcteristic.Id.ToString();
|
||
|
||
private static string ToSerilogString(byte[] byteArray)
|
||
=> "***"; // For debugging purposes it might be required to return string.Join(",", byteArray); Do not log any confidential value in production context.
|
||
}
|
||
}
|