sharee.bike-App/LockItBLE/Services/BluetoothLock/BLE/LockItBase.cs
2022-09-20 13:51:55 +02:00

1257 lines
47 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 TINK.Model.Connector;
using TINK.Model.Device;
using TINK.Services.BluetoothLock.Crypto;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.Tdo;
using Xamarin.Essentials;
namespace TINK.Services.BluetoothLock.BLE
{
/// <summary> Manages a single lock.</summary>
public abstract class LockItBase : ILockService
{
/// <summary> Lenght 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;
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[]>
.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[]> _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 charcteristic 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-charcteristic 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 charcteristic 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-charcteristic 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 charcteristic 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 charcteristic 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 charcteristic 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 charcteristic 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 idvertisement name.</summary>
public string Name { get; private set; } = string.Empty;
/// <summary> Id part of idvertisement 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-lenght {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-lenght {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 autenticate for {lockIt.Name}.");
var authCharacteristic = await lockIt.GetAuthCharacteristicAsync();
if (authCharacteristic == null)
{
Log.ForContext<LockItBase>().Debug("Getting auth-charcteristic failed.");
throw new CoundntGetCharacteristicException("Authentication failed. Auth characteristic must not be null.");
}
bool 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)
{
Log.ForContext<LockItBase>().Debug("Writing copri seed failed.{AuthCharacteristic}{CommandWritten}", ToSerilogString(authCharacteristic), ToSerilogString(lockInfo.K_seed));
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;
var cts = new CancellationTokenSource();
cts.CancelAfter(READ_TIMEOUT_MS);
try
{
seedLockEncrypted = await authCharacteristic.ReadAsync(cts.Token); // encrypted seed value (random value) from lock to decypt using k_user
}
catch (System.Exception exception)
{
Log.ForContext<LockItBase>().Error("Retrieveing 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();
}
Log.ForContext<LockItBase>().Debug("Retrieveing encrypted random value from lock (seed)(ReadAsync-call) succeeded.{ReadCharacteristic}{Reading}", ToSerilogString(authCharacteristic), "***");
var crypto = new AuthCryptoHelper(
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)
{
Log.ForContext<LockItBase>().Debug("Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}", ToSerilogString(crypto.KeyCopri), ToSerilogString(lockInfo.K_seed), ToSerilogString(authCharacteristic), "***");
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 of value read from state characteristic ("0000baaa-1212-efde-1523-785fef13d123").
/// Values are as follows
/// Open = 0x00,
/// Closed = 0x01,
/// Unknown = 0x02,
/// CouldntCloseMoving = 0x03,
/// CouldntOpenBoldBlocked = 0x04,
/// CouldntCloseBoldBlocked = 0x05
/// TINK.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 unkonwn 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.");
}
byte[] state;
async Task<byte[]> readAsyncDelegate()
{
var cts = new CancellationTokenSource();
cts.CancelAfter(READ_TIMEOUT_MS);
try
{
return 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();
}
}
try
{
state = 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 (state == null || 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");
}
var lockInfoTdo = new LockInfoTdo.Builder
{
Id = Id,
Guid = Guid,
State = (LockitLockingState?)state[0]
}.Build();
Log.ForContext<LockItBase>().Debug("Retrieving lock state (ReadAsync-call) succeeded. {@LockInfoTdo}{StateCharacteristic}{Reading}",
lockInfoTdo,
ToSerilogString(stateCharacteristic),
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;
var cts = new CancellationTokenSource();
cts.CancelAfter(READ_TIMEOUT_MS);
try
{
percentage = 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 (percentage == null || 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}", percentage[0], ToSerilogString(batteryCharacteristic), percentage);
return percentage[0];
}
/// <summary> Gets version info about the lock. </summary>
/// <remarks>
/// Lock state is first byte of 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;
var cts = new CancellationTokenSource();
cts.CancelAfter(READ_TIMEOUT_MS);
try
{
version = 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 (version == null || 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 = version[0],
LockVersion = version[1],
HardwareVersion = version[2]
}.Build();
Log.ForContext<LockItBase>().Debug("Retrieving versions info (ReadAsync-call) succeeded. {@LockInfoTdo}{StateCharacteristic}{Reading}",
VersionInfo,
ToSerilogString(firmwareVersionCharacteristic),
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
? "Encypting command to open lock failed. {Exception}"
: "Encypting 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);
}
bool 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);
}
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 success;
}
/// <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;
var cts = new CancellationTokenSource();
cts.CancelAfter(READ_TIMEOUT_MS);
try
{
alarmSettings = 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}.");
}
finally
{
cts.Dispose();
}
if (alarmSettings == null || 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 = alarmSettings[0] == 0;
Log.ForContext<LockItBase>().Debug("Retrieving alarm settings (ReadAsync-call) succeeded.{IsArlarmOff}{AlarmCharacteristic}{Reading}", isAlarmOff, ToSerilogString(alarmCharacteristic), 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.");
}
bool 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)
{
Log.ForContext<LockItBase>().Error("Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}", ToSerilogString(alarmCharacteristic), command[0]);
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;
}
bool 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);
}
Log.ForContext<LockItBase>().Debug(success
? "Writing sound settings failed.{SoundCharacteristic}{CommandWritten}"
: "Sound settings written successfully.{SoundCharacteristic}{CommandWritten}",
ToSerilogString(soundCharacteristic), command);
return success;
}
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;
}
bool 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);
}
Log.ForContext<LockItBase>().Debug(success
? "Writing alarm settings failed.{SoundCharacteristic}{CommandWritten}"
: "Alarm settings written successfully.{SoundCharacteristic}{CommandWritten}",
ToSerilogString(alarmSettingsCharacteristic), command);
return success;
}
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 debuging purposes it might be reqired to return string.Join(",", byteArray); Do not log any confidental value in production context.
}
}