From 6bab491a21e1a8312b39e67753d3582010748f61 Mon Sep 17 00:00:00 2001 From: Oliver Hauff Date: Thu, 13 May 2021 17:07:16 +0200 Subject: [PATCH] Initial version. --- .gitattributes | 63 +++++ .gitignore | 261 ++++++++++++++++++ LockItShared/LockItShared.csproj | 42 +++ LockItShared/LockItShared.sln | 25 ++ .../Bikes/Bike/BluetoothLock/ILockInfo.cs | 11 + .../Bikes/Bike/BluetoothLock/LockInfo.cs | 127 +++++++++ .../Model/Connector/TextToLockItTypeHelper.cs | 32 +++ LockItShared/Model/Device/ICipher.cs | 17 ++ .../MultilingualResources/LockItShared.de.xlf | 45 +++ .../Resources.Designer.cs | 135 +++++++++ .../MultilingualResources/Resources.de.resx | 39 +++ .../MultilingualResources/Resources.resx | 144 ++++++++++ .../BluetoothLock/Crypto/AuthCryptoHelper.cs | 82 ++++++ .../Services/BluetoothLock/Crypto/Cipher.cs | 88 ++++++ .../Exception/AlreadyConnectedException.cs | 7 + .../Exception/AuthKeyException.cs | 8 + .../BluetoothDisconnectedException.cs | 11 + .../CouldntCloseBoldBlockedException.cs | 13 + .../CouldntCloseInconsistentStateExecption.cs | 18 ++ .../CouldntOpenBoldBlockedException.cs | 13 + .../CouldntOpenInconsistentStateExecption.cs | 18 ++ .../CoundntGetCharacteristicException.cs | 8 + .../Exception/CounldntCloseMovingException.cs | 14 + .../Exception/GuidUnknownException.cs | 9 + .../Exception/OutOfReachException.cs | 7 + .../Exception/StateAwareException.cs | 15 + .../Services/BluetoothLock/ILockService.cs | 58 ++++ .../Services/BluetoothLock/ILocksService.cs | 58 ++++ .../Services/BluetoothLock/ITimeOutProvide.cs | 11 + .../Services/BluetoothLock/LockInfoHelper.cs | 70 +++++ .../LockItByGuidServiceHelper.cs | 21 ++ .../Services/BluetoothLock/NullLock.cs | 60 ++++ .../BluetoothLock/Tdo/LockInfoAuthTdo.cs | 61 ++++ .../Services/BluetoothLock/Tdo/LockInfoTdo.cs | 53 ++++ .../Services/BluetoothLock/TimeOutProvider.cs | 30 ++ ...tCouldntCloseInconsistentStateExecption.cs | 29 ++ ...stCouldntOpenInconsistentStateExecption.cs | 29 ++ .../BluetoothLock/TestTimeOutProvider.cs | 28 ++ TestLockItShared/TestLockItShared.csproj | 21 ++ TestLockItShared/TestLockItShared.sln | 31 +++ 40 files changed, 1812 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LockItShared/LockItShared.csproj create mode 100644 LockItShared/LockItShared.sln create mode 100644 LockItShared/Model/Bikes/Bike/BluetoothLock/ILockInfo.cs create mode 100644 LockItShared/Model/Bikes/Bike/BluetoothLock/LockInfo.cs create mode 100644 LockItShared/Model/Connector/TextToLockItTypeHelper.cs create mode 100644 LockItShared/Model/Device/ICipher.cs create mode 100644 LockItShared/MultilingualResources/LockItShared.de.xlf create mode 100644 LockItShared/MultilingualResources/Resources.Designer.cs create mode 100644 LockItShared/MultilingualResources/Resources.de.resx create mode 100644 LockItShared/MultilingualResources/Resources.resx create mode 100644 LockItShared/Services/BluetoothLock/Crypto/AuthCryptoHelper.cs create mode 100644 LockItShared/Services/BluetoothLock/Crypto/Cipher.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/AlreadyConnectedException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/AuthKeyException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/BluetoothDisconnectedException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/CouldntCloseBoldBlockedException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/CouldntCloseInconsistentStateExecption.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/CouldntOpenBoldBlockedException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/CouldntOpenInconsistentStateExecption.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/CoundntGetCharacteristicException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/CounldntCloseMovingException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/GuidUnknownException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/OutOfReachException.cs create mode 100644 LockItShared/Services/BluetoothLock/Exception/StateAwareException.cs create mode 100644 LockItShared/Services/BluetoothLock/ILockService.cs create mode 100644 LockItShared/Services/BluetoothLock/ILocksService.cs create mode 100644 LockItShared/Services/BluetoothLock/ITimeOutProvide.cs create mode 100644 LockItShared/Services/BluetoothLock/LockInfoHelper.cs create mode 100644 LockItShared/Services/BluetoothLock/LockItByGuidServiceHelper.cs create mode 100644 LockItShared/Services/BluetoothLock/NullLock.cs create mode 100644 LockItShared/Services/BluetoothLock/Tdo/LockInfoAuthTdo.cs create mode 100644 LockItShared/Services/BluetoothLock/Tdo/LockInfoTdo.cs create mode 100644 LockItShared/Services/BluetoothLock/TimeOutProvider.cs create mode 100644 TestLockItShared/Services/BluetoothLock/Exception/TestCouldntCloseInconsistentStateExecption.cs create mode 100644 TestLockItShared/Services/BluetoothLock/Exception/TestCouldntOpenInconsistentStateExecption.cs create mode 100644 TestLockItShared/Services/BluetoothLock/TestTimeOutProvider.cs create mode 100644 TestLockItShared/TestLockItShared.csproj create mode 100644 TestLockItShared/TestLockItShared.sln diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c4efe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,261 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/LockItShared/LockItShared.csproj b/LockItShared/LockItShared.csproj new file mode 100644 index 0000000..d8df954 --- /dev/null +++ b/LockItShared/LockItShared.csproj @@ -0,0 +1,42 @@ + + + + 4.0 + en-GB + true + true + + + netstandard2.0 + TINK + en-GB + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + \ No newline at end of file diff --git a/LockItShared/LockItShared.sln b/LockItShared/LockItShared.sln new file mode 100644 index 0000000..c2e25b5 --- /dev/null +++ b/LockItShared/LockItShared.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31229.75 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LockItShared", "LockItShared.csproj", "{C4194BC7-22CF-4F1C-B5F8-E75F9F552E34}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C4194BC7-22CF-4F1C-B5F8-E75F9F552E34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4194BC7-22CF-4F1C-B5F8-E75F9F552E34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4194BC7-22CF-4F1C-B5F8-E75F9F552E34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4194BC7-22CF-4F1C-B5F8-E75F9F552E34}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9539C371-F0FB-47C0-B1B3-76875B4923FB} + EndGlobalSection +EndGlobal diff --git a/LockItShared/Model/Bikes/Bike/BluetoothLock/ILockInfo.cs b/LockItShared/Model/Bikes/Bike/BluetoothLock/ILockInfo.cs new file mode 100644 index 0000000..c2366f1 --- /dev/null +++ b/LockItShared/Model/Bikes/Bike/BluetoothLock/ILockInfo.cs @@ -0,0 +1,11 @@ +namespace TINK.Model.Bike.BluetoothLock +{ + public interface ILockInfo + { + /// Identification number of bluetooth lock. + int Id { get; } + + /// Gets the user key. + byte[] UserKey { get; } + } +} diff --git a/LockItShared/Model/Bikes/Bike/BluetoothLock/LockInfo.cs b/LockItShared/Model/Bikes/Bike/BluetoothLock/LockInfo.cs new file mode 100644 index 0000000..ca29ef3 --- /dev/null +++ b/LockItShared/Model/Bikes/Bike/BluetoothLock/LockInfo.cs @@ -0,0 +1,127 @@ +using TINK.Model.Connector; +using Newtonsoft.Json; +using System; +using System.Runtime.Serialization; + +namespace TINK.Model.Bike.BluetoothLock +{ + /// Locking states. + public enum LockingState + { + Disconnected, + + /// Lock might be open, closed or something in between.. + Unknown, + + /// Lock is closed. + Closed, + + /// Lock is open. + Open + } + + [DataContract] + public class LockInfo : ILockInfo, IEquatable + { + /// Identification number of bluetooth lock, 6-digits, second part of advertisement name. + [DataMember] + public int Id { get; private set; } = TextToLockItTypeHelper.INVALIDLOCKID; + + /// Identification GUID of bluetooth lock. + [DataMember] + public Guid Guid { get; private set; } = TextToLockItTypeHelper.INVALIDLOCKGUID; + + [DataMember] + public byte[] UserKey { get; private set; } + + [DataMember] + public byte[] AdminKey { get; private set; } + + [DataMember] + public byte[] Seed { get; private set; } + + /// Locking state of bluetooth lock. + [DataMember] + public LockingState State { get; private set; } = LockingState.Disconnected; + + public bool IsIdValid => Id != TextToLockItTypeHelper.INVALIDLOCKID; + + public bool IsGuidValid => Guid != TextToLockItTypeHelper.INVALIDLOCKGUID; + + public override bool Equals(object obj) => this.Equals(obj as LockInfo); + + public bool Equals(LockInfo other) + { + if (Object.ReferenceEquals(other, null)) return false; + if (Object.ReferenceEquals(this, other)) return true; + if (this.GetType() != other.GetType()) return false; + + return ToString() == other.ToString(); + } + + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } + + public static bool operator ==(LockInfo lhs, LockInfo rhs) + { + if (Object.ReferenceEquals(lhs, null)) + return Object.ReferenceEquals(rhs, null) ? true /*null == null = true*/: false; + + return lhs.Equals(rhs); + } + + public static bool operator !=(LockInfo lhs, LockInfo rhs) + { + return !(lhs == rhs); + } + + public class Builder + { + public Builder(LockInfo lockInfo = null) + { + if (lockInfo == null) + { + return; + } + + LockInfo = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(lockInfo)); + } + + private readonly LockInfo LockInfo = new LockInfo(); + + public byte[] UserKey { get => LockInfo.UserKey; set => LockInfo.UserKey = value; } + + public byte[] AdminKey { get => LockInfo.AdminKey; set => LockInfo.AdminKey = value; } + + public byte[] Seed { get => LockInfo.Seed; set => LockInfo.Seed = value; } + + public int Id { get => LockInfo.Id; set => LockInfo.Id = value; } + + public Guid Guid { get => LockInfo.Guid; set => LockInfo.Guid = value; } + + public LockingState State { get => LockInfo.State; set => LockInfo.State = value; } + + public LockInfo Build() + { + // Ensure consistency. + if ((UserKey?.Length > 0 || Seed?.Length > 0) + && (UserKey?.Length == 0 || Seed?.Length == 0 || !LockInfo.IsIdValid)) + throw new ArgumentException($"Can not build {typeof(LockInfo).Name}. Lock parameters must either be all know or all unknown."); + + if (UserKey == null) UserKey = new byte[0]; + if (AdminKey == null) AdminKey = new byte[0]; + if (Seed == null) Seed = new byte[0]; + + return LockInfo; + } + } + + } +} diff --git a/LockItShared/Model/Connector/TextToLockItTypeHelper.cs b/LockItShared/Model/Connector/TextToLockItTypeHelper.cs new file mode 100644 index 0000000..a3fc465 --- /dev/null +++ b/LockItShared/Model/Connector/TextToLockItTypeHelper.cs @@ -0,0 +1,32 @@ +using System; +using System.Text.RegularExpressions; + +namespace TINK.Model.Connector +{ + public static class TextToLockItTypeHelper + { + /// Lock id which representing a non valid id. + public const int INVALIDLOCKID = 0; + + /// Lock GUID which representing a non valid id. + public readonly static Guid INVALIDLOCKGUID = new Guid(); + + /// First part of advertisement name. + public static string ISHAREITADVERTISMENTTITLE = "ISHAREIT"; + + /// Gets the ID part from advertisment name. + /// Advertisement name is made up of name plus separator (+ or -) and a ID + /// Advertisment name to extract info from. + /// From information. + public static int GetBluetoothLockId(this string advertisementName) + { + var name = advertisementName?.ToUpper(); + if (string.IsNullOrEmpty(name)) + return INVALIDLOCKID; + + return int.TryParse(Regex.Replace(advertisementName, $"{ISHAREITADVERTISMENTTITLE}[\\-,\\+ ]", ""), out int lockId) + ? lockId + : INVALIDLOCKID; + } + } +} diff --git a/LockItShared/Model/Device/ICipher.cs b/LockItShared/Model/Device/ICipher.cs new file mode 100644 index 0000000..79e4369 --- /dev/null +++ b/LockItShared/Model/Device/ICipher.cs @@ -0,0 +1,17 @@ +namespace TINK.Model.Device +{ + public interface ICipher + { + /// Encrypt data. + /// Key to encrypt data. + /// Data to entrycpt. + /// + byte[] Encrypt(byte[] key, byte[] clear); + + /// Decrypt data. + /// Key to decrypt data. + /// Encrpyted data to decrypt. + /// + byte[] Decrypt(byte[] key, byte[] encrypted); + } +} diff --git a/LockItShared/MultilingualResources/LockItShared.de.xlf b/LockItShared/MultilingualResources/LockItShared.de.xlf new file mode 100644 index 0000000..6fe319c --- /dev/null +++ b/LockItShared/MultilingualResources/LockItShared.de.xlf @@ -0,0 +1,45 @@ + + + +
+ +
+ + + + Unexpected locking state "{0}" detected after sending close command. + Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführung des Abschließen-Befehls. + + + Lock reports unknown bold position. + Schloss meldet unbekannten Schließzustand. + + + Unexpected locking state "{0}" detected after sending open command. + Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführen des Öffnen-Befehls. + + + Lock reports unknown bold position. + Schloss meldet unbekannten Schließzustand. + Please verify the translation’s accuracy as the source string was updated after it was translated. + + + No bluetooth connection. + Keine Bluetooth-Verbindung. + + + Bike is moving. + Rad ist in Bewegung. + + + Bold is blocked. + Schloss ist blockiert. + + + Bold is blocked. + Schloss ist blockiert. + + + +
+
\ No newline at end of file diff --git a/LockItShared/MultilingualResources/Resources.Designer.cs b/LockItShared/MultilingualResources/Resources.Designer.cs new file mode 100644 index 0000000..1d94ff4 --- /dev/null +++ b/LockItShared/MultilingualResources/Resources.Designer.cs @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace TINK.MultilingualResources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TINK.MultilingualResources.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to No bluetooth connection.. + /// + internal static string ErrorBluetoothDisconnectedException { + get { + return ResourceManager.GetString("ErrorBluetoothDisconnectedException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bike is moving.. + /// + internal static string ErrorCloseLockBikeMoving { + get { + return ResourceManager.GetString("ErrorCloseLockBikeMoving", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bold is blocked.. + /// + internal static string ErrorCloseLockBoldBlocked { + get { + return ResourceManager.GetString("ErrorCloseLockBoldBlocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected locking state "{0}" detected after sending close command.. + /// + internal static string ErrorCloseLockUnexpectedState { + get { + return ResourceManager.GetString("ErrorCloseLockUnexpectedState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock reports unknown bold position.. + /// + internal static string ErrorCloseLockUnknownPosition { + get { + return ResourceManager.GetString("ErrorCloseLockUnknownPosition", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bold is blocked.. + /// + internal static string ErrorOpenLockBoldBlocked { + get { + return ResourceManager.GetString("ErrorOpenLockBoldBlocked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected locking state "{0}" detected after sending open command.. + /// + internal static string ErrorOpenLockUnexpectedState { + get { + return ResourceManager.GetString("ErrorOpenLockUnexpectedState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock reports unknown bold position.. + /// + internal static string ErrorOpenLockUnknownPosition { + get { + return ResourceManager.GetString("ErrorOpenLockUnknownPosition", resourceCulture); + } + } + } +} diff --git a/LockItShared/MultilingualResources/Resources.de.resx b/LockItShared/MultilingualResources/Resources.de.resx new file mode 100644 index 0000000..ad7fac7 --- /dev/null +++ b/LockItShared/MultilingualResources/Resources.de.resx @@ -0,0 +1,39 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführung des Abschließen-Befehls. + + + Schloss meldet unbekannten Schließzustand. + + + Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführen des Öffnen-Befehls. + + + Schloss meldet unbekannten Schließzustand. + + + Keine Bluetooth-Verbindung. + + + Rad ist in Bewegung. + + + Schloss ist blockiert. + + + Schloss ist blockiert. + + \ No newline at end of file diff --git a/LockItShared/MultilingualResources/Resources.resx b/LockItShared/MultilingualResources/Resources.resx new file mode 100644 index 0000000..b1c2481 --- /dev/null +++ b/LockItShared/MultilingualResources/Resources.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + No bluetooth connection. + + + Bike is moving. + + + Bold is blocked. + + + Unexpected locking state "{0}" detected after sending close command. + + + Lock reports unknown bold position. + + + Bold is blocked. + + + Unexpected locking state "{0}" detected after sending open command. + + + Lock reports unknown bold position. + + \ No newline at end of file diff --git a/LockItShared/Services/BluetoothLock/Crypto/AuthCryptoHelper.cs b/LockItShared/Services/BluetoothLock/Crypto/AuthCryptoHelper.cs new file mode 100644 index 0000000..7f05cc0 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Crypto/AuthCryptoHelper.cs @@ -0,0 +1,82 @@ +using Serilog; +using TINK.Model.Device; + +namespace TINK.Services.BluetoothLock.Crypto +{ + public class AuthCryptoHelper + { + private ICipher Cipher { get; } + + /// Encrypted seed (random number) created inside ILOCKIT and passd to app. + private byte[] SeedLockEncrypted { get; } + + /// Contstructs a auth crypto helper object. + /// Encrypted seed to deocode using . + /// Key used to to decrypt . + public AuthCryptoHelper( + byte[] seedLockEncrypted, + byte[] keyCopri, + ICipher cipher) + { + KeyCopri = keyCopri; + SeedLockEncrypted = seedLockEncrypted; + Cipher = cipher ?? new Cipher(); + } + + + /// Public for testing purposes only. + public byte[] GetSeedLock() + { + byte[] seedLockDecrypted; + var seedLockEncrypted = SeedLockEncrypted; + var keyCopri = KeyCopri; + try + { + seedLockDecrypted = Cipher.Decrypt( + keyCopri, + seedLockEncrypted); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Decrypting seed from lock failed. {Exception}", exception); + throw; + } + + Log.ForContext().Verbose($"Lock random number decrypted from {string.Join(",", seedLockEncrypted)} to {string.Join(",", seedLockDecrypted)} using {string.Join(", ", keyCopri)}."); + return seedLockDecrypted; + } + + public byte[] GetAccessKeyEncrypted() + { + + var accessKey = GetSeedLock(); + + if (accessKey == null || accessKey.Length <= 0) + { + Log.ForContext().Error("Creating access key failed, Key must not be null or empty."); + throw new System.Exception(); + } + + accessKey[accessKey.Length - 1] += 1; + + var keyCopri = KeyCopri; + byte[] acccessKeyEncrypted; + try + { + acccessKeyEncrypted = Cipher.Encrypt( + keyCopri, + accessKey); + } + catch (System.Exception exception) + { + Log.ForContext().Error("Encrypting access key failed. {Exception}", exception); + throw; + } + + Log.ForContext().Verbose($"Access key encrypted from {string.Join(",", accessKey)} to {string.Join(",", acccessKeyEncrypted)} using {string.Join(", ", keyCopri)}."); + return acccessKeyEncrypted; + } + + public byte[] KeyCopri { get; } + } +} diff --git a/LockItShared/Services/BluetoothLock/Crypto/Cipher.cs b/LockItShared/Services/BluetoothLock/Crypto/Cipher.cs new file mode 100644 index 0000000..fdc69c2 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Crypto/Cipher.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using TINK.Model.Device; + +namespace TINK.Services.BluetoothLock.Crypto +{ + public class Cipher : ICipher + { + /// Decrypt data. + /// + /// Further info see: + /// https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=netcore-3.1 for further info + // https://docs.microsoft.com/en-us/dotnet/standard/security/cryptographic-services + // https://stackoverflow.com/questions/24903575/how-to-return-byte-when-decrypt-using-cryptostream-descryptoserviceprovider/24903689 + + /// + /// Key to decrypt data with. + /// Encrpyted data to decrypt. + /// Decrypted data. + public byte[] Decrypt(byte[] key, byte[] encrypted) + { + // Check arguments. + if (encrypted == null || encrypted.Length <= 0) + throw new ArgumentNullException(nameof(encrypted)); + + if (key == null || key.Length <= 0) + throw new ArgumentNullException(nameof(key)); + + using (Aes aesAlg = Aes.Create()) + { + aesAlg.KeySize = 192; + aesAlg.Mode = CipherMode.ECB; + aesAlg.Padding = PaddingMode.None; + aesAlg.Key = key; + + // Create a decryptor to perform the stream transform. + ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + + // Create the streams used for decryption. + using (var msDecrypt = new MemoryStream()) + { + using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write)) + { + csDecrypt.Write(encrypted, 0, encrypted.Length); + csDecrypt.FlushFinalBlock(); + return msDecrypt.ToArray(); + } + } + } + } + + public byte[] Encrypt(byte[] key, byte[] clear) + { + // Check arguments. + if (clear == null || clear.Length <= 0) + throw new ArgumentNullException("plainText"); + + if (key == null || key.Length <= 0) + throw new ArgumentNullException("Key"); + + // Create an AesCryptoServiceProvider object + // with the specified key and IV. + using (AesCryptoServiceProvider aesAlg = new AesCryptoServiceProvider()) + { + aesAlg.KeySize = 192; + aesAlg.Mode = CipherMode.ECB; + aesAlg.Padding = PaddingMode.None; + aesAlg.Key = key; + + // Create an encryptor to perform the stream transform. + ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + + // Create the streams used for encryption. + using (var msEncrypt = new MemoryStream()) + { + using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) + { + csEncrypt.Write(clear, 0, clear.Length); + csEncrypt.FlushFinalBlock(); + return msEncrypt.ToArray(); + } + } + } + + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/AlreadyConnectedException.cs b/LockItShared/Services/BluetoothLock/Exception/AlreadyConnectedException.cs new file mode 100644 index 0000000..4ae287a --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/AlreadyConnectedException.cs @@ -0,0 +1,7 @@ +namespace TINK.Services.BluetoothLock.Exception +{ + public class AlreadyConnectedException : System.Exception + { + public AlreadyConnectedException() : base("Invalid reconnect call detected. Device is already connected.") {} + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/AuthKeyException.cs b/LockItShared/Services/BluetoothLock/Exception/AuthKeyException.cs new file mode 100644 index 0000000..d5646f5 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/AuthKeyException.cs @@ -0,0 +1,8 @@ +namespace TINK.Services.BluetoothLock.Exception +{ + public class AuthKeyException : System.Exception + { + public AuthKeyException(string message) : base(message) + { } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/BluetoothDisconnectedException.cs b/LockItShared/Services/BluetoothLock/Exception/BluetoothDisconnectedException.cs new file mode 100644 index 0000000..efd768b --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/BluetoothDisconnectedException.cs @@ -0,0 +1,11 @@ +using TINK.Model.Bike.BluetoothLock; + +namespace TINK.Services.BluetoothLock.Exception +{ + public class BluetoothDisconnectedException : StateAwareException + { + public BluetoothDisconnectedException() : base(LockingState.Disconnected, MultilingualResources.Resources.ErrorBluetoothDisconnectedException) + { + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/CouldntCloseBoldBlockedException.cs b/LockItShared/Services/BluetoothLock/Exception/CouldntCloseBoldBlockedException.cs new file mode 100644 index 0000000..cc5709d --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/CouldntCloseBoldBlockedException.cs @@ -0,0 +1,13 @@ +using TINK.Model.Bike.BluetoothLock; + +namespace TINK.Services.BluetoothLock.Exception +{ + public class CouldntCloseBoldBlockedException : StateAwareException + { + public CouldntCloseBoldBlockedException() : base( + LockingState.Unknown, // Lock is closed in most cases, but this is not guarnteed according to haveltec. + MultilingualResources.Resources.ErrorCloseLockBoldBlocked) + { + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/CouldntCloseInconsistentStateExecption.cs b/LockItShared/Services/BluetoothLock/Exception/CouldntCloseInconsistentStateExecption.cs new file mode 100644 index 0000000..2ca8259 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/CouldntCloseInconsistentStateExecption.cs @@ -0,0 +1,18 @@ + +using TINK.Model.Bike.BluetoothLock; +using TINK.MultilingualResources; + +namespace TINK.Services.BluetoothLock.Exception +{ + public class CouldntCloseInconsistentStateExecption : StateAwareException + { + public CouldntCloseInconsistentStateExecption(LockingState state) : + base( + state, + state != LockingState.Unknown + ? string.Format(Resources.ErrorCloseLockUnexpectedState, state) + : Resources.ErrorCloseLockUnknownPosition) + { + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/CouldntOpenBoldBlockedException.cs b/LockItShared/Services/BluetoothLock/Exception/CouldntOpenBoldBlockedException.cs new file mode 100644 index 0000000..5dd83c2 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/CouldntOpenBoldBlockedException.cs @@ -0,0 +1,13 @@ +using TINK.Model.Bike.BluetoothLock; + +namespace TINK.Services.BluetoothLock.Exception +{ + public class CouldntOpenBoldBlockedException : StateAwareException + { + public CouldntOpenBoldBlockedException() : base( + LockingState.Unknown, // + MultilingualResources.Resources.ErrorOpenLockBoldBlocked) + { + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/CouldntOpenInconsistentStateExecption.cs b/LockItShared/Services/BluetoothLock/Exception/CouldntOpenInconsistentStateExecption.cs new file mode 100644 index 0000000..059e46a --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/CouldntOpenInconsistentStateExecption.cs @@ -0,0 +1,18 @@ + +using TINK.Model.Bike.BluetoothLock; +using TINK.MultilingualResources; + +namespace TINK.Services.BluetoothLock.Exception +{ + public class CouldntOpenInconsistentStateExecption : StateAwareException + { + public CouldntOpenInconsistentStateExecption(LockingState state) : + base( + state, + state != LockingState.Unknown + ? string.Format(Resources.ErrorOpenLockUnexpectedState, state) + : Resources.ErrorOpenLockUnknownPosition) + { + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/CoundntGetCharacteristicException.cs b/LockItShared/Services/BluetoothLock/Exception/CoundntGetCharacteristicException.cs new file mode 100644 index 0000000..abed7bf --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/CoundntGetCharacteristicException.cs @@ -0,0 +1,8 @@ +namespace TINK.Services.BluetoothLock.Exception +{ + public class CoundntGetCharacteristicException : System.Exception + { + public CoundntGetCharacteristicException(string message) : base(message) + { } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/CounldntCloseMovingException.cs b/LockItShared/Services/BluetoothLock/Exception/CounldntCloseMovingException.cs new file mode 100644 index 0000000..d089524 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/CounldntCloseMovingException.cs @@ -0,0 +1,14 @@ +using TINK.Model.Bike.BluetoothLock; +using TINK.Services.BluetoothLock.Tdo; + +namespace TINK.Services.BluetoothLock.Exception +{ + public class CounldntCloseMovingException : StateAwareException + { + public CounldntCloseMovingException() : base( + LockingState.Open, // Locking bold is probable (according to haveltec) still open. + MultilingualResources.Resources.ErrorCloseLockBikeMoving) + { + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/GuidUnknownException.cs b/LockItShared/Services/BluetoothLock/Exception/GuidUnknownException.cs new file mode 100644 index 0000000..c862e96 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/GuidUnknownException.cs @@ -0,0 +1,9 @@ +namespace TINK.Services.BluetoothLock.Exception +{ + public class GuidUnknownException : System.Exception + { + public GuidUnknownException() : base("Can not connect to lock. No Guid available.") + { + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/OutOfReachException.cs b/LockItShared/Services/BluetoothLock/Exception/OutOfReachException.cs new file mode 100644 index 0000000..7f3134a --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/OutOfReachException.cs @@ -0,0 +1,7 @@ +namespace TINK.Services.BluetoothLock.Exception +{ + /// Thrown whenever lock is out of reach. + public class OutOfReachException : System.Exception + { + } +} diff --git a/LockItShared/Services/BluetoothLock/Exception/StateAwareException.cs b/LockItShared/Services/BluetoothLock/Exception/StateAwareException.cs new file mode 100644 index 0000000..77cdc98 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Exception/StateAwareException.cs @@ -0,0 +1,15 @@ +using TINK.Model.Bike.BluetoothLock; + +namespace TINK.Services.BluetoothLock.Exception +{ + public abstract class StateAwareException : System.Exception + { + public StateAwareException(LockingState state, string description) : base(description) + { + State = state; + } + + /// Holds the state reported by lock. + public LockingState State { get; } + } +} diff --git a/LockItShared/Services/BluetoothLock/ILockService.cs b/LockItShared/Services/BluetoothLock/ILockService.cs new file mode 100644 index 0000000..0e4e940 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/ILockService.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using TINK.Services.BluetoothLock.Tdo; + +namespace TINK.Services.BluetoothLock +{ + public enum DeviceState + { + Disconnected = 0, + Connecting = 1, + Connected = 2, + Limited = 3 /* Mactches Connecting? */ + } + + public interface ILockService + { + /// Reconnect to lock. + Task ReconnectAsync( + LockInfoAuthTdo authInfo, + TimeSpan connectTimeout); + + /// Opens lock. + /// State of lock after performing open operation. + Task OpenAsync(); + + /// Closes lock. + /// State of lock after performing close operation. + Task CloseAsync(); + + string Name { get; } + + Guid Guid { get; } + + int Id { get; } + + /// Gets the device state. + DeviceState? GetDeviceState(); + + /// + /// Gets the locking state. + /// + /// True if to wait and retry in case of failures. + /// + Task GetLockStateAsync(bool doWaitRetry = false); + + Task SetSoundAsync(SoundSettings settings); + + Task GetIsAlarmOffAsync(); + + Task SetIsAlarmOffAsync(bool isActivated); + + /// Gets the battery percentage. + Task GetBatteryPercentageAsync(); + + /// Disconnect from lock. + Task Disconnect(); + } +} diff --git a/LockItShared/Services/BluetoothLock/ILocksService.cs b/LockItShared/Services/BluetoothLock/ILocksService.cs new file mode 100644 index 0000000..5511629 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/ILocksService.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TINK.Model.Bike.BluetoothLock; +using TINK.Services.BluetoothLock.Tdo; + +namespace TINK.Services.BluetoothLock +{ + public interface ILocksService + { + /// Holds timeout values for series of connecting attemps to a lock or multiple locks. + ITimeOutProvider TimeOut { get; set; } + + /// Gets lock info for all lock in reach./// + /// Timeout for connect operation of a single lock. + Task> GetLocksStateAsync( + IEnumerable locksInfo, + TimeSpan connectTimeout); + + /// Connects to lock. + /// Info required to connect to lock. + /// Timeout for connect operation. + Task ConnectAsync( + LockInfoAuthTdo authInfo, + TimeSpan connectTimeout); + + /// Gets a lock by bike Id. + /// + /// Lock object + ILockService this[int bikeId] { get; } + + /// Disconnects lock. + /// Id of lock to disconnect. + /// Guid of lock to disconnect. + /// State disconnected it lock is already disconneced or after successfully disconnecting. + Task DisconnectAsync(int bikeId, Guid bikeGuid); + } + + /// Sound types. + /// + /// 1. Locking process started: One long beep. + /// 2. Unlocking successful: One short beep. + /// 3. Bike moved while trying to lock: Three short beeps. + /// 4. Locking bolt blocked while locking: Three short beeps. + /// 5. Unable to unlock because locking bolt is blocked: Three short beeps. + /// + public enum SoundSettings + { + AllButOpenedSuccessfully, // Sounds: 1, 3, 4, 5 + MovingBlocked = 1, // Sounds: 3, 4, 5 + LockingStarted = 2, // Sounds: 1 + AllOff = 3, // Mute + OpenedSuccessfully = 4, // Sounds: 2 + LockingStartedOpenedSuccessfully = 5, // Sounds: 1, 2 + AllOn = 6, // All sounds on + AllButLockingStarted = 7, // Sounds: 2, 3, 4, 5 + } +} diff --git a/LockItShared/Services/BluetoothLock/ITimeOutProvide.cs b/LockItShared/Services/BluetoothLock/ITimeOutProvide.cs new file mode 100644 index 0000000..ffd9417 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/ITimeOutProvide.cs @@ -0,0 +1,11 @@ +using System; + +namespace TINK.Services.BluetoothLock +{ + public interface ITimeOutProvider + { + TimeSpan MultiConnect { get; } + + TimeSpan GetSingleConnect(int countOfTry); + } +} diff --git a/LockItShared/Services/BluetoothLock/LockInfoHelper.cs b/LockItShared/Services/BluetoothLock/LockInfoHelper.cs new file mode 100644 index 0000000..9b79263 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/LockInfoHelper.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using TINK.Services.BluetoothLock.Tdo; + +namespace TINK.Model.Bike.BluetoothLock +{ + public static class LockInfoHelper + { + /// Update by id. + /// Lock info objects to update by id. + /// Tdos holding data to updat from + /// + public static IEnumerable UpdateById( + this IEnumerable locksInfo, + IEnumerable locksInfoTdo) + { + var locksInfoUpdated = new List(); + + foreach (var lockInfo in locksInfo) + { + var lockInfoTdo = locksInfoTdo.FirstOrDefault(x => x.Id == lockInfo.Id); + if (lockInfoTdo == null) + { + // No object to update from found. + locksInfoUpdated.Add(lockInfo); + continue; + } + + var state = lockInfoTdo.State.HasValue ? lockInfoTdo.State.Value.GetLockingState() : LockingState.Disconnected; + + locksInfoUpdated.Add(state != lockInfo.State || lockInfoTdo.Guid != lockInfo.Guid + ? new LockInfo.Builder(lockInfo) { Guid = lockInfoTdo.Guid, State = state}.Build() // State has changed, update required. + : lockInfo); + } + + return locksInfoUpdated; + } + + public static LockingState GetLockingState(this LockitLockingState lockingState) + { + switch (lockingState) + { + case LockitLockingState.Unknown: + case LockitLockingState.CouldntOpenBoldBlocked: // Lock is closed in most cases, but this is not guarnteed according to haveltec. + return LockingState.Unknown; + + case LockitLockingState.Open: + case LockitLockingState.CouldntCloseMoving: + case LockitLockingState.CouldntCloseBoldBlocked: + return LockingState.Open; + + case LockitLockingState.Closed: + return LockingState.Closed; + } + + throw new ArgumentException($"Can not convert LockIt specific locking state to logical locking state. Unknown state {lockingState} detected."); + } + + /// Gets one type from another. + /// + /// + public static LockInfoAuthTdo ToLockInfoTdo(this LockInfo lockInfo) + { + return new LockInfoAuthTdo.Builder() { Id = lockInfo.Id, Guid = lockInfo.Guid, K_u = lockInfo.UserKey, K_a = lockInfo.AdminKey, K_seed = lockInfo.Seed }.Build(); + } + + + } +} diff --git a/LockItShared/Services/BluetoothLock/LockItByGuidServiceHelper.cs b/LockItShared/Services/BluetoothLock/LockItByGuidServiceHelper.cs new file mode 100644 index 0000000..97ffed6 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/LockItByGuidServiceHelper.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LockItShared.Services.BluetoothLock +{ + public static class LockItByGuidServiceHelper + { + /// + /// Holds guids of developent locks to ensure that no wrong guid are provided by COPRI + /// + public static Dictionary DevelGuids = new Dictionary + { + { 2200537, new Guid("00000000-0000-0000-0000-d589a8023487") }, + { 2200543, new Guid("00000000-0000-0000-0000-cc141a6f68bb") }, + { 2200544, new Guid("00000000-0000-0000-0000-dc969f648732") }, + { 2200545, new Guid("00000000-0000-0000-0000-e38bf9d32234") } + }; + + } +} diff --git a/LockItShared/Services/BluetoothLock/NullLock.cs b/LockItShared/Services/BluetoothLock/NullLock.cs new file mode 100644 index 0000000..ba17a47 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/NullLock.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using TINK.Services.BluetoothLock.Tdo; + +namespace TINK.Services.BluetoothLock +{ + /// + /// Represents a not exisitng lock. + /// + public class NullLock : ILockService + { + private int BikeId { get; } + + public NullLock(int bikeId) + { + BikeId = bikeId; + } + + public Task ReconnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout) => + throw new NotImplementedException(); + + public string Name => + throw new NotImplementedException(); + + public Guid Guid => + throw new NotImplementedException(); + + public int Id => + throw new NotImplementedException(); + + public async Task OpenAsync() => + await Task.FromResult((LockitLockingState?)null); + + public async Task CloseAsync() => + await Task.FromResult((LockitLockingState?)null); + + public Task GetBatteryPercentageAsync() => + throw new System.Exception($"Can not get battery percentage. Lock {BikeId} not found."); + + public DeviceState? GetDeviceState() => + throw new NotImplementedException(); + + public Task GetIsAlarmOffAsync() => + throw new System.Exception($"Can not get whether alarm is on or off. Lock {BikeId} not found."); + + public Task GetLockStateAsync(bool doWaitRetry = false) => + throw new NotImplementedException(); + + public Task SetIsAlarmOffAsync(bool isActivated) => + throw new System.Exception($"Can not set alarm {isActivated}. Lock {BikeId} not found."); + + public async Task SetSoundAsync(SoundSettings settings) => + await Task.FromResult(false); + + /// Disconnect from bluetooth lock. + public Task Disconnect() => + throw new NotImplementedException(); + + } +} diff --git a/LockItShared/Services/BluetoothLock/Tdo/LockInfoAuthTdo.cs b/LockItShared/Services/BluetoothLock/Tdo/LockInfoAuthTdo.cs new file mode 100644 index 0000000..127ff00 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Tdo/LockInfoAuthTdo.cs @@ -0,0 +1,61 @@ +using TINK.Model.Connector; +using System; + +namespace TINK.Services.BluetoothLock.Tdo +{ + /// Data required to connect to a blutooth lock. + public class LockInfoAuthTdo + { + /// Identification number of bluetooth lock, 6-digits, second part of advertisement name. + public int Id { get; private set; } + + /// GUID to the device. + public Guid Guid { get; private set; } + + /// Seed used to generate key for connecting to bluetooth lock. + public byte[] K_seed { get; private set; } + + /// Key for connect to bluetooth lock as user. + public byte[] K_u { get; private set; } + + /// Key for connect to bluetooth lock as admin. + public byte[] K_a { get; private set; } + + public bool IsIdValid => Id != TextToLockItTypeHelper.INVALIDLOCKID; + + public bool IsGuidValid => Guid != TextToLockItTypeHelper.INVALIDLOCKGUID; + + public class Builder + { + private LockInfoAuthTdo lockInfoTdo = new LockInfoAuthTdo(); + + public Builder() { } + + /// Identification number of bluetooth lock, 6-digits, second part of advertisement name. + public int Id { get => lockInfoTdo.Id; set { lockInfoTdo.Id = value; } } + + /// GUID to the device. + public Guid Guid { get => lockInfoTdo.Guid; set { lockInfoTdo.Guid = value; } } + + /// Seed used to generate key for connecting to bluetooth lock. + public byte[] K_seed { get => lockInfoTdo.K_seed; set { lockInfoTdo.K_seed = value; } } + + /// Key for connect to bluetooth lock as user. + public byte[] K_u { get => lockInfoTdo.K_u; set { lockInfoTdo.K_u = value; } } + + /// Key for connect to bluetooth lock as admin. + public byte[] K_a { get => lockInfoTdo.K_a; set { lockInfoTdo.K_a = value; } } + + public LockInfoAuthTdo Build() + { + if (K_seed == null) K_seed = new byte[0]; + + if (K_u == null) K_u = new byte[0]; + + if (K_a == null) K_a = new byte[0]; + + return lockInfoTdo; + } + } + } +} diff --git a/LockItShared/Services/BluetoothLock/Tdo/LockInfoTdo.cs b/LockItShared/Services/BluetoothLock/Tdo/LockInfoTdo.cs new file mode 100644 index 0000000..f814c52 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/Tdo/LockInfoTdo.cs @@ -0,0 +1,53 @@ +using System; + +namespace TINK.Services.BluetoothLock.Tdo +{ + public enum LockitLockingState + { + Open = 0x00, + + Closed = 0x01, + + Unknown = 0x02, + + CouldntCloseMoving = 0x03, + + CouldntOpenBoldBlocked = 0x04, + + CouldntCloseBoldBlocked = 0x05 + } + + /// Object holding info about bluetooth lock. + public class LockInfoTdo + { + private LockInfoTdo() { } + + /// Identification number of bluetooth lock, 6-digits, second part of advertisement name. + public int Id { get; private set; } + + /// Guid for direct connect. + public Guid Guid { get; private set; } + + /// Gets the current state of the lock. + public LockitLockingState? State { get; private set; } + + public class Builder + { + private LockInfoTdo lockInfoTdo = new LockInfoTdo(); + + /// Identification number of bluetooth lock, 6-digits, second part of advertisement name. + public int Id { get => lockInfoTdo.Id; set => lockInfoTdo.Id = value; } + + /// Guid for direct connect. + public Guid Guid { get => lockInfoTdo.Guid; set => lockInfoTdo.Guid = value; } + + /// Gets the current state of the lock. + public LockitLockingState? State { get => lockInfoTdo.State; set => lockInfoTdo.State = value; } + + public LockInfoTdo Build() + { + return lockInfoTdo; + } + } + } +} diff --git a/LockItShared/Services/BluetoothLock/TimeOutProvider.cs b/LockItShared/Services/BluetoothLock/TimeOutProvider.cs new file mode 100644 index 0000000..c5a08e6 --- /dev/null +++ b/LockItShared/Services/BluetoothLock/TimeOutProvider.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Services.BluetoothLock +{ + public class TimeOutProvider : ITimeOutProvider + { + /// Maximum factor applied on timeout factor. + private readonly int MAXIMUMFACTORTIMEOUT = 4; + + /// Default timeout to connect to bluetooth lock. + public static int DEFAULT_BLUETOOTHCONNECT_TIMEOUTSECONDS = 5; + + public TimeOutProvider(IEnumerable timeOuts = null) + { + TimeOuts = timeOuts != null && timeOuts.Count() > 0 + ? timeOuts.ToList() + : new List { new TimeSpan(0, 0, DEFAULT_BLUETOOTHCONNECT_TIMEOUTSECONDS) }; + } + + private List TimeOuts { get; } + + public TimeSpan MultiConnect => TimeOuts.ToArray()[0]; + + public TimeSpan GetSingleConnect(int countOfTry) => countOfTry < TimeOuts.Count + ? TimeOuts.ToArray()[countOfTry] + : new TimeSpan(TimeOuts.ToArray()[0].Ticks * Math.Min(countOfTry, MAXIMUMFACTORTIMEOUT)) ; + } +} diff --git a/TestLockItShared/Services/BluetoothLock/Exception/TestCouldntCloseInconsistentStateExecption.cs b/TestLockItShared/Services/BluetoothLock/Exception/TestCouldntCloseInconsistentStateExecption.cs new file mode 100644 index 0000000..b87bbbb --- /dev/null +++ b/TestLockItShared/Services/BluetoothLock/Exception/TestCouldntCloseInconsistentStateExecption.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using TINK.Model.Bike.BluetoothLock; + +namespace TINK.Services.BluetoothLock.Exception +{ + [TestFixture] + public class TestCouldntCloseInconsistentStateExecption + { + [Test] + public void TestCtor_Unknown() + { + var ex = new CouldntCloseInconsistentStateExecption(LockingState.Unknown); + + Assert.That( + ex.Message, + Is.EqualTo("Lock reports unknown bold position.")); + } + + [Test] + public void TestCtor_Open() + { + var ex = new CouldntCloseInconsistentStateExecption(LockingState.Open); + + Assert.That( + ex.Message, + Does.Contain("locking state \"Open\"")); + } + } +} diff --git a/TestLockItShared/Services/BluetoothLock/Exception/TestCouldntOpenInconsistentStateExecption.cs b/TestLockItShared/Services/BluetoothLock/Exception/TestCouldntOpenInconsistentStateExecption.cs new file mode 100644 index 0000000..abfc532 --- /dev/null +++ b/TestLockItShared/Services/BluetoothLock/Exception/TestCouldntOpenInconsistentStateExecption.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using TINK.Model.Bike.BluetoothLock; + +namespace TINK.Services.BluetoothLock.Exception +{ + [TestFixture] + public class TestCouldntOpenInconsistentStateExecption + { + [Test] + public void TestCtor_Unknown() + { + var ex = new CouldntOpenInconsistentStateExecption(LockingState.Unknown); + + Assert.That( + ex.Message, + Is.EqualTo("Lock reports unknown bold position.")); + } + + [Test] + public void TestCtor_Open() + { + var ex = new CouldntOpenInconsistentStateExecption(LockingState.Closed); + + Assert.That( + ex.Message, + Does.Contain("locking state \"Closed\"")); + } + } +} diff --git a/TestLockItShared/Services/BluetoothLock/TestTimeOutProvider.cs b/TestLockItShared/Services/BluetoothLock/TestTimeOutProvider.cs new file mode 100644 index 0000000..6c3d2fa --- /dev/null +++ b/TestLockItShared/Services/BluetoothLock/TestTimeOutProvider.cs @@ -0,0 +1,28 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace TINK.Services.BluetoothLock +{ + [TestFixture] + public class TestTimeOutProvider + { + [Test] + public void TestCtor() + { + Assert.That(new TimeOutProvider().MultiConnect.TotalSeconds, Is.EqualTo(5), "Unexpected bluetooth default timeout detected."); + + Assert.That(new TimeOutProvider(new List() { new TimeSpan(0, 0, 4) }).MultiConnect.TotalSeconds, Is.EqualTo(4)); + } + + [Test] + public void TestGetSingleConnect() + { + Assert.That(new TimeOutProvider(new List() { new TimeSpan(0, 0, 4) }).GetSingleConnect(1).TotalSeconds, Is.EqualTo(4)); + Assert.That(new TimeOutProvider(new List() { new TimeSpan(0, 0, 4) }).GetSingleConnect(2).TotalSeconds, Is.EqualTo(8)); + Assert.That(new TimeOutProvider(new List() { new TimeSpan(0, 0, 4) }).GetSingleConnect(3).TotalSeconds, Is.EqualTo(12)); + Assert.That(new TimeOutProvider(new List() { new TimeSpan(0, 0, 4) }).GetSingleConnect(4).TotalSeconds, Is.EqualTo(16)); + Assert.That(new TimeOutProvider(new List() { new TimeSpan(0, 0, 4) }).GetSingleConnect(5).TotalSeconds, Is.EqualTo(16)); + } + } +} diff --git a/TestLockItShared/TestLockItShared.csproj b/TestLockItShared/TestLockItShared.csproj new file mode 100644 index 0000000..6718760 --- /dev/null +++ b/TestLockItShared/TestLockItShared.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.1 + + false + + TINK + + + + + + + + + + + + + diff --git a/TestLockItShared/TestLockItShared.sln b/TestLockItShared/TestLockItShared.sln new file mode 100644 index 0000000..1f62c29 --- /dev/null +++ b/TestLockItShared/TestLockItShared.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31229.75 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestLockItShared", "TestLockItShared.csproj", "{0432F508-8B41-4CA3-A1FC-38906346B82A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LockItShared", "..\LockItShared\LockItShared.csproj", "{A8AC4131-BF07-46BE-A8E2-51CB5CADA37E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0432F508-8B41-4CA3-A1FC-38906346B82A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0432F508-8B41-4CA3-A1FC-38906346B82A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0432F508-8B41-4CA3-A1FC-38906346B82A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0432F508-8B41-4CA3-A1FC-38906346B82A}.Release|Any CPU.Build.0 = Release|Any CPU + {A8AC4131-BF07-46BE-A8E2-51CB5CADA37E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8AC4131-BF07-46BE-A8E2-51CB5CADA37E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8AC4131-BF07-46BE-A8E2-51CB5CADA37E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8AC4131-BF07-46BE-A8E2-51CB5CADA37E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6DF9BEE1-A4B7-4785-AFDA-BA5DEFAE50A3} + EndGlobalSection +EndGlobal