2024-04-09 12:53:23 +02:00
// Ignore Spelling: "0000f00d-1212-efde-1523-785fef13d123"
2022-09-06 16:08:19 +02:00
using System ;
2022-08-30 15:42:25 +02:00
using System.Collections.Generic ;
using System.Threading ;
using System.Threading.Tasks ;
using Plugin.BLE.Abstractions ;
2021-05-13 17:25:46 +02:00
using Plugin.BLE.Abstractions.Contracts ;
using Plugin.BLE.Abstractions.Exceptions ;
2022-08-30 15:42:25 +02:00
using Polly ;
using Polly.Retry ;
2021-05-13 17:25:46 +02:00
using Serilog ;
2024-04-09 12:53:23 +02:00
using ShareeBike.Model.Connector ;
using ShareeBike.Model.Device ;
using ShareeBike.Services.BluetoothLock.Crypto ;
using ShareeBike.Services.BluetoothLock.Exception ;
using ShareeBike.Services.BluetoothLock.Tdo ;
2021-05-13 17:25:46 +02:00
using Xamarin.Essentials ;
2024-04-09 12:53:23 +02:00
namespace ShareeBike.Services.BluetoothLock.BLE
2021-05-13 17:25:46 +02:00
{
2022-09-06 16:08:19 +02:00
/// <summary> Manages a single lock.</summary>
public abstract class LockItBase : ILockService
{
2023-08-31 12:20:06 +02:00
/// <summary> Length of seed in bytes.</summary>
2022-09-06 16:08:19 +02:00
private const int SEEDLENGTH = 16 ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Timeout for open/ close operations.</summary>
protected const int OPEN_CLOSE_TIMEOUT_MS = 30000 ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Timeout for get service operations.</summary>
private const int GETSERVICE_TIMEOUT_MS = 3000 ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Timeout for read operations.</summary>
private const int READ_TIMEOUT_MS = 3000 ;
2021-05-13 17:25:46 +02:00
2024-04-09 12:53:23 +02:00
/// <summary> Return code which indicates success.</summary>
public const int ERRORCODE_SUCCESS = 0 ;
2022-09-06 16:08:19 +02:00
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 ) ) ;
2021-05-13 17:25:46 +02:00
2024-04-09 12:53:23 +02:00
_retryPollicy = Policy < ( byte [ ] , int ) >
2022-09-06 16:08:19 +02:00
. Handle < CharacteristicReadException > ( )
. WaitAndRetryAsync ( 2 , index = > TimeSpan . FromMilliseconds ( 100 ) ) ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
GetGuid ( ) ;
GetName ( ) ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
if ( InvalidatedSeed . ContainsKey ( Guid ) )
{
// Lock was already connected. No need to add entry.
return ;
}
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
InvalidatedSeed . Add ( Guid , new List < string > ( ) ) ;
}
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private static readonly Dictionary < Guid , List < string > > InvalidatedSeed = new Dictionary < Guid , List < string > > ( ) ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Count of write- actions to activate lock characteristic..</summary>
protected int ActivateLockWriteCounter { get ; private set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
protected ICipher Cipher { get ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
protected IAdapter Adapter { get ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
protected IDevice Device { get ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private IService LockControl { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private IService BatteryControl { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private ICharacteristic ActivateLockCharacteristic { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private ICharacteristic AlarmCharacteristic { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private ICharacteristic AlarmSettingsCharacteristic { get ; set ; }
2022-01-22 18:28:01 +01:00
2022-09-06 16:08:19 +02:00
private ICharacteristic AuthCharacteristic { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private ICharacteristic StateCharacteristic { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private ICharacteristic SoundCharacteristic { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
private ICharacteristic BatteryCharacteristic { get ; set ; }
2021-05-13 17:25:46 +02:00
2022-09-20 13:51:55 +02:00
private ICharacteristic FirmwareVersionCharacteristic { get ; set ; }
2024-04-09 12:53:23 +02:00
private readonly AsyncRetryPolicy < ( byte [ ] , int ) > _retryPollicy ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Gets the lock control service.</summary>
private async Task < IService > GetLockControlService ( )
{
if ( LockControl ! = null ) return LockControl ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
LockControl = null ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
Log . ForContext < LockItBase > ( ) . Debug ( "Request to get lock control service." ) ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
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 ( ) ;
}
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
Log . ForContext < LockItBase > ( ) . Debug ( "Getting lock control service succeeded." ) ;
return LockControl ;
}
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
/// <summary> Gets battery service.</summary>
private async Task < IService > GetBatteryService ( )
{
if ( BatteryControl ! = null ) return BatteryControl ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
BatteryControl = null ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
Log . ForContext < LockItBase > ( ) . Debug ( "Request to get battery control service." ) ;
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
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 ;
}
2021-05-13 17:25:46 +02:00
2022-09-06 16:08:19 +02:00
/// <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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting activate lock characteristic failed. {Exception}" , exception ) ;
2022-09-06 16:08:19 +02:00
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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting alarm-characteristic failed. {Exception}" , exception ) ;
2022-09-06 16:08:19 +02:00
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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting alarm settings characteristic failed. {Exception}" , exception ) ;
2022-09-06 16:08:19 +02:00
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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting auth-characteristic failed. {Exception}" , exception ) ;
2022-09-06 16:08:19 +02:00
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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting state characteristic failed. {Exception}" , exception ) ;
2022-09-06 16:08:19 +02:00
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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting sound characteristic failed. {Exception}" , exception ) ;
2022-09-06 16:08:19 +02:00
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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting battery characteristic failed. {Exception}" , exception ) ;
2022-09-06 16:08:19 +02:00
throw new System . Exception ( $"Can not get battery characteristic. {exception.Message}" , exception ) ;
}
Log . ForContext < LockItBase > ( ) . Debug ( "Get battery characteristic retrieved successfully." ) ;
return BatteryCharacteristic ;
}
2022-09-20 13:51:55 +02:00
/// <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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Getting versions info characteristic failed. {Exception}" , exception ) ;
2022-09-20 13:51:55 +02:00
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 ;
}
2022-09-06 16:08:19 +02:00
/// <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 ;
}
2023-08-31 12:20:06 +02:00
/// <summary> Full advertisement name.</summary>
2022-09-06 16:08:19 +02:00
public string Name { get ; private set ; } = string . Empty ;
2023-08-31 12:20:06 +02:00
/// <summary> Id part of advertisement name.</summary>
2022-09-06 16:08:19 +02:00
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 )
{
2023-04-19 12:14:14 +02:00
throw new AuthKeyException ( $"Can not authenticate. Invalid seed-/ k-user-length {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected." ) ;
2022-09-06 16:08:19 +02:00
}
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 )
{
2023-04-19 12:14:14 +02:00
throw new AuthKeyException ( $"Can not authenticate. Invalid seed-/ k-user-length {authInfo.K_seed.Length}/ {authInfo.K_u.Length} detected." ) ;
2022-09-06 16:08:19 +02:00
}
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 )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Debug ( $"Request to authenticate for {lockIt.Name}." ) ;
2022-09-06 16:08:19 +02:00
var authCharacteristic = await lockIt . GetAuthCharacteristicAsync ( ) ;
if ( authCharacteristic = = null )
{
2023-08-31 12:20:06 +02:00
Log . ForContext < LockItBase > ( ) . Debug ( "Getting auth-characteristic failed." ) ;
2022-09-06 16:08:19 +02:00
throw new CoundntGetCharacteristicException ( "Authentication failed. Auth characteristic must not be null." ) ;
}
2024-04-09 12:53:23 +02:00
int success ;
2022-09-06 16:08:19 +02:00
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 ) ;
}
2024-04-09 12:53:23 +02:00
if ( success ! = ERRORCODE_SUCCESS )
2022-09-06 16:08:19 +02:00
{
2024-04-09 12:53:23 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Writing copri seed failed.{AuthCharacteristic}{CommandWritten}{ErrorCode}" , ToSerilogString ( authCharacteristic ) , ToSerilogString ( lockInfo . K_seed ) , success ) ;
2022-09-06 16:08:19 +02:00
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 ) ) ;
2024-04-09 12:53:23 +02:00
( byte [ ] SeedLockEncrypted , int Success ) readResult ;
2022-09-06 16:08:19 +02:00
var cts = new CancellationTokenSource ( ) ;
cts . CancelAfter ( READ_TIMEOUT_MS ) ;
try
{
2024-04-09 12:53:23 +02:00
readResult = await authCharacteristic . ReadAsync ( cts . Token ) ; // encrypted seed value (random value) from lock to decrypt using k_user
2022-09-06 16:08:19 +02:00
}
catch ( System . Exception exception )
{
2023-04-19 12:14:14 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Retrieving encrypted random value from lock (seed)(ReadAsync-call) failed.{ReadCharacteristic}{Exception}" , ToSerilogString ( authCharacteristic ) , exception ) ;
2024-04-09 12:53:23 +02:00
throw new System . Exception (
string . Format ( "Can not authenticate. Reading encrypted seed failed. {0}" , exception . Message ) ,
exception ) ;
2022-09-06 16:08:19 +02:00
}
finally
{
cts . Dispose ( ) ;
}
2024-04-09 12:53:23 +02:00
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." ) ;
}
2023-04-19 12:14:14 +02:00
Log . ForContext < LockItBase > ( ) . Debug ( "Retrieving encrypted random value from lock (seed)(ReadAsync-call) succeeded.{ReadCharacteristic}{Reading}" , ToSerilogString ( authCharacteristic ) , "***" ) ;
2022-09-06 16:08:19 +02:00
var crypto = new AuthCryptoHelper (
2024-04-09 12:53:23 +02:00
readResult . SeedLockEncrypted ,
2022-09-06 16:08:19 +02:00
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 ) ;
}
2024-04-09 12:53:23 +02:00
if ( success ! = ERRORCODE_SUCCESS )
2022-09-06 16:08:19 +02:00
{
2024-04-09 12:53:23 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Writing encrypted access key failed.{Key}{Seed}{AuthCharacteristic}{CommandWritten}{ErrorCode}" , ToSerilogString ( crypto . KeyCopri ) , ToSerilogString ( lockInfo . K_seed ) , ToSerilogString ( authCharacteristic ) , "***" , success ) ;
2022-09-06 16:08:19 +02:00
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>
2023-08-31 12:20:06 +02:00
/// Lock state is first byte of value read from state characteristic ("0000baaa-1212-efde-1523-785fef13d123").
2022-09-06 16:08:19 +02:00
/// Values are as follows
/// Open = 0x00,
/// Closed = 0x01,
/// Unknown = 0x02,
/// CouldntCloseMoving = 0x03,
2023-08-31 12:20:06 +02:00
/// CouldntOpenBoltBlocked = 0x04,
/// CouldntCloseBoltBlocked = 0x05
2024-04-09 12:53:23 +02:00
/// ShareeBike.Services.BluetoothLock.Tdo.LockitLockingState.
2022-09-06 16:08:19 +02:00
/// </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">
2023-04-19 12:14:14 +02:00
/// Call not from main thread or unknown platform detected or
2022-09-06 16:08:19 +02:00
/// 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." ) ;
}
2023-08-31 12:20:06 +02:00
// Reads the lock state from characteristic
2024-04-09 12:53:23 +02:00
async Task < ( byte [ ] , int ) > readAsyncDelegate ( )
2022-09-06 16:08:19 +02:00
{
var cts = new CancellationTokenSource ( ) ;
2024-04-09 12:53:23 +02:00
( byte [ ] State , int Success ) innerReadResult ;
2022-09-06 16:08:19 +02:00
cts . CancelAfter ( READ_TIMEOUT_MS ) ;
try
{
2024-04-09 12:53:23 +02:00
innerReadResult = await stateCharacteristic . ReadAsync ( cts . Token ) ;
2022-09-06 16:08:19 +02:00
}
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 ( ) ;
}
2024-04-09 12:53:23 +02:00
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 ;
2022-09-06 16:08:19 +02:00
}
2024-04-09 12:53:23 +02:00
( byte [ ] State , int Success ) readResult ;
2022-09-06 16:08:19 +02:00
try
{
2024-04-09 12:53:23 +02:00
readResult = doWaitRetry
2022-09-06 16:08:19 +02:00
? 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 ) ;
}
2024-04-09 12:53:23 +02:00
if ( readResult . State = = null | | readResult . State . Length < = 0 )
2022-09-06 16:08:19 +02:00
{
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" ) ;
}
2024-04-09 12:53:23 +02:00
int lockingState = readResult . State [ 0 ] ;
2022-09-06 16:08:19 +02:00
var lockInfoTdo = new LockInfoTdo . Builder
{
Id = Id ,
Guid = Guid ,
2023-08-31 12:20:06 +02:00
State = Enum . IsDefined ( typeof ( LockitLockingState ) , lockingState ) ? ( LockitLockingState ? ) lockingState : null
2022-09-06 16:08:19 +02:00
} . Build ( ) ;
Log . ForContext < LockItBase > ( ) . Debug ( "Retrieving lock state (ReadAsync-call) succeeded. {@LockInfoTdo}{StateCharacteristic}{Reading}" ,
lockInfoTdo ,
ToSerilogString ( stateCharacteristic ) ,
2024-04-09 12:53:23 +02:00
readResult . State ) ;
2022-09-06 16:08:19 +02:00
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." ) ;
}
2024-04-09 12:53:23 +02:00
( byte [ ] Percentage , int Success ) readResult ;
2022-09-06 16:08:19 +02:00
var cts = new CancellationTokenSource ( ) ;
cts . CancelAfter ( READ_TIMEOUT_MS ) ;
try
{
2024-04-09 12:53:23 +02:00
readResult = await batteryCharacteristic . ReadAsync ( cts . Token ) ;
2022-09-06 16:08:19 +02:00
}
catch ( System . Exception exception )
{
Log . ForContext < LockItBase > ( ) . Error ( "Retrieving charging level (ReadAsync-call) failed. {BatteryCharacteristic}{Exception}" , ToSerilogString ( batteryCharacteristic ) , exception ) ;
2024-04-09 12:53:23 +02:00
throw new System . Exception (
string . Format ( "Can not get battery percentage. Reading data failed. {0}" , exception . Message ) ,
exception ) ;
2022-09-06 16:08:19 +02:00
}
finally
{
cts . Dispose ( ) ;
}
2024-04-09 12:53:23 +02:00
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 )
2022-09-06 16:08:19 +02:00
{
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." ) ;
}
2024-04-09 12:53:23 +02:00
Log . ForContext < LockItBase > ( ) . Debug ( "Retrieving charging level (ReadAsync-call) succeeded.{Level}{BatteryCharacteristic}{Reading}" , readResult . Percentage [ 0 ] , ToSerilogString ( batteryCharacteristic ) , readResult . Percentage ) ;
2022-09-20 13:51:55 +02:00
2024-04-09 12:53:23 +02:00
return readResult . Percentage [ 0 ] ;
2022-09-20 13:51:55 +02:00
}
/// <summary> Gets version info about the lock. </summary>
/// <remarks>
2024-04-09 12:53:23 +02:00
/// Lock state is first byte of value read from state characteristic ("0000baaa-1212-efde-1523-785fef13d123").
2022-09-20 13:51:55 +02:00
/// 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." ) ;
}
2022-09-06 16:08:19 +02:00
2024-04-09 12:53:23 +02:00
( byte [ ] Version , int Success ) readResult ;
2022-09-20 13:51:55 +02:00
var cts = new CancellationTokenSource ( ) ;
cts . CancelAfter ( READ_TIMEOUT_MS ) ;
try
{
2024-04-09 12:53:23 +02:00
readResult = await firmwareVersionCharacteristic . ReadAsync ( cts . Token ) ;
2022-09-20 13:51:55 +02:00
}
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 ( ) ;
}
2024-04-09 12:53:23 +02:00
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 )
2022-09-20 13:51:55 +02:00
{
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
{
2024-04-09 12:53:23 +02:00
FirmwareVersion = readResult . Version [ 0 ] ,
LockVersion = readResult . Version [ 1 ] ,
HardwareVersion = readResult . Version [ 2 ]
2022-09-20 13:51:55 +02:00
} . Build ( ) ;
Log . ForContext < LockItBase > ( ) . Debug ( "Retrieving versions info (ReadAsync-call) succeeded. {@LockInfoTdo}{StateCharacteristic}{Reading}" ,
VersionInfo ,
ToSerilogString ( firmwareVersionCharacteristic ) ,
2024-04-09 12:53:23 +02:00
readResult . Version ) ;
2022-09-20 13:51:55 +02:00
return VersionInfo ;
2022-09-06 16:08:19 +02:00
}
2022-09-20 13:51:55 +02:00
public VersionInfoTdo VersionInfo { get ; private set ; }
2022-09-06 16:08:19 +02:00
/// <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
2023-04-19 12:14:14 +02:00
? "Encrypting command to open lock failed. {Exception}"
: "Encrypting command to close lock failed. {Exception}" ,
2022-09-06 16:08:19 +02:00
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 ) ;
}
2024-04-09 12:53:23 +02:00
int success ;
2022-09-06 16:08:19 +02:00
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 ) ;
}
2024-04-09 12:53:23 +02:00
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 ;
}
2022-09-06 16:08:19 +02:00
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 ) ) ;
2024-04-09 12:53:23 +02:00
return true ;
2022-09-06 16:08:19 +02:00
}
/// <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." ) ;
}
2024-04-09 12:53:23 +02:00
( byte [ ] AlarmSettings , int Success ) readResult ;
2022-09-06 16:08:19 +02:00
var cts = new CancellationTokenSource ( ) ;
cts . CancelAfter ( READ_TIMEOUT_MS ) ;
try
{
2024-04-09 12:53:23 +02:00
readResult = await alarmCharacteristic . ReadAsync ( cts . Token ) ;
2022-09-06 16:08:19 +02:00
}
catch ( System . Exception exception )
{
Log . ForContext < LockItBase > ( ) . Error ( "Retrieving alarm settings (ReadAsync-call) failed.{AlarmCharacteristic}{Exception}" , ToSerilogString ( alarmCharacteristic ) , exception ) ;
2024-04-09 12:53:23 +02:00
throw new System . Exception (
$"Can not get whether alarm is off or on {exception.Message}." ,
exception ) ;
2022-09-06 16:08:19 +02:00
}
finally
{
cts . Dispose ( ) ;
}
2024-04-09 12:53:23 +02:00
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 )
2022-09-06 16:08:19 +02:00
{
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." ) ;
}
2024-04-09 12:53:23 +02:00
var isAlarmOff = readResult . AlarmSettings [ 0 ] = = 0 ;
Log . ForContext < LockItBase > ( ) . Debug ( "Retrieving alarm settings (ReadAsync-call) succeeded.{IsArlarmOff}{AlarmCharacteristic}{Reading}" , isAlarmOff , ToSerilogString ( alarmCharacteristic ) , readResult . AlarmSettings ) ;
2022-09-06 16:08:19 +02:00
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." ) ;
}
2024-04-09 12:53:23 +02:00
int success ;
2022-09-06 16:08:19 +02:00
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 ) ;
}
2024-04-09 12:53:23 +02:00
if ( success ! = ERRORCODE_SUCCESS )
2022-09-06 16:08:19 +02:00
{
2024-04-09 12:53:23 +02:00
Log . ForContext < LockItBase > ( ) . Error ( "Writing alarm settings failed.{AlarmCharacteristic}{CommandWritten}{ErrorCode}" , ToSerilogString ( alarmCharacteristic ) , command [ 0 ] , success ) ;
2022-09-06 16:08:19 +02:00
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 ;
}
2024-04-09 12:53:23 +02:00
int success ;
2022-09-06 16:08:19 +02:00
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 ) ;
}
2024-04-09 12:53:23 +02:00
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 ) ;
2022-09-06 16:08:19 +02:00
2024-04-09 12:53:23 +02:00
return true ;
2022-09-06 16:08:19 +02:00
}
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 ;
}
2024-04-09 12:53:23 +02:00
int success ;
2022-09-06 16:08:19 +02:00
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 ) ;
}
2024-04-09 12:53:23 +02:00
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 ) ;
2022-09-06 16:08:19 +02:00
2024-04-09 12:53:23 +02:00
return true ;
2022-09-06 16:08:19 +02:00
}
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 )
2023-08-31 12:20:06 +02:00
= > "***" ; // For debugging purposes it might be required to return string.Join(",", byteArray); Do not log any confidential value in production context.
2022-09-06 16:08:19 +02:00
}
2021-05-13 17:25:46 +02:00
}