Initial version.

This commit is contained in:
Oliver Hauff 2021-05-13 17:07:16 +02:00
commit 6bab491a21
40 changed files with 1812 additions and 0 deletions

63
.gitattributes vendored Normal file
View file

@ -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

261
.gitignore vendored Normal file
View file

@ -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

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="MultilingualAppToolkit">
<MultilingualAppToolkitVersion>4.0</MultilingualAppToolkitVersion>
<MultilingualFallbackLanguage>en-GB</MultilingualFallbackLanguage>
<TranslationReport Condition="'$(Configuration)' == 'Release'">true</TranslationReport>
<SuppressPseudoWarning Condition="'$(Configuration)' == 'Debug'">true</SuppressPseudoWarning>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>TINK</RootNamespace>
<NeutralLanguage>en-GB</NeutralLanguage>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\Multilingual App Toolkit\Microsoft.Multilingual.ResxResources.targets" Label="MultilingualAppToolkit" Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\Multilingual App Toolkit\v$(MultilingualAppToolkitVersion)\Microsoft.Multilingual.ResxResources.targets')" />
<Target Name="MATPrerequisite" BeforeTargets="PrepareForBuild" Condition="!Exists('$(MSBuildExtensionsPath)\Microsoft\Multilingual App Toolkit\Microsoft.Multilingual.ResxResources.targets')" Label="MultilingualAppToolkit">
<Warning Text="$(MSBuildProjectFile) is Multilingual build enabled, but the Multilingual App Toolkit is unavailable during the build. If building with Visual Studio, please check to ensure that toolkit is properly installed." />
</Target>
<ItemGroup>
<Folder Include="Model\Bikes\Bike\BluetoothLock\" />
<Folder Include="Model\Bikes\Bike\" />
<Folder Include="Model\Device\" />
<Folder Include="Services\BluetoothLock\Crypto\" />
<Folder Include="Services\BluetoothLock\Tdo\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>
<Compile Update="MultilingualResources\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="MultilingualResources\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View file

@ -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

View file

@ -0,0 +1,11 @@
namespace TINK.Model.Bike.BluetoothLock
{
public interface ILockInfo
{
/// <summary> Identification number of bluetooth lock.</summary>
int Id { get; }
/// <summary> Gets the user key.</summary>
byte[] UserKey { get; }
}
}

View file

@ -0,0 +1,127 @@
using TINK.Model.Connector;
using Newtonsoft.Json;
using System;
using System.Runtime.Serialization;
namespace TINK.Model.Bike.BluetoothLock
{
/// <summary> Locking states. </summary>
public enum LockingState
{
Disconnected,
/// <summary> Lock might be open, closed or something in between..</summary>
Unknown,
/// <summary> Lock is closed. </summary>
Closed,
/// <summary> Lock is open. </summary>
Open
}
[DataContract]
public class LockInfo : ILockInfo, IEquatable<LockInfo>
{
/// <summary> Identification number of bluetooth lock, 6-digits, second part of advertisement name.</summary>
[DataMember]
public int Id { get; private set; } = TextToLockItTypeHelper.INVALIDLOCKID;
/// <summary> Identification GUID of bluetooth lock.</summary>
[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; }
/// <summary> Locking state of bluetooth lock. </summary>
[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<LockInfo>(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;
}
}
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Text.RegularExpressions;
namespace TINK.Model.Connector
{
public static class TextToLockItTypeHelper
{
/// <summary> Lock id which representing a non valid id. </summary>
public const int INVALIDLOCKID = 0;
/// <summary> Lock GUID which representing a non valid id. </summary>
public readonly static Guid INVALIDLOCKGUID = new Guid();
/// <summary> First part of advertisement name.</summary>
public static string ISHAREITADVERTISMENTTITLE = "ISHAREIT";
/// <summary> Gets the ID part from advertisment name. </summary>
/// <remarks> Advertisement name is made up of name plus separator (+ or -) and a ID</remarks>
/// <param name="advertisementName">Advertisment name to extract info from.</param>
/// <returns>From information.</returns>
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;
}
}
}

View file

@ -0,0 +1,17 @@
namespace TINK.Model.Device
{
public interface ICipher
{
/// <summary> Encrypt data.</summary>
/// <param name="key">Key to encrypt data.</param>
/// <param name="clear">Data to entrycpt.</param>
/// <returns></returns>
byte[] Encrypt(byte[] key, byte[] clear);
/// <summary> Decrypt data. </summary>
/// <param name="key">Key to decrypt data.</param>
/// <param name="encrypted">Encrpyted data to decrypt.</param>
/// <returns></returns>
byte[] Decrypt(byte[] key, byte[] encrypted);
}
}

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
<file datatype="xml" source-language="en-GB" target-language="de" original="LOCKITSHARED/MULTILINGUALRESOURCES/RESOURCES.RESX" tool-id="MultilingualAppToolkit" product-name="n/a" product-version="n/a" build-num="n/a">
<header>
<tool tool-id="MultilingualAppToolkit" tool-name="Multilingual App Toolkit" tool-version="4.0.6916.0" tool-company="Microsoft" />
</header>
<body>
<group id="LOCKITSHARED/MULTILINGUALRESOURCES/RESOURCES.RESX" datatype="resx">
<trans-unit id="ErrorCloseLockUnexpectedState" translate="yes" xml:space="preserve">
<source>Unexpected locking state "{0}" detected after sending close command.</source>
<target state="translated">Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführung des Abschließen-Befehls.</target>
</trans-unit>
<trans-unit id="ErrorCloseLockUnknownPosition" translate="yes" xml:space="preserve">
<source>Lock reports unknown bold position.</source>
<target state="translated">Schloss meldet unbekannten Schließzustand.</target>
</trans-unit>
<trans-unit id="ErrorOpenLockUnexpectedState" translate="yes" xml:space="preserve">
<source>Unexpected locking state "{0}" detected after sending open command.</source>
<target state="translated">Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführen des Öffnen-Befehls.</target>
</trans-unit>
<trans-unit id="ErrorOpenLockUnknownPosition" translate="yes" xml:space="preserve">
<source>Lock reports unknown bold position.</source>
<target state="needs-review-translation">Schloss meldet unbekannten Schließzustand.</target>
<note from="MultilingualUpdate" annotates="source" priority="2">Please verify the translations accuracy as the source string was updated after it was translated.</note>
</trans-unit>
<trans-unit id="ErrorBluetoothDisconnectedException" translate="yes" xml:space="preserve">
<source>No bluetooth connection.</source>
<target state="translated">Keine Bluetooth-Verbindung.</target>
</trans-unit>
<trans-unit id="ErrorCloseLockBikeMoving" translate="yes" xml:space="preserve">
<source>Bike is moving.</source>
<target state="translated">Rad ist in Bewegung.</target>
</trans-unit>
<trans-unit id="ErrorCloseLockBoldBlocked" translate="yes" xml:space="preserve">
<source>Bold is blocked.</source>
<target state="translated">Schloss ist blockiert.</target>
</trans-unit>
<trans-unit id="ErrorOpenLockBoldBlocked" translate="yes" xml:space="preserve">
<source>Bold is blocked.</source>
<target state="translated">Schloss ist blockiert.</target>
</trans-unit>
</group>
</body>
</file>
</xliff>

View file

@ -0,0 +1,135 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace TINK.MultilingualResources {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to No bluetooth connection..
/// </summary>
internal static string ErrorBluetoothDisconnectedException {
get {
return ResourceManager.GetString("ErrorBluetoothDisconnectedException", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bike is moving..
/// </summary>
internal static string ErrorCloseLockBikeMoving {
get {
return ResourceManager.GetString("ErrorCloseLockBikeMoving", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bold is blocked..
/// </summary>
internal static string ErrorCloseLockBoldBlocked {
get {
return ResourceManager.GetString("ErrorCloseLockBoldBlocked", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unexpected locking state &quot;{0}&quot; detected after sending close command..
/// </summary>
internal static string ErrorCloseLockUnexpectedState {
get {
return ResourceManager.GetString("ErrorCloseLockUnexpectedState", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Lock reports unknown bold position..
/// </summary>
internal static string ErrorCloseLockUnknownPosition {
get {
return ResourceManager.GetString("ErrorCloseLockUnknownPosition", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Bold is blocked..
/// </summary>
internal static string ErrorOpenLockBoldBlocked {
get {
return ResourceManager.GetString("ErrorOpenLockBoldBlocked", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unexpected locking state &quot;{0}&quot; detected after sending open command..
/// </summary>
internal static string ErrorOpenLockUnexpectedState {
get {
return ResourceManager.GetString("ErrorOpenLockUnexpectedState", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Lock reports unknown bold position..
/// </summary>
internal static string ErrorOpenLockUnknownPosition {
get {
return ResourceManager.GetString("ErrorOpenLockUnknownPosition", resourceCulture);
}
}
}
}

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ErrorCloseLockUnexpectedState" xml:space="preserve">
<value>Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführung des Abschließen-Befehls.</value>
</data>
<data name="ErrorCloseLockUnknownPosition" xml:space="preserve">
<value>Schloss meldet unbekannten Schließzustand.</value>
</data>
<data name="ErrorOpenLockUnexpectedState" xml:space="preserve">
<value>Unerwarteter Schlosszustand "{0}" gemeldet nach Ausführen des Öffnen-Befehls.</value>
</data>
<data name="ErrorOpenLockUnknownPosition" xml:space="preserve">
<value>Schloss meldet unbekannten Schließzustand.</value>
</data>
<data name="ErrorBluetoothDisconnectedException" xml:space="preserve">
<value>Keine Bluetooth-Verbindung.</value>
</data>
<data name="ErrorCloseLockBikeMoving" xml:space="preserve">
<value>Rad ist in Bewegung.</value>
</data>
<data name="ErrorCloseLockBoldBlocked" xml:space="preserve">
<value>Schloss ist blockiert.</value>
</data>
<data name="ErrorOpenLockBoldBlocked" xml:space="preserve">
<value>Schloss ist blockiert.</value>
</data>
</root>

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ErrorBluetoothDisconnectedException" xml:space="preserve">
<value>No bluetooth connection.</value>
</data>
<data name="ErrorCloseLockBikeMoving" xml:space="preserve">
<value>Bike is moving.</value>
</data>
<data name="ErrorCloseLockBoldBlocked" xml:space="preserve">
<value>Bold is blocked.</value>
</data>
<data name="ErrorCloseLockUnexpectedState" xml:space="preserve">
<value>Unexpected locking state "{0}" detected after sending close command.</value>
</data>
<data name="ErrorCloseLockUnknownPosition" xml:space="preserve">
<value>Lock reports unknown bold position.</value>
</data>
<data name="ErrorOpenLockBoldBlocked" xml:space="preserve">
<value>Bold is blocked.</value>
</data>
<data name="ErrorOpenLockUnexpectedState" xml:space="preserve">
<value>Unexpected locking state "{0}" detected after sending open command.</value>
</data>
<data name="ErrorOpenLockUnknownPosition" xml:space="preserve">
<value>Lock reports unknown bold position.</value>
</data>
</root>

View file

@ -0,0 +1,82 @@
using Serilog;
using TINK.Model.Device;
namespace TINK.Services.BluetoothLock.Crypto
{
public class AuthCryptoHelper
{
private ICipher Cipher { get; }
/// <summary> Encrypted seed (random number) created inside ILOCKIT and passd to app.</summary>
private byte[] SeedLockEncrypted { get; }
/// <summary> Contstructs a auth crypto helper object.</summary>
/// <param name="seedLockEncrypted">Encrypted seed to deocode using <see cref="KeyCopri"/>.</param>
/// <param name="keyCopri">Key used to to decrypt <see cref="SeedLockEncrypted"/>.</param>
public AuthCryptoHelper(
byte[] seedLockEncrypted,
byte[] keyCopri,
ICipher cipher)
{
KeyCopri = keyCopri;
SeedLockEncrypted = seedLockEncrypted;
Cipher = cipher ?? new Cipher();
}
/// <summary> Public for testing purposes only.</summary>
public byte[] GetSeedLock()
{
byte[] seedLockDecrypted;
var seedLockEncrypted = SeedLockEncrypted;
var keyCopri = KeyCopri;
try
{
seedLockDecrypted = Cipher.Decrypt(
keyCopri,
seedLockEncrypted);
}
catch (System.Exception exception)
{
Log.ForContext<AuthCryptoHelper>().Error("Decrypting seed from lock failed. {Exception}", exception);
throw;
}
Log.ForContext<AuthCryptoHelper>().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<AuthCryptoHelper>().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<AuthCryptoHelper>().Error("Encrypting access key failed. {Exception}", exception);
throw;
}
Log.ForContext<AuthCryptoHelper>().Verbose($"Access key encrypted from {string.Join(",", accessKey)} to {string.Join(",", acccessKeyEncrypted)} using {string.Join(", ", keyCopri)}.");
return acccessKeyEncrypted;
}
public byte[] KeyCopri { get; }
}
}

View file

@ -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
{
/// <summary> Decrypt data. </summary>
/// <remarks>
/// 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
/// </remarks>
/// <param name="key">Key to decrypt data with.</param>
/// <param name="encrypted">Encrpyted data to decrypt.</param>
/// <returns>Decrypted data.</returns>
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();
}
}
}
}
}
}

View file

@ -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.") {}
}
}

View file

@ -0,0 +1,8 @@
namespace TINK.Services.BluetoothLock.Exception
{
public class AuthKeyException : System.Exception
{
public AuthKeyException(string message) : base(message)
{ }
}
}

View file

@ -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)
{
}
}
}

View file

@ -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)
{
}
}
}

View file

@ -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)
{
}
}
}

View file

@ -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)
{
}
}
}

View file

@ -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)
{
}
}
}

View file

@ -0,0 +1,8 @@
namespace TINK.Services.BluetoothLock.Exception
{
public class CoundntGetCharacteristicException : System.Exception
{
public CoundntGetCharacteristicException(string message) : base(message)
{ }
}
}

View file

@ -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)
{
}
}
}

View file

@ -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.")
{
}
}
}

View file

@ -0,0 +1,7 @@
namespace TINK.Services.BluetoothLock.Exception
{
/// <summary> Thrown whenever lock is out of reach.</summary>
public class OutOfReachException : System.Exception
{
}
}

View file

@ -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;
}
/// <summary> Holds the state reported by lock.</summary>
public LockingState State { get; }
}
}

View file

@ -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
{
/// <summary> Reconnect to lock. </summary>
Task ReconnectAsync(
LockInfoAuthTdo authInfo,
TimeSpan connectTimeout);
/// <summary> Opens lock. </summary>
/// <returns> State of lock after performing open operation. </returns>
Task<LockitLockingState?> OpenAsync();
/// <summary> Closes lock. </summary>
/// <returns>State of lock after performing close operation.</returns>
Task<LockitLockingState?> CloseAsync();
string Name { get; }
Guid Guid { get; }
int Id { get; }
/// <summary> Gets the device state.</summary>
DeviceState? GetDeviceState();
/// <summary>
/// Gets the locking state.
/// </summary>
/// <param name="doWaitRetry">True if to wait and retry in case of failures. </param>
/// <returns></returns>
Task<LockInfoTdo> GetLockStateAsync(bool doWaitRetry = false);
Task<bool> SetSoundAsync(SoundSettings settings);
Task<bool> GetIsAlarmOffAsync();
Task SetIsAlarmOffAsync(bool isActivated);
/// <summary>Gets the battery percentage.</summary>
Task<double> GetBatteryPercentageAsync();
/// <summary> Disconnect from lock.</summary>
Task Disconnect();
}
}

View file

@ -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
{
/// <summary> Holds timeout values for series of connecting attemps to a lock or multiple locks. </summary>
ITimeOutProvider TimeOut { get; set; }
/// <summary> Gets lock info for all lock in reach./// </summary>
/// <param name="connectTimeout">Timeout for connect operation of a single lock.</param>
Task<IEnumerable<LockInfoTdo>> GetLocksStateAsync(
IEnumerable<LockInfoAuthTdo> locksInfo,
TimeSpan connectTimeout);
/// <summary> Connects to lock.</summary>
/// <param name="authInfo"> Info required to connect to lock.</param>
/// <param name="connectTimeout">Timeout for connect operation.</param>
Task<LockInfoTdo> ConnectAsync(
LockInfoAuthTdo authInfo,
TimeSpan connectTimeout);
/// <summary>Gets a lock by bike Id.</summary>
/// <param name="bikeId"></param>
/// <returns>Lock object</returns>
ILockService this[int bikeId] { get; }
/// <summary> Disconnects lock.</summary>
/// <param name="bikeId"> Id of lock to disconnect.</param>
/// <param name="bikeGuid"> Guid of lock to disconnect.</param>
/// <returns> State disconnected it lock is already disconneced or after successfully disconnecting.</returns>
Task<LockingState> DisconnectAsync(int bikeId, Guid bikeGuid);
}
/// <summary> Sound types. </summary>
/// <remarks>
/// 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.
/// </remarks>
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
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace TINK.Services.BluetoothLock
{
public interface ITimeOutProvider
{
TimeSpan MultiConnect { get; }
TimeSpan GetSingleConnect(int countOfTry);
}
}

View file

@ -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
{
/// <summary> Update by id.</summary>
/// <param name="locksInfo"> Lock info objects to update by id.</param>
/// <param name="locksInfoTdo">Tdos holding data to updat from</param>
/// <returns></returns>
public static IEnumerable<LockInfo> UpdateById(
this IEnumerable<LockInfo> locksInfo,
IEnumerable<LockInfoTdo> locksInfoTdo)
{
var locksInfoUpdated = new List<LockInfo>();
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.");
}
/// <summary> Gets one type from another.</summary>
/// <param name="lockInfo"></param>
/// <returns></returns>
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();
}
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace LockItShared.Services.BluetoothLock
{
public static class LockItByGuidServiceHelper
{
/// <summary>
/// Holds guids of developent locks to ensure that no wrong guid are provided by COPRI
/// </summary>
public static Dictionary<int, Guid> DevelGuids = new Dictionary<int, Guid>
{
{ 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") }
};
}
}

View file

@ -0,0 +1,60 @@
using System;
using System.Threading.Tasks;
using TINK.Services.BluetoothLock.Tdo;
namespace TINK.Services.BluetoothLock
{
/// <summary>
/// Represents a not exisitng lock.
/// </summary>
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<LockitLockingState?> OpenAsync() =>
await Task.FromResult((LockitLockingState?)null);
public async Task<LockitLockingState?> CloseAsync() =>
await Task.FromResult((LockitLockingState?)null);
public Task<double> GetBatteryPercentageAsync() =>
throw new System.Exception($"Can not get battery percentage. Lock {BikeId} not found.");
public DeviceState? GetDeviceState() =>
throw new NotImplementedException();
public Task<bool> GetIsAlarmOffAsync() =>
throw new System.Exception($"Can not get whether alarm is on or off. Lock {BikeId} not found.");
public Task<LockInfoTdo> 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<bool> SetSoundAsync(SoundSettings settings) =>
await Task.FromResult(false);
/// <summary> Disconnect from bluetooth lock. </summary>
public Task Disconnect() =>
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,61 @@
using TINK.Model.Connector;
using System;
namespace TINK.Services.BluetoothLock.Tdo
{
/// <summary> Data required to connect to a blutooth lock.</summary>
public class LockInfoAuthTdo
{
/// <summary> Identification number of bluetooth lock, 6-digits, second part of advertisement name.</summary>
public int Id { get; private set; }
/// <summary> GUID to the device. </summary>
public Guid Guid { get; private set; }
/// <summary> Seed used to generate key for connecting to bluetooth lock.</summary>
public byte[] K_seed { get; private set; }
/// <summary> Key for connect to bluetooth lock as user.</summary>
public byte[] K_u { get; private set; }
/// <summary> Key for connect to bluetooth lock as admin.</summary>
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() { }
/// <summary> Identification number of bluetooth lock, 6-digits, second part of advertisement name.</summary>
public int Id { get => lockInfoTdo.Id; set { lockInfoTdo.Id = value; } }
/// <summary> GUID to the device. </summary>
public Guid Guid { get => lockInfoTdo.Guid; set { lockInfoTdo.Guid = value; } }
/// <summary> Seed used to generate key for connecting to bluetooth lock.</summary>
public byte[] K_seed { get => lockInfoTdo.K_seed; set { lockInfoTdo.K_seed = value; } }
/// <summary> Key for connect to bluetooth lock as user.</summary>
public byte[] K_u { get => lockInfoTdo.K_u; set { lockInfoTdo.K_u = value; } }
/// <summary> Key for connect to bluetooth lock as admin.</summary>
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;
}
}
}
}

View file

@ -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
}
/// <summary> Object holding info about bluetooth lock. </summary>
public class LockInfoTdo
{
private LockInfoTdo() { }
/// <summary> Identification number of bluetooth lock, 6-digits, second part of advertisement name.</summary>
public int Id { get; private set; }
/// <summary> Guid for direct connect.</summary>
public Guid Guid { get; private set; }
/// <summary> Gets the current state of the lock.</summary>
public LockitLockingState? State { get; private set; }
public class Builder
{
private LockInfoTdo lockInfoTdo = new LockInfoTdo();
/// <summary> Identification number of bluetooth lock, 6-digits, second part of advertisement name.</summary>
public int Id { get => lockInfoTdo.Id; set => lockInfoTdo.Id = value; }
/// <summary> Guid for direct connect.</summary>
public Guid Guid { get => lockInfoTdo.Guid; set => lockInfoTdo.Guid = value; }
/// <summary> Gets the current state of the lock.</summary>
public LockitLockingState? State { get => lockInfoTdo.State; set => lockInfoTdo.State = value; }
public LockInfoTdo Build()
{
return lockInfoTdo;
}
}
}
}

View file

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace TINK.Services.BluetoothLock
{
public class TimeOutProvider : ITimeOutProvider
{
/// <summary> Maximum factor applied on timeout factor. </summary>
private readonly int MAXIMUMFACTORTIMEOUT = 4;
/// <summary> Default timeout to connect to bluetooth lock. </summary>
public static int DEFAULT_BLUETOOTHCONNECT_TIMEOUTSECONDS = 5;
public TimeOutProvider(IEnumerable<TimeSpan> timeOuts = null)
{
TimeOuts = timeOuts != null && timeOuts.Count() > 0
? timeOuts.ToList()
: new List<TimeSpan> { new TimeSpan(0, 0, DEFAULT_BLUETOOTHCONNECT_TIMEOUTSECONDS) };
}
private List<TimeSpan> 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)) ;
}
}

View file

@ -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\""));
}
}
}

View file

@ -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\""));
}
}
}

View file

@ -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<TimeSpan>() { new TimeSpan(0, 0, 4) }).MultiConnect.TotalSeconds, Is.EqualTo(4));
}
[Test]
public void TestGetSingleConnect()
{
Assert.That(new TimeOutProvider(new List<TimeSpan>() { new TimeSpan(0, 0, 4) }).GetSingleConnect(1).TotalSeconds, Is.EqualTo(4));
Assert.That(new TimeOutProvider(new List<TimeSpan>() { new TimeSpan(0, 0, 4) }).GetSingleConnect(2).TotalSeconds, Is.EqualTo(8));
Assert.That(new TimeOutProvider(new List<TimeSpan>() { new TimeSpan(0, 0, 4) }).GetSingleConnect(3).TotalSeconds, Is.EqualTo(12));
Assert.That(new TimeOutProvider(new List<TimeSpan>() { new TimeSpan(0, 0, 4) }).GetSingleConnect(4).TotalSeconds, Is.EqualTo(16));
Assert.That(new TimeOutProvider(new List<TimeSpan>() { new TimeSpan(0, 0, 4) }).GetSingleConnect(5).TotalSeconds, Is.EqualTo(16));
}
}
}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>TINK</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="nunit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LockItShared\LockItShared.csproj" />
</ItemGroup>
</Project>

View file

@ -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