diff --git a/BrickController2.sln b/BrickController2.sln
index 427c9da4..bdd401b9 100644
--- a/BrickController2.sln
+++ b/BrickController2.sln
@@ -9,130 +9,276 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrickController2.iOS", "Bri
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrickController2", "BrickController2\BrickController2\BrickController2.csproj", "{852D9034-471A-42D0-8701-63D12E2EDACA}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrickController2.UWP", "BrickController2\BrickController2.UWP\BrickController2.UWP.csproj", "{DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
+ Ad-Hoc|ARM = Ad-Hoc|ARM
Ad-Hoc|iPhone = Ad-Hoc|iPhone
Ad-Hoc|iPhoneSimulator = Ad-Hoc|iPhoneSimulator
+ Ad-Hoc|x64 = Ad-Hoc|x64
+ Ad-Hoc|x86 = Ad-Hoc|x86
AppStore|Any CPU = AppStore|Any CPU
+ AppStore|ARM = AppStore|ARM
AppStore|iPhone = AppStore|iPhone
AppStore|iPhoneSimulator = AppStore|iPhoneSimulator
+ AppStore|x64 = AppStore|x64
+ AppStore|x86 = AppStore|x86
Debug|Any CPU = Debug|Any CPU
+ Debug|ARM = Debug|ARM
Debug|iPhone = Debug|iPhone
Debug|iPhoneSimulator = Debug|iPhoneSimulator
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|ARM = Release|ARM
Release|iPhone = Release|iPhone
Release|iPhoneSimulator = Release|iPhoneSimulator
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|Any CPU.Deploy.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|ARM.Deploy.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|iPhone.Deploy.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|x64.Deploy.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Ad-Hoc|x86.Deploy.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|Any CPU.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|Any CPU.Deploy.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|ARM.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|ARM.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|ARM.Deploy.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|iPhone.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|iPhone.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|iPhone.Deploy.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|iPhoneSimulator.Deploy.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|x64.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|x64.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|x64.Deploy.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|x86.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|x86.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.AppStore|x86.Deploy.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|ARM.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|ARM.Deploy.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|iPhone.Build.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|iPhone.Deploy.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|x64.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|x64.Deploy.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|x86.Build.0 = Debug|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Debug|x86.Deploy.0 = Debug|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|Any CPU.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|ARM.ActiveCfg = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|ARM.Build.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|ARM.Deploy.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|iPhone.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|iPhone.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|iPhone.Deploy.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|x64.ActiveCfg = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|x64.Build.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|x64.Deploy.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|x86.ActiveCfg = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|x86.Build.0 = Release|Any CPU
+ {A82CFFAA-423D-473B-820C-174F7AE48B7E}.Release|x86.Deploy.0 = Release|Any CPU
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|Any CPU.Build.0 = Ad-Hoc|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|Any CPU.Deploy.0 = Ad-Hoc|iPhone
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|iPhone.Deploy.0 = Ad-Hoc|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Ad-Hoc|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|Any CPU.Build.0 = AppStore|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|Any CPU.Deploy.0 = AppStore|iPhone
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|ARM.ActiveCfg = AppStore|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|iPhone.ActiveCfg = AppStore|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|iPhone.Build.0 = AppStore|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|iPhone.Deploy.0 = AppStore|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|iPhoneSimulator.Deploy.0 = AppStore|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|x64.ActiveCfg = AppStore|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.AppStore|x86.ActiveCfg = AppStore|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|Any CPU.Deploy.0 = Debug|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|ARM.ActiveCfg = Debug|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|iPhone.ActiveCfg = Debug|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|iPhone.Build.0 = Debug|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|iPhone.Deploy.0 = Debug|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|iPhoneSimulator.Deploy.0 = Debug|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|x64.ActiveCfg = Debug|iPhone
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Debug|x86.ActiveCfg = Debug|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|Any CPU.ActiveCfg = Release|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|Any CPU.Build.0 = Release|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|Any CPU.Deploy.0 = Release|iPhone
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|ARM.ActiveCfg = Release|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|iPhone.ActiveCfg = Release|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|iPhone.Build.0 = Release|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|iPhone.Deploy.0 = Release|iPhone
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
{E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|iPhoneSimulator.Deploy.0 = Release|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|x64.ActiveCfg = Release|iPhoneSimulator
+ {E52D9D91-6F31-42E2-8261-D85AA62D39D1}.Release|x86.ActiveCfg = Release|iPhoneSimulator
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|Any CPU.Deploy.0 = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|ARM.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|iPhone.Deploy.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|x64.Build.0 = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Ad-Hoc|x86.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|Any CPU.Deploy.0 = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|ARM.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|ARM.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|iPhone.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|iPhone.Deploy.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|iPhoneSimulator.Deploy.0 = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|x64.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|x64.Build.0 = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|x86.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.AppStore|x86.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|ARM.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|iPhone.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|iPhone.Deploy.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|x64.Build.0 = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Debug|x86.Build.0 = Debug|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|Any CPU.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|Any CPU.Deploy.0 = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Release|ARM.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Release|ARM.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|iPhone.ActiveCfg = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|iPhone.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|iPhone.Deploy.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{852D9034-471A-42D0-8701-63D12E2EDACA}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Release|x64.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Release|x64.Build.0 = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Release|x86.ActiveCfg = Release|Any CPU
+ {852D9034-471A-42D0-8701-63D12E2EDACA}.Release|x86.Build.0 = Release|Any CPU
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|Any CPU.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|Any CPU.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|Any CPU.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|ARM.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|ARM.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|ARM.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|iPhone.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|iPhone.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|iPhone.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|x64.ActiveCfg = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|x64.Build.0 = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|x64.Deploy.0 = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|x86.ActiveCfg = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|x86.Build.0 = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Ad-Hoc|x86.Deploy.0 = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|Any CPU.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|Any CPU.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|Any CPU.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|ARM.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|ARM.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|ARM.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|iPhone.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|iPhone.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|iPhone.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|iPhoneSimulator.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|iPhoneSimulator.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|iPhoneSimulator.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|x64.ActiveCfg = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|x64.Build.0 = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|x64.Deploy.0 = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|x86.ActiveCfg = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|x86.Build.0 = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.AppStore|x86.Deploy.0 = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|ARM.ActiveCfg = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|ARM.Build.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|ARM.Deploy.0 = Debug|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|iPhone.ActiveCfg = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|iPhoneSimulator.ActiveCfg = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|x64.ActiveCfg = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|x64.Build.0 = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|x64.Deploy.0 = Debug|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|x86.ActiveCfg = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|x86.Build.0 = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Debug|x86.Deploy.0 = Debug|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|Any CPU.ActiveCfg = Release|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|ARM.ActiveCfg = Release|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|ARM.Build.0 = Release|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|ARM.Deploy.0 = Release|ARM
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|iPhone.ActiveCfg = Release|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|iPhoneSimulator.ActiveCfg = Release|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|x64.ActiveCfg = Release|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|x64.Build.0 = Release|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|x64.Deploy.0 = Release|x64
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|x86.ActiveCfg = Release|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|x86.Build.0 = Release|x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}.Release|x86.Deploy.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs
index 54cdc625..283f287d 100644
--- a/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs
+++ b/BrickController2/BrickController2.Android/PlatformServices/BluetoothLE/BluetoothLEDevice.cs
@@ -131,7 +131,6 @@ public Task DisconnectAsync()
{
Disconnect();
}
-
return Task.CompletedTask;
}
diff --git a/BrickController2/BrickController2.UWP/App.xaml b/BrickController2/BrickController2.UWP/App.xaml
new file mode 100644
index 00000000..9fed9db8
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/App.xaml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/BrickController2/BrickController2.UWP/App.xaml.cs b/BrickController2/BrickController2.UWP/App.xaml.cs
new file mode 100644
index 00000000..95eadb51
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/App.xaml.cs
@@ -0,0 +1,83 @@
+using BrickController2.Helpers;
+using System;
+using Windows.ApplicationModel;
+using Windows.ApplicationModel.Activation;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace BrickController2.Windows
+{
+ ///
+ /// Provides application-specific behavior to supplement the default Application class.
+ ///
+ sealed partial class App : Application
+ {
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ // workaround resource loading for EN language
+ // enforce blocking of modern resource loading so as EN resources are properly loaded
+ var location = System.IO.Path.GetDirectoryName(typeof(ResourceHelper).Assembly.Location);
+ AppDomain.CurrentDomain.SetData("PLATFORM_RESOURCE_ROOTS", location);
+
+
+ this.InitializeComponent();
+ this.Suspending += OnSuspending;
+ }
+
+ ///
+ /// Invoked when the application is launched normally by the end user. Other entry points
+ /// will be used such as when the application is launched to open a specific file.
+ ///
+ /// Details about the launch request and process.
+ protected override void OnLaunched(LaunchActivatedEventArgs e)
+ {
+#if DEBUG
+ if (System.Diagnostics.Debugger.IsAttached)
+ {
+ this.DebugSettings.EnableFrameRateCounter = true;
+ }
+#endif
+
+ Frame rootFrame = Window.Current.Content as Frame;
+
+ // Do not repeat app initialization when the Window already has content,
+ if (rootFrame == null)
+ {
+ // Create a Frame to act as the navigation context and navigate to the first page
+ rootFrame = new Frame();
+
+
+ Xamarin.Forms.Forms.Init(e);
+
+ // Place the frame in the current Window
+ Window.Current.Content = rootFrame;
+ }
+
+ if (rootFrame.Content == null)
+ {
+ // navigate to the first page
+ rootFrame.Navigate(typeof(MainPage), e.Arguments);
+ }
+ // Ensure the current window is active
+ Window.Current.Activate();
+ }
+
+
+ ///
+ /// Invoked when application execution is being suspended. Application state is saved
+ /// without knowing whether the application will be terminated or resumed with the contents
+ /// of memory still intact.
+ ///
+ /// The source of the suspend request.
+ /// Details about the suspend request.
+ private void OnSuspending(object sender, SuspendingEventArgs e)
+ {
+ var deferral = e.SuspendingOperation.GetDeferral();
+ deferral.Complete();
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-100.png b/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-100.png
new file mode 100644
index 00000000..d014072a
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-100.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-200.png b/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-200.png
new file mode 100644
index 00000000..32e82d21
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-200.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-400.png b/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-400.png
new file mode 100644
index 00000000..5edb898d
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/SplashScreen.scale-400.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-16.png
new file mode 100644
index 00000000..5d026440
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-256.png
new file mode 100644
index 00000000..3e1b0a80
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-48.png
new file mode 100644
index 00000000..6a86ecc7
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-100.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-100.png
new file mode 100644
index 00000000..dce32382
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-100.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-200.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-200.png
new file mode 100644
index 00000000..e4638aac
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-200.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-400.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-400.png
new file mode 100644
index 00000000..5776f819
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.scale-400.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-16.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-16.png
new file mode 100644
index 00000000..5d026440
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-16.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-256.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-256.png
new file mode 100644
index 00000000..3e1b0a80
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-256.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-48.png b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-48.png
new file mode 100644
index 00000000..6a86ecc7
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/Square44x44Logo.targetsize-48.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-100.png b/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-100.png
new file mode 100644
index 00000000..dce32382
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-100.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-200.png b/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-200.png
new file mode 100644
index 00000000..e4638aac
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-200.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-400.png b/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-400.png
new file mode 100644
index 00000000..5776f819
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/StoreLogo.scale-400.png differ
diff --git a/BrickController2/BrickController2.UWP/Assets/ic_launcher.png b/BrickController2/BrickController2.UWP/Assets/ic_launcher.png
new file mode 100644
index 00000000..b00cbb33
Binary files /dev/null and b/BrickController2/BrickController2.UWP/Assets/ic_launcher.png differ
diff --git a/BrickController2/BrickController2.UWP/BrickController2.UWP.csproj b/BrickController2/BrickController2.UWP/BrickController2.UWP.csproj
new file mode 100644
index 00000000..1a0a9da0
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/BrickController2.UWP.csproj
@@ -0,0 +1,173 @@
+
+
+
+
+ Debug
+ x86
+ {DC1AE8D2-77CD-4C25-8E43-D2D3F3E68339}
+ {98C37F10-6541-44BE-B1E6-7EB3EB8C08F1}
+ AppContainerExe
+ Properties
+ BrickController2.Windows
+ BrickController2.UWP
+ en
+ UAP
+ 10.0.19041.0
+ 10.0.16299.0
+ 14
+ true
+ 512
+ {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ false
+
+
+ true
+ bin\ARM\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ ;2008
+ portable
+ ARM
+ false
+ prompt
+ true
+
+
+ bin\ARM\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ ARM
+ false
+ prompt
+ true
+ true
+
+
+ true
+ bin\x64\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ ;2008
+ portable
+ x64
+ false
+ prompt
+ true
+ true
+
+
+ bin\x64\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ x64
+ false
+ prompt
+ true
+ true
+
+
+ true
+ bin\x86\Debug\
+ DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP
+ ;2008
+ portable
+ x86
+ false
+ prompt
+ true
+
+
+ bin\x86\Release\
+ TRACE;NETFX_CORE;WINDOWS_UWP
+ true
+ ;2008
+ pdbonly
+ x86
+ false
+ prompt
+ true
+ true
+
+
+
+ App.xaml
+
+
+
+
+
+
+ MainPage.xaml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Designer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+
+
+
+
+
+
+
+ {B1508B2C-0A46-4407-9984-D4412DEF337F}
+ BrickController2
+
+
+
+
+ 14.0
+
+
+
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/Extensions/AdvertismentExtensions.cs b/BrickController2/BrickController2.UWP/Extensions/AdvertismentExtensions.cs
new file mode 100644
index 00000000..2164c041
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/Extensions/AdvertismentExtensions.cs
@@ -0,0 +1,23 @@
+using Windows.Devices.Bluetooth.Advertisement;
+
+namespace BrickController2.Windows.Extensions
+{
+ public static class AdvertismentExtensions
+ {
+ public static string GetLocalName(this BluetoothLEAdvertisementReceivedEventArgs args)
+ {
+ return args.Advertisement.LocalName.TrimEnd();
+ }
+
+ public static bool IsValidDeviceName(this string deviceName)
+ {
+ return !string.IsNullOrEmpty(deviceName);
+ }
+
+ public static bool CanCarryData(this BluetoothLEAdvertisementReceivedEventArgs args)
+ {
+ return args.AdvertisementType == BluetoothLEAdvertisementType.ScanResponse ||
+ args.AdvertisementType == BluetoothLEAdvertisementType.ConnectableUndirected;
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/Extensions/ControllerExtensions.cs b/BrickController2/BrickController2.UWP/Extensions/ControllerExtensions.cs
new file mode 100644
index 00000000..0c6c3f9d
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/Extensions/ControllerExtensions.cs
@@ -0,0 +1,13 @@
+using Windows.Gaming.Input;
+
+namespace BrickController2.Windows.Extensions
+{
+ public static class ControllerExtensions
+ {
+ public static string GetDeviceId(this Gamepad gamepad)
+ {
+ // kinda hack
+ return gamepad.User.NonRoamableId;
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/Extensions/ConvertExtensions.cs b/BrickController2/BrickController2.UWP/Extensions/ConvertExtensions.cs
new file mode 100644
index 00000000..e647fb2a
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/Extensions/ConvertExtensions.cs
@@ -0,0 +1,59 @@
+namespace BrickController2.Windows.Extensions
+{
+ public static class ConvertExtensions
+ {
+ public static string ToBluetoothAddressString(this ulong bluetoothAddress)
+ {
+ // 48bit physical BT address
+ var a = (byte)((bluetoothAddress >> 40) & 0xFF);
+ var b = (byte)((bluetoothAddress >> 32) & 0xFF);
+ var c = (byte)((bluetoothAddress >> 24) & 0xFF);
+ var d = (byte)((bluetoothAddress >> 16) & 0xFF);
+ var e = (byte)((bluetoothAddress >> 8) & 0xFF);
+ var f = (byte)(bluetoothAddress & 0xFF);
+
+ return $"{a:X2}:{b:X2}:{c:X2}:{d:X2}:{e:X2}:{f:X2}";
+ }
+
+ public static bool TryParseBluetoothAddressString(this string stringValue, out ulong bluetoothAddress)
+ {
+ bluetoothAddress = default;
+
+ if (string.IsNullOrEmpty(stringValue) || stringValue.Length != 17)
+ {
+ return false;
+ }
+
+ ulong value = 0;
+
+ for (int i = 1; i <= stringValue.Length; i++)
+ {
+ var ch = (uint)stringValue[i - 1];
+ if (i % 3 == 0)
+ {
+ if (ch != '-' && ch != ':')
+ {
+ // missing dash
+ return false;
+ }
+ }
+ else if (ch >= 0x30 && ch <= 0x39)
+ {
+ value = (value << 4) + ch - 0x30;
+ }
+ else if (ch >= 0x41 && ch <= 0x46)
+ {
+ value = (value << 4) + ch - 0x37;
+ }
+ else
+ {
+ // wrong character
+ return false;
+ }
+ }
+
+ bluetoothAddress = value;
+ return true;
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/Extensions/IBufferExtensions.cs b/BrickController2/BrickController2.UWP/Extensions/IBufferExtensions.cs
new file mode 100644
index 00000000..5f435316
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/Extensions/IBufferExtensions.cs
@@ -0,0 +1,26 @@
+using Windows.Storage.Streams;
+
+namespace BrickController2.Windows.Extensions
+{
+ public static class IBufferExtensions
+ {
+ public static IBuffer ToBuffer(this byte[] data)
+ {
+ var writer = new DataWriter();
+ writer.WriteBytes(data);
+
+ return writer.DetachBuffer();
+ }
+
+ public static byte[] ToByteArray(this IBuffer buffer)
+ {
+ using (var reader = DataReader.FromBuffer(buffer))
+ {
+ byte[] input = new byte[reader.UnconsumedBufferLength];
+ reader.ReadBytes(input);
+
+ return input;
+ }
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/MainPage.xaml b/BrickController2/BrickController2.UWP/MainPage.xaml
new file mode 100644
index 00000000..171e0fac
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/MainPage.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/MainPage.xaml.cs b/BrickController2/BrickController2.UWP/MainPage.xaml.cs
new file mode 100644
index 00000000..895b6366
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/MainPage.xaml.cs
@@ -0,0 +1,53 @@
+using Autofac;
+using BrickController2.BusinessLogic.DI;
+using BrickController2.CreationManagement.DI;
+using BrickController2.Database.DI;
+using BrickController2.DeviceManagement.DI;
+using BrickController2.UI.DI;
+using BrickController2.Windows.PlatformServices.DI;
+using BrickController2.Windows.PlatformServices.GameController;
+using Windows.UI;
+using Windows.UI.ViewManagement;
+using Windows.UI.Xaml;
+
+namespace BrickController2.Windows
+{
+ public sealed partial class MainPage
+ {
+ private readonly GameControllerService _gameControllerService;
+ private readonly IContainer _container;
+
+ public MainPage()
+ {
+ this.InitializeComponent();
+
+ // override system settings
+ var appView = ApplicationView.GetForCurrentView();
+ appView.TitleBar.ButtonBackgroundColor = Colors.White;
+ appView.TitleBar.ButtonPressedBackgroundColor = appView.TitleBar.ButtonHoverBackgroundColor = Colors.Red;
+
+ _container = InitDI();
+ _gameControllerService = _container.Resolve();
+
+ base.LoadApplication(_container.Resolve());
+
+ // ensure GameControllerService is properly linked
+ _gameControllerService.InitializeComponent(Window.Current.CoreWindow);
+ }
+
+ private static IContainer InitDI()
+ {
+ var builder = new ContainerBuilder();
+
+ builder.RegisterModule(new PlatformServicesModule());
+
+ builder.RegisterModule(new BusinessLogicModule());
+ builder.RegisterModule(new DatabaseModule());
+ builder.RegisterModule(new CreationManagementModule());
+ builder.RegisterModule(new DeviceManagementModule());
+ builder.RegisterModule(new UiModule());
+
+ return builder.Build();
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/Package.appxmanifest b/BrickController2/BrickController2.UWP/Package.appxmanifest
new file mode 100644
index 00000000..24eae599
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/Package.appxmanifest
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+ BrickController2.UWP
+ 20e2eb05-a9ce-4df2-b83a-efe50e08ac16
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleDevice.cs b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleDevice.cs
new file mode 100644
index 00000000..a278edd5
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleDevice.cs
@@ -0,0 +1,313 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using BrickController2.Helpers;
+using BrickController2.PlatformServices.BluetoothLE;
+using BrickController2.Windows.Extensions;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace BrickController2.Windows.PlatformServices.BluetoothLE
+{
+ public class BleDevice : IBluetoothLEDevice
+ {
+ private readonly AsyncLock _lock = new AsyncLock();
+
+ private BluetoothLEDevice _bluetoothDevice;
+ private ICollection _services;
+
+ private TaskCompletionSource> _connectCompletionSource;
+
+ private Action _onCharacteristicChanged;
+ private Action _onDeviceDisconnected;
+
+ public BleDevice(string address)
+ {
+ Address = address;
+ }
+
+ public string Address { get; }
+ public BluetoothLEDeviceState State { get; private set; } = BluetoothLEDeviceState.Disconnected;
+
+ public async Task> ConnectAndDiscoverServicesAsync(
+ bool autoConnect,
+ Action onCharacteristicChanged,
+ Action onDeviceDisconnected,
+ CancellationToken token)
+ {
+ using (var tokenRegistration = token.Register(async () =>
+ {
+ using (await _lock.LockAsync())
+ {
+ InternalDisconnect();
+ _connectCompletionSource?.TrySetResult(null);
+ }
+ }))
+ {
+ _services = await ConnectAsync(onCharacteristicChanged, onDeviceDisconnected);
+ return _services;
+ }
+ }
+
+ private async Task> ConnectAsync(
+ Action onCharacteristicChanged,
+ Action onDeviceDisconnected)
+ {
+ using (await _lock.LockAsync())
+ {
+ if (State != BluetoothLEDeviceState.Disconnected)
+ {
+ return null;
+ }
+ _onCharacteristicChanged = onCharacteristicChanged;
+ _onDeviceDisconnected = onDeviceDisconnected;
+
+ State = BluetoothLEDeviceState.Connecting;
+
+ if (Address.TryParseBluetoothAddressString(out var bluetoothAddress))
+ {
+ _bluetoothDevice?.Dispose();
+ _bluetoothDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(bluetoothAddress);
+ }
+
+ if (_bluetoothDevice == null)
+ {
+ InternalDisconnect();
+ return null;
+ }
+
+ _bluetoothDevice.ConnectionStatusChanged += _bluetoothDevice_ConnectionStatusChanged;
+
+ _connectCompletionSource = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously);
+ }
+
+ // enforce connection check
+ await OnConnection();
+
+ var result = await _connectCompletionSource.Task;
+ _connectCompletionSource = null;
+
+ return result;
+ }
+
+ public async Task DisconnectAsync()
+ {
+ using (await _lock.LockAsync())
+ {
+ InternalDisconnect();
+ }
+ }
+
+ private void InternalDisconnect()
+ {
+ _onDeviceDisconnected = null;
+ _onCharacteristicChanged = null;
+
+ if (_services != null)
+ {
+ foreach (var service in _services)
+ {
+ service.Dispose();
+ }
+ _services = null;
+ }
+
+ if (_bluetoothDevice != null)
+ {
+ _bluetoothDevice.ConnectionStatusChanged -= _bluetoothDevice_ConnectionStatusChanged;
+ _bluetoothDevice.Dispose();
+ _bluetoothDevice = null;
+ }
+
+ State = BluetoothLEDeviceState.Disconnected;
+ }
+
+ public async Task EnableNotificationAsync(IGattCharacteristic characteristic, CancellationToken token)
+ {
+ using (await _lock.LockAsync())
+ {
+ if (State == BluetoothLEDeviceState.Connected &&
+ characteristic is BleGattCharacteristic bleGattCharacteristic &&
+ bleGattCharacteristic.CanNotify)
+ {
+ return await bleGattCharacteristic
+ .EnableNotificationAsync(_onCharacteristicChanged);
+ }
+
+ return false;
+ }
+ }
+
+ public async Task DisableNotificationAsync(IGattCharacteristic characteristic, CancellationToken token)
+ {
+ using (await _lock.LockAsync())
+ {
+ if (State == BluetoothLEDeviceState.Connected &&
+ characteristic is BleGattCharacteristic bleGattCharacteristic &&
+ bleGattCharacteristic.CanNotify)
+ {
+ return await bleGattCharacteristic.DisableNotificationAsync();
+ }
+
+ return false;
+ }
+ }
+
+ public async Task WriteAsync(IGattCharacteristic characteristic, byte[] data, CancellationToken token)
+ {
+ using (await _lock.LockAsync())
+ {
+ if (State == BluetoothLEDeviceState.Connected &&
+ characteristic is BleGattCharacteristic bleGattCharacteristic)
+ {
+
+ var result = await bleGattCharacteristic.WriteWithResponseAsync(data);
+ return result.Status == GattCommunicationStatus.Success;
+ }
+ return false;
+ }
+ }
+
+ public async Task WriteNoResponseAsync(IGattCharacteristic characteristic, byte[] data, CancellationToken token)
+ {
+ using (await _lock.LockAsync())
+ {
+ if (State == BluetoothLEDeviceState.Connected &&
+ characteristic is BleGattCharacteristic bleGattCharacteristic)
+ {
+ var result = await bleGattCharacteristic.WriteNoResponseAsync(data);
+ return result == GattCommunicationStatus.Success;
+ }
+ return false;
+ }
+ }
+
+ public async Task ReadAsync(IGattCharacteristic characteristic, CancellationToken token)
+ {
+ using (await _lock.LockAsync())
+ {
+ if (State == BluetoothLEDeviceState.Connected &&
+ characteristic is BleGattCharacteristic bleGattCharacteristic)
+ {
+ var result = await bleGattCharacteristic.ReadValueAsync();
+
+ if (result.Status == GattCommunicationStatus.Success)
+ {
+ return result.Value.ToByteArray();
+ }
+ }
+ return null;
+ }
+ }
+
+ private void _bluetoothDevice_ConnectionStatusChanged(BluetoothLEDevice sender, object args)
+ {
+ // check for a raise condition
+ if (sender != _bluetoothDevice)
+ return;
+
+ // uses lock inside OnXXX methods, execution is not awaited
+ switch (sender.ConnectionStatus)
+ {
+ case BluetoothConnectionStatus.Connected:
+ _ = OnConnection();
+ break;
+
+ case BluetoothConnectionStatus.Disconnected:
+ _ = OnDisconnection();
+ break;
+ }
+ }
+
+ private async Task OnConnection()
+ {
+ using (await _lock.LockAsync())
+ {
+ if (State == BluetoothLEDeviceState.Connecting)
+ {
+ State = BluetoothLEDeviceState.Discovering;
+
+ await DiscoverServices(BluetoothCacheMode.Uncached);
+ }
+ else if (State == BluetoothLEDeviceState.Connected)
+ {
+ // no need to react
+ }
+ else
+ {
+ InternalDisconnect();
+ _connectCompletionSource?.SetResult(null);
+ }
+ }
+ }
+
+ private async Task OnDisconnection()
+ {
+ using (await _lock.LockAsync())
+ {
+ switch (State)
+ {
+ case BluetoothLEDeviceState.Connecting:
+ case BluetoothLEDeviceState.Discovering:
+ InternalDisconnect();
+ _connectCompletionSource?.SetResult(null);
+ break;
+
+ case BluetoothLEDeviceState.Connected:
+
+ var onDeviceDisconnected = _onDeviceDisconnected;
+ InternalDisconnect();
+ onDeviceDisconnected?.Invoke(this);
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ private async Task DiscoverServices(BluetoothCacheMode cacheMode)
+ {
+ // expectation is the method is already called within lock
+ if (_bluetoothDevice != null && State == BluetoothLEDeviceState.Discovering)
+ {
+ var services = new List();
+
+ var availabelServices = await _bluetoothDevice.GetGattServicesAsync(cacheMode);
+
+ if (availabelServices.Status == GattCommunicationStatus.Success)
+ {
+ foreach (var service in availabelServices.Services)
+ {
+ var openStatus = await service.OpenAsync(GattSharingMode.SharedReadAndWrite);
+
+ if (openStatus != GattOpenStatus.Success)
+ {
+ //TODO log
+ continue;
+ }
+
+ var availableCharacteristics = await service.GetCharacteristicsAsync(cacheMode);
+
+ if (availableCharacteristics.Status == GattCommunicationStatus.Success)
+ {
+ var characteristics = availableCharacteristics.Characteristics
+ .Select(ch => new BleGattCharacteristic(ch))
+ .ToList();
+
+ services.Add(new BleGattService(service, characteristics));
+ }
+ }
+ State = BluetoothLEDeviceState.Connected;
+ _connectCompletionSource?.SetResult(services);
+ return true;
+ }
+ }
+ InternalDisconnect();
+ _connectCompletionSource?.SetResult(null);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleGattCharacteristic.cs b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleGattCharacteristic.cs
new file mode 100644
index 00000000..6b5fa711
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleGattCharacteristic.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Threading.Tasks;
+using BrickController2.PlatformServices.BluetoothLE;
+using BrickController2.Windows.Extensions;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace BrickController2.Windows.PlatformServices.BluetoothLE
+{
+ internal class BleGattCharacteristic : IGattCharacteristic
+ {
+ private readonly GattCharacteristic _gattCharacteristic;
+
+ private bool isNotifySet;
+ private Action _valueChangedCallback;
+
+ public BleGattCharacteristic(GattCharacteristic bluetoothGattCharacteristic)
+ {
+ _gattCharacteristic = bluetoothGattCharacteristic;
+ Uuid = bluetoothGattCharacteristic.Uuid;
+ }
+
+ public Guid Uuid { get; }
+
+ public bool CanNotify => _gattCharacteristic != null &&
+ _gattCharacteristic.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Notify);
+
+ public async Task WriteNoResponseAsync(byte[] data)
+ {
+ var buffer = data.ToBuffer();
+
+ return await _gattCharacteristic
+ .WriteValueAsync(buffer, GattWriteOption.WriteWithoutResponse)
+ .AsTask();
+ }
+
+ public async Task WriteWithResponseAsync(byte[] data)
+ {
+ var buffer = data.ToBuffer();
+
+ return await _gattCharacteristic
+ .WriteValueWithResultAsync(buffer, GattWriteOption.WriteWithResponse)
+ .AsTask();
+ }
+
+ public async Task ReadValueAsync()
+ {
+ return await _gattCharacteristic
+ .ReadValueAsync(BluetoothCacheMode.Uncached);
+ }
+
+ internal async Task EnableNotificationAsync(Action callback)
+ {
+ // setup callback before writing client char. so as no event is skipped
+ _gattCharacteristic.ValueChanged += _gattCharacteristic_ValueChanged;
+ _valueChangedCallback = callback;
+
+ var result = await ApplyClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify, isNotifySet)
+ .ConfigureAwait(false);
+
+ isNotifySet = result;
+ return result;
+ }
+
+ internal async Task DisableNotificationAsync()
+ {
+ _valueChangedCallback = null;
+ _gattCharacteristic.ValueChanged -= _gattCharacteristic_ValueChanged;
+
+ var result = await ApplyClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.None, isNotifySet);
+
+ isNotifySet = result;
+ return result;
+ }
+
+ private void _gattCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
+ {
+ if (_valueChangedCallback != null)
+ {
+ var eventData = args.CharacteristicValue.ToByteArray();
+
+ _valueChangedCallback.Invoke(Uuid, eventData);
+ }
+ }
+
+ ///
+ /// Sets the notify / indicate / characteristic
+ ///
+ /// If application was successfull (or has been already applied)
+ private async Task ApplyClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue value, bool currentFlagValue)
+ {
+ bool targetFlagValue = value == GattClientCharacteristicConfigurationDescriptorValue.None ? false : true;
+
+ if (currentFlagValue == targetFlagValue)
+ {
+ // already applied
+ return true;
+ }
+
+ try
+ {
+ // write ClientCharacteristicConfigurationDescriptor in order to get notifications
+ // it's recieved in ValueChanged event handler than
+ var result = await _gattCharacteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync(value);
+ if (result.Status == GattCommunicationStatus.Success)
+ {
+ return true;
+ }
+ }
+ catch (UnauthorizedAccessException)
+ {
+ //TODO report
+ }
+ catch (Exception)
+ {
+ //TODO report
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleGattService.cs b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleGattService.cs
new file mode 100644
index 00000000..7d878930
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleGattService.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using BrickController2.PlatformServices.BluetoothLE;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+
+namespace BrickController2.Windows.PlatformServices.BluetoothLE
+{
+ internal class BleGattService : IGattService, IDisposable
+ {
+ public BleGattService(GattDeviceService bluetoothGattService, IEnumerable characteristics)
+ {
+ BluetoothGattService = bluetoothGattService;
+ Characteristics = characteristics;
+ }
+
+ public GattDeviceService BluetoothGattService { get; }
+ public Guid Uuid => BluetoothGattService.Uuid;
+ public IEnumerable Characteristics { get; }
+
+ private bool disposed;
+
+ public void Dispose()
+ {
+ try
+ {
+ if (!disposed)
+ {
+ disposed = true;
+ BluetoothGattService.Dispose();
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleScanner.cs b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleScanner.cs
new file mode 100644
index 00000000..ea2863d7
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleScanner.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using BrickController2.PlatformServices.BluetoothLE;
+using BrickController2.Windows.Extensions;
+using Windows.Devices.Bluetooth.Advertisement;
+
+namespace BrickController2.Windows.PlatformServices.BluetoothLE
+{
+ public class BleScanner
+ {
+ private readonly Action _scanCallback;
+ private readonly ConcurrentDictionary _deviceNameCache;
+
+ private readonly BluetoothLEAdvertisementWatcher _passiveWatcher;
+ private readonly BluetoothLEAdvertisementWatcher _activeWatcher;
+
+ private static readonly HashSet AdvertismentDataTypes = new HashSet(new[]
+ {
+ BluetoothLEAdvertisementDataTypes.ManufacturerSpecificData,
+ BluetoothLEAdvertisementDataTypes.IncompleteService128BitUuids,
+ BluetoothLEAdvertisementDataTypes.CompleteLocalName
+ });
+
+ public BleScanner(Action scanCallback)
+ {
+ _scanCallback = scanCallback;
+ _deviceNameCache = new ConcurrentDictionary();
+
+ // use passive advertisment for name resolution
+ _passiveWatcher = new BluetoothLEAdvertisementWatcher { ScanningMode = BluetoothLEScanningMode.Passive };
+
+ _passiveWatcher.Received += _passiveWatcher_Received;
+ _passiveWatcher.Stopped += _passiveWatcher_Stopped;
+
+ // use active scanner as ScanResult advertisment processor
+ // because SBrick contains large manufacture data which may not come in single packet with device name
+ _activeWatcher = new BluetoothLEAdvertisementWatcher { ScanningMode = BluetoothLEScanningMode.Active };
+ _activeWatcher.Received += _activeWatcher_Received;
+ }
+
+ public void Start()
+ {
+ _passiveWatcher.Start();
+ _activeWatcher.Start();
+ }
+
+ private void _passiveWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
+ {
+ // simply update device name cache - if valid
+ var deviceName = args.GetLocalName();
+ if (deviceName.IsValidDeviceName())
+ {
+ _deviceNameCache.AddOrUpdate(args.BluetoothAddress, deviceName, (key, oldValue) => deviceName);
+ }
+ }
+
+ private void _passiveWatcher_Stopped(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementWatcherStoppedEventArgs args)
+ {
+ _deviceNameCache.Clear();
+ }
+
+ private void _activeWatcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
+ {
+ if (!args.CanCarryData())
+ {
+ return;
+ }
+ // prefer local name if set, otherwise use cache (where only valid names can be)
+ string deviceName = args.GetLocalName();
+ if (!deviceName.IsValidDeviceName() && !_deviceNameCache.TryGetValue(args.BluetoothAddress, out deviceName))
+ {
+ return;
+ }
+
+ var bluetoothAddress = args.BluetoothAddress.ToBluetoothAddressString();
+
+ var advertismentData = args.Advertisement.DataSections
+ .Where(s => AdvertismentDataTypes.Contains(s.DataType))
+ .ToDictionary(s => s.DataType, s => s.Data.ToByteArray());
+
+ // enrich data with name manually (SBrick do not like CompleteLocalName, but Buwizz3 requires it)
+ if (!advertismentData.ContainsKey(BluetoothLEAdvertisementDataTypes.CompleteLocalName))
+ {
+ advertismentData[BluetoothLEAdvertisementDataTypes.CompleteLocalName] = Encoding.ASCII.GetBytes(deviceName);
+ }
+
+ _scanCallback(new ScanResult(deviceName, bluetoothAddress, advertismentData));
+ }
+
+ public void Stop()
+ {
+ _passiveWatcher.Stop();
+ _activeWatcher.Stop();
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleService.cs b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleService.cs
new file mode 100644
index 00000000..5cbe3d9e
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/BluetoothLE/BleService.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using BrickController2.PlatformServices.BluetoothLE;
+using Windows.Devices.Bluetooth;
+
+namespace BrickController2.Windows.PlatformServices.BluetoothLE
+{
+ public class BleService : IBluetoothLEService
+ {
+ [Flags]
+ private enum BluetoothStatus
+ {
+ None = 0x00,
+ ClassicSupported = 0x01,
+ LowEnergySupported = 0x02,
+
+ AllFeatures = ClassicSupported | LowEnergySupported
+ }
+
+ private bool _isScanning;
+
+ public BleService()
+ {
+ }
+
+ public bool IsBluetoothLESupported => CurrentBluetoothStatus.HasFlag(BluetoothStatus.LowEnergySupported);
+ public bool IsBluetoothOn => CurrentBluetoothStatus.HasFlag(BluetoothStatus.ClassicSupported);
+
+ private BluetoothStatus CurrentBluetoothStatus
+ {
+ get
+ {
+ // synchroniously wait
+ var adapterTask = GetBluetoothAdapter();
+ adapterTask.Wait();
+
+ BluetoothStatus status = (adapterTask.Result?.IsClassicSupported ?? false) ? BluetoothStatus.ClassicSupported : BluetoothStatus.None;
+ status |= (adapterTask.Result?.IsLowEnergySupported ?? false) ? BluetoothStatus.LowEnergySupported : BluetoothStatus.None;
+
+ return status;
+ }
+ }
+
+ private static async Task GetBluetoothAdapter() => await BluetoothAdapter.GetDefaultAsync()
+ .AsTask()
+ .ConfigureAwait(false);
+
+ public async Task ScanDevicesAsync(Action scanCallback, CancellationToken token)
+ {
+ if (_isScanning || CurrentBluetoothStatus != BluetoothStatus.AllFeatures)
+ {
+ return false;
+ }
+
+ try
+ {
+ _isScanning = true;
+ return await NewScanAsync(scanCallback, token);
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ finally
+ {
+ _isScanning = false;
+ }
+ }
+
+ public IBluetoothLEDevice GetKnownDevice(string address)
+ {
+ if (!IsBluetoothLESupported)
+ {
+ return null;
+ }
+
+ return new BleDevice(address);
+ }
+
+ private async Task NewScanAsync(Action scanCallback, CancellationToken token)
+ {
+ try
+ {
+ var leScanner = new BleScanner(scanCallback);
+
+ leScanner.Start();
+
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ token.Register(() =>
+ {
+ leScanner.Stop();
+ tcs.SetResult(true);
+ });
+
+ return await tcs.Task;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.UWP/PlatformServices/DI/PlatformServicesModule.cs
new file mode 100644
index 00000000..884ffadd
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/DI/PlatformServicesModule.cs
@@ -0,0 +1,33 @@
+using Autofac;
+using BrickController2.Windows.PlatformServices.BluetoothLE;
+using BrickController2.PlatformServices.BluetoothLE;
+using BrickController2.PlatformServices.GameController;
+using BrickController2.PlatformServices.Infrared;
+using BrickController2.PlatformServices.Localization;
+using BrickController2.PlatformServices.SharedFileStorage;
+using BrickController2.PlatformServices.Versioning;
+using BrickController2.Windows.PlatformServices.Infrared;
+using BrickController2.Windows.PlatformServices.Versioning;
+using BrickController2.Windows.PlatformServices.Localization;
+using BrickController2.Windows.PlatformServices.GameController;
+using BrickController2.Windows.PlatformServices.SharedFileStorage;
+using BrickController2.Windows.PlatformServices.Permission;
+using BrickController2.PlatformServices.Permission;
+
+namespace BrickController2.Windows.PlatformServices.DI
+{
+ public class PlatformServicesModule : Module
+ {
+ protected override void Load(ContainerBuilder builder)
+ {
+ builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().AsSelf().As().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().As().InstancePerDependency();
+ builder.RegisterType().As().InstancePerDependency();
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/GameController/GameControllerService.cs b/BrickController2/BrickController2.UWP/PlatformServices/GameController/GameControllerService.cs
new file mode 100644
index 00000000..1ec3330d
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/GameController/GameControllerService.cs
@@ -0,0 +1,220 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using BrickController2.PlatformServices.GameController;
+using BrickController2.UI.Services.MainThread;
+using BrickController2.Windows.Extensions;
+using Windows.Gaming.Input;
+using Windows.System;
+using Windows.UI.Core;
+using Windows.UI.Xaml.Input;
+
+namespace BrickController2.Windows.PlatformServices.GameController
+{
+ public class GameControllerService : IGameControllerService
+ {
+
+ private readonly IDictionary _availableControllers = new Dictionary();
+ private readonly object _lockObject = new object();
+ private readonly IMainThreadService _mainThreadService;
+ private CoreWindow _coreWindow;
+
+ private event EventHandler GameControllerEventInternal;
+
+ public GameControllerService(IMainThreadService mainThreadService)
+ {
+ _mainThreadService = mainThreadService;
+ }
+
+ public event EventHandler GameControllerEvent
+ {
+ add
+ {
+ lock (_lockObject)
+ {
+ if (GameControllerEventInternal == null)
+ {
+ InitializeKeyHandling();
+ InitializeControllers();
+ }
+
+ GameControllerEventInternal += value;
+ }
+ }
+
+ remove
+ {
+ lock (_lockObject)
+ {
+ GameControllerEventInternal -= value;
+
+ if (GameControllerEventInternal == null)
+ {
+ TerminateKeyHandling();
+ TerminateControllers();
+ }
+ }
+ }
+ }
+
+ internal void RaiseEvent(IDictionary<(GameControllerEventType, string), float> events)
+ {
+ if (!events.Any())
+ {
+ return;
+ }
+
+ GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(events));
+ }
+
+ internal void RaiseEvent(string deviceId, string key, GameControllerEventType eventType, float value = 0.0f)
+ {
+ GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(eventType, key, value));
+ }
+
+ internal void InitializeComponent(CoreWindow coreWindow)
+ {
+ _coreWindow = coreWindow;
+ }
+
+ private void CoreWindow_KeyDown(CoreWindow sender, KeyEventArgs args)
+ {
+ args.Handled = HandleKeyDown(args.DeviceId, args.VirtualKey, args.KeyStatus);
+ }
+
+ private void CoreWindow_KeyUp(CoreWindow sender, KeyEventArgs args)
+ {
+ args.Handled = HandleKeyUp(args.DeviceId, args.VirtualKey, args.KeyStatus);
+ }
+
+ private bool HandleKeyDown(string deviceId, VirtualKey key, CorePhysicalKeyStatus keyStatus)
+ {
+ if (GamepadMapping.IsGamepadButton(key, out string buttonCode))
+ {
+ if (keyStatus.RepeatCount == 1)
+ {
+ RaiseEvent(deviceId, buttonCode, GameControllerEventType.Button, GamepadMapping.Positive);
+ }
+ return true;
+ }
+ else if (GamepadMapping.IsGamepadAxis(key, out string axisCode, out float axisValue))
+ {
+ if (keyStatus.RepeatCount == 1)
+ {
+ RaiseEvent(deviceId, axisCode, GameControllerEventType.Axis, axisValue);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool HandleKeyUp(string deviceId, VirtualKey key, CorePhysicalKeyStatus keyStatus)
+ {
+ if (GamepadMapping.IsGamepadButton(key, out string buttonCode))
+ {
+ if (keyStatus.RepeatCount == 1)
+ {
+ RaiseEvent(deviceId, buttonCode, GameControllerEventType.Button);
+ }
+ return true;
+ }
+ else if (GamepadMapping.IsGamepadAxis(key, out string axisCode, out float _))
+ {
+ if (keyStatus.RepeatCount == 1)
+ {
+ RaiseEvent(deviceId, axisCode, GameControllerEventType.Axis);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ private void InitializeKeyHandling()
+ {
+ if (_coreWindow == null)
+ return;
+
+ _coreWindow.KeyDown += CoreWindow_KeyDown;
+ _coreWindow.KeyUp += CoreWindow_KeyUp;
+ }
+
+ private void TerminateKeyHandling()
+ {
+ if (_coreWindow == null)
+ return;
+
+ _coreWindow.KeyDown -= CoreWindow_KeyDown;
+ _coreWindow.KeyUp -= CoreWindow_KeyUp;
+ }
+
+ private void InitializeControllers()
+ {
+ // get all available gamepads
+ if (Gamepad.Gamepads.Any())
+ {
+ AddDevices(Gamepad.Gamepads);
+ }
+
+ Gamepad.GamepadRemoved += Gamepad_GamepadRemoved;
+ Gamepad.GamepadAdded += Gamepad_GamepadAdded;
+ }
+
+ private void TerminateControllers()
+ {
+ Gamepad.GamepadRemoved -= Gamepad_GamepadRemoved;
+ Gamepad.GamepadAdded -= Gamepad_GamepadAdded;
+
+ foreach (var controller in _availableControllers.Values)
+ {
+ controller.Stop();
+ }
+ _availableControllers.Clear();
+ }
+
+ private void Gamepad_GamepadRemoved(object sender, Gamepad e)
+ {
+ lock (_lockObject)
+ {
+ var deviceId = e.GetDeviceId();
+
+ if (_availableControllers.TryGetValue(deviceId, out var controller))
+ {
+ _availableControllers.Remove(deviceId);
+
+ // enesure created in UI thread
+ _ = _mainThreadService.RunOnMainThread(() => controller.Stop());
+ }
+ }
+ }
+
+ private void Gamepad_GamepadAdded(object sender, Gamepad e)
+ {
+ _ = AddDevicesInMainThread(new[] { e });
+ }
+
+ private Task AddDevicesInMainThread(IEnumerable gamepads)
+ {
+ // enesure created in UI thread
+ return _mainThreadService.RunOnMainThread(() => AddDevices(gamepads));
+ }
+
+ private void AddDevices(IEnumerable gamepads)
+ {
+ lock (_lockObject)
+ {
+ foreach (var gamepad in gamepads)
+ {
+ var deviceId = gamepad.GetDeviceId();
+
+ var newController = new GamepadController(this, gamepad);
+ _availableControllers[deviceId] = newController;
+
+ newController.Start();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/GameController/GamepadController.cs b/BrickController2/BrickController2.UWP/PlatformServices/GameController/GamepadController.cs
new file mode 100644
index 00000000..b3345a2e
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/GameController/GamepadController.cs
@@ -0,0 +1,98 @@
+using BrickController2.PlatformServices.GameController;
+using BrickController2.Windows.Extensions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Windows.Gaming.Input;
+using Windows.UI.Xaml;
+
+namespace BrickController2.Windows.PlatformServices.GameController
+{
+ internal class GamepadController
+ {
+ private static readonly TimeSpan DefaultInterval = TimeSpan.FromMilliseconds(20);
+
+ private readonly GameControllerService _controllerService;
+ private readonly IDictionary _lastReadingValues = new Dictionary();
+
+ private DispatcherTimer _timer;
+
+ public GamepadController(GameControllerService service, Gamepad gamepad) : this(service, gamepad, DefaultInterval)
+ {
+ }
+
+ private GamepadController(GameControllerService service, Gamepad gamepad, TimeSpan timerInterval)
+ {
+ _controllerService = service ?? throw new ArgumentNullException(nameof(service));
+ UwpController = gamepad ?? throw new ArgumentNullException(nameof(gamepad));
+
+ _timer = new DispatcherTimer
+ {
+ Interval = timerInterval
+ };
+ _timer.Tick += Timer_Tick;
+ }
+
+ public Gamepad UwpController { get; }
+
+ public string DeviceId => UwpController.GetDeviceId();
+
+ public void Start()
+ {
+ _lastReadingValues.Clear();
+
+ // finally start timer
+ _timer.Start();
+ }
+
+ public void Stop()
+ {
+ _timer.Stop();
+
+ _lastReadingValues.Clear();
+ }
+
+ private void Timer_Tick(object sender, object e)
+ {
+ var currentReading = GetCurrentReadings();
+
+ var currentEvents = currentReading
+ .Where(HasChanged)
+ .ToDictionary(x => (GameControllerEventType.Axis, x.AxisName), x => x.Value);
+
+ _controllerService.RaiseEvent(currentEvents);
+ }
+
+ private IEnumerable<(string AxisName, float Value)> GetCurrentReadings()
+ {
+ var currentReading = UwpController.GetCurrentReading();
+
+ yield return GamepadMapping.GetAxisValue(GamepadMapping.XAxis, currentReading.LeftThumbstickX);
+ yield return GamepadMapping.GetAxisValue(GamepadMapping.YAxis, currentReading.LeftThumbstickY);
+ yield return GamepadMapping.GetAxisValue(GamepadMapping.BrakeAxis, currentReading.LeftTrigger);
+ yield return GamepadMapping.GetAxisValue(GamepadMapping.ZAxis, currentReading.RightThumbstickX);
+ yield return GamepadMapping.GetAxisValue(GamepadMapping.RzAxis, currentReading.RightThumbstickY);
+ yield return GamepadMapping.GetAxisValue(GamepadMapping.GasAxis, currentReading.RightTrigger);
+ }
+
+ private static bool AreAlmostEqual(float a, float b)
+ {
+ return Math.Abs(a - b) < 0.001;
+ }
+
+ private bool HasChanged((string AxisName, float Value) readingValue)
+ {
+ if (_lastReadingValues.TryGetValue(readingValue.AxisName, out float lastValue))
+ {
+ if (AreAlmostEqual(readingValue.Value, lastValue))
+ {
+ // axisValue == lastValue
+ return false;
+ }
+ }
+
+ _lastReadingValues[readingValue.AxisName] = readingValue.Value;
+ return true;
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/GameController/GamepadMapping.cs b/BrickController2/BrickController2.UWP/PlatformServices/GameController/GamepadMapping.cs
new file mode 100644
index 00000000..288a0d06
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/GameController/GamepadMapping.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using Windows.System;
+
+namespace BrickController2.Windows.PlatformServices.GameController
+{
+ public static class GamepadMapping
+ {
+ public const float Positive = 1.0f;
+ public const float Negative = -1.0f;
+
+ public const string XAxis = "X";
+ public const string YAxis = "Y";
+ public const string RzAxis = "Rz";
+ public const string ZAxis = "Z";
+ public const string BrakeAxis = "Brake";
+ public const string GasAxis = "Gas";
+
+ ///
+ /// Set of virtual keys related to gamepad buttons
+ ///
+ private static readonly Dictionary GamePadButtonMapping = new Dictionary()
+ {
+ { VirtualKey.GamepadA, "ButtonA" },
+ { VirtualKey.GamepadB, "ButtonB" },
+ { VirtualKey.GamepadX, "ButtonX" },
+ { VirtualKey.GamepadY, "ButtonY" },
+
+ { VirtualKey.GamepadLeftShoulder, "ButtonL1" },
+ { VirtualKey.GamepadRightShoulder, "ButtonR1" },
+ { VirtualKey.GamepadLeftTrigger, "ButtonL2" },
+ { VirtualKey.GamepadRightTrigger, "ButtonR2" },
+
+ { VirtualKey.GamepadMenu, "ButtonStart" },
+ { VirtualKey.GamepadView, "ButtonSelect" },
+
+ { VirtualKey.GamepadLeftThumbstickButton, "ButtonThumbl" },
+ { VirtualKey.GamepadRightThumbstickButton, "ButtonThumbr" },
+ };
+
+ ///
+ /// Set of virtual keys related to gamepad buttons handled as AXIS
+ ///
+ private static readonly Dictionary GamePadAxisMapping = new Dictionary()
+ {
+ { VirtualKey.GamepadDPadUp, ("HatY", Negative) },
+ { VirtualKey.GamepadDPadDown, ("HatY", Positive) },
+ { VirtualKey.GamepadDPadLeft, ("HatX", Negative) },
+ { VirtualKey.GamepadDPadRight, ("HatX", Positive) },
+ };
+
+ ///
+ /// Set of inverted axis
+ ///
+ private static readonly HashSet GamePadInvertedAxis = new HashSet { YAxis, RzAxis };
+
+ public static bool IsGamepadButton(VirtualKey virtualKey, out string buttonCode)
+ {
+ if (GamePadButtonMapping.TryGetValue(virtualKey, out buttonCode))
+ {
+ return true;
+ }
+
+ buttonCode = default;
+ return false;
+ }
+
+ public static bool IsGamepadAxis(VirtualKey virtualKey, out string axisCode, out float axisValue)
+ {
+ if (GamePadAxisMapping.TryGetValue(virtualKey, out (string, float) button))
+ {
+ axisCode = button.Item1;
+ axisValue = button.Item2;
+
+ return true;
+ }
+
+ axisCode = default;
+ axisValue = default;
+
+ return false;
+ }
+
+ public static (string AxisName, float Value) GetAxisValue(string axisName, double value)
+ {
+ if (Math.Abs(value) < 0.05)
+ {
+ return (axisName, 0.0F);
+ }
+
+ float coef = GamePadInvertedAxis.Contains(axisName) ? Negative : Positive;
+
+ if (value > 0.95)
+ {
+ return (axisName, coef);
+ }
+ if (value < -0.95)
+ {
+ return (axisName, -coef);
+ }
+ return (axisName, coef * (float)value);
+ }
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/Infrared/InfraredService.cs b/BrickController2/BrickController2.UWP/PlatformServices/Infrared/InfraredService.cs
new file mode 100644
index 00000000..ba16163e
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/Infrared/InfraredService.cs
@@ -0,0 +1,25 @@
+using BrickController2.PlatformServices.Infrared;
+using System;
+using System.Threading.Tasks;
+
+namespace BrickController2.Windows.PlatformServices.Infrared
+{
+ public class InfraredService : IInfraredService
+ {
+ public InfraredService()
+ {
+ }
+
+ public bool IsInfraredSupported => false;
+
+ public bool IsCarrierFrequencySupported(int carrierFrequency)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SendPacketAsync(int carrierFrequency, int[] packet)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/Localization/LocalizationService.cs b/BrickController2/BrickController2.UWP/PlatformServices/Localization/LocalizationService.cs
new file mode 100644
index 00000000..8c5c85cd
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/Localization/LocalizationService.cs
@@ -0,0 +1,26 @@
+using BrickController2.PlatformServices.Localization;
+using System.Globalization;
+using System.Threading;
+using Xamarin.Forms;
+
+[assembly:Dependency(typeof(BrickController2.Windows.PlatformServices.Localization.LocalizationService))]
+namespace BrickController2.Windows.PlatformServices.Localization
+{
+ public class LocalizationService : ILocalizationService
+ {
+ public CultureInfo CurrentCultureInfo
+ {
+ get
+ {
+ return CultureInfo.CurrentUICulture;
+ }
+
+ set
+ {
+ CultureInfo.CurrentUICulture = value;
+ Thread.CurrentThread.CurrentCulture = value;
+ Thread.CurrentThread.CurrentUICulture = value;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/Permission/BluetoothPermission.cs b/BrickController2/BrickController2.UWP/PlatformServices/Permission/BluetoothPermission.cs
new file mode 100644
index 00000000..8f532c26
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/Permission/BluetoothPermission.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using static Xamarin.Essentials.Permissions;
+using BrickController2.PlatformServices.Permission;
+using System;
+
+namespace BrickController2.Windows.PlatformServices.Permission
+{
+ public class BluetoothPermission : BasePlatformPermission, IBluetoothPermission
+ {
+ protected override Func> RequiredDeclarations => () => new[] { "bluetooth" };
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/Permission/ReadWriteExternalStoragePermission.cs b/BrickController2/BrickController2.UWP/PlatformServices/Permission/ReadWriteExternalStoragePermission.cs
new file mode 100644
index 00000000..596dfc66
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/Permission/ReadWriteExternalStoragePermission.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using static Xamarin.Essentials.Permissions;
+using BrickController2.PlatformServices.Permission;
+using System;
+
+namespace BrickController2.Windows.PlatformServices.Permission
+{
+ public class ReadWriteExternalStoragePermission : BasePlatformPermission, IReadWriteExternalStoragePermission
+ {
+ protected override Func> RequiredDeclarations => () => new[] { "removableStorage" };
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/SharedFileStorage/SharedFileStorageService.cs b/BrickController2/BrickController2.UWP/PlatformServices/SharedFileStorage/SharedFileStorageService.cs
new file mode 100644
index 00000000..1f02fe67
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/SharedFileStorage/SharedFileStorageService.cs
@@ -0,0 +1,15 @@
+using BrickController2.PlatformServices.SharedFileStorage;
+using System;
+using Windows.Storage;
+
+namespace BrickController2.Windows.PlatformServices.SharedFileStorage
+{
+ public class SharedFileStorageService : ISharedFileStorageService
+ {
+ public bool IsSharedStorageAvailable => true;
+
+ public bool IsPermissionGranted { get; set; }
+
+ public string SharedStorageDirectory => ApplicationData.Current.RoamingFolder.Path;
+ }
+}
diff --git a/BrickController2/BrickController2.UWP/PlatformServices/Versioning/VersionService.cs b/BrickController2/BrickController2.UWP/PlatformServices/Versioning/VersionService.cs
new file mode 100644
index 00000000..92c0cda9
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/PlatformServices/Versioning/VersionService.cs
@@ -0,0 +1,29 @@
+using BrickController2.PlatformServices.Versioning;
+using Windows.ApplicationModel;
+
+namespace BrickController2.Windows.PlatformServices.Versioning
+{
+ public class VersionService : IVersionService
+ {
+ public VersionService()
+ {
+
+ }
+
+ public string ApplicationVersion
+ {
+ get
+ {
+ try
+ {
+ var info = Package.Current.Id.Version;
+ return $"{info.Major}.{info.Minor}.{info.Revision}";
+ }
+ catch
+ {
+ return "Unknown version";
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/Properties/AssemblyInfo.cs b/BrickController2/BrickController2.UWP/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..c3b9eed6
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/Properties/AssemblyInfo.cs
@@ -0,0 +1,22 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("BrickController2")]
+[assembly: AssemblyDescription("Cross platform mobile application for controlling Lego creations using a bluetooth gamepad.")]
+[assembly: AssemblyProduct("BrickController2")]
+[assembly: AssemblyCopyright("Copyright © 2021")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+[assembly: AssemblyVersion("1.0.0.0")]
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/Properties/Default.rd.xml b/BrickController2/BrickController2.UWP/Properties/Default.rd.xml
new file mode 100644
index 00000000..7c40ffeb
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/Properties/Default.rd.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/UI/CustomRenderers/ExtendedSliderRenderer.cs b/BrickController2/BrickController2.UWP/UI/CustomRenderers/ExtendedSliderRenderer.cs
new file mode 100644
index 00000000..ed36be99
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/UI/CustomRenderers/ExtendedSliderRenderer.cs
@@ -0,0 +1,31 @@
+using BrickController2.UI.Controls;
+using BrickController2.Windows.UI.CustomRenderers;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.UWP;
+
+[assembly: ExportRenderer(typeof(ExtendedSlider), typeof(ExtendedSliderRenderer))]
+namespace BrickController2.Windows.UI.CustomRenderers
+{
+ public class ExtendedSliderRenderer : SliderRenderer
+ {
+ public ExtendedSliderRenderer() : base()
+ {
+ }
+
+ protected override void OnElementChanged(ElementChangedEventArgs e)
+ {
+ base.OnElementChanged(e);
+
+ if (Element is ExtendedSlider extendedSlider && Control != null)
+ {
+ Control.PointerCaptureLost += (sender, args) => extendedSlider.TouchUp();
+
+ if (extendedSlider.Step > 0)
+ {
+ Control.StepFrequency = extendedSlider.Step;
+ Control.SmallChange = extendedSlider.Step;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/UI/CustomRenderers/ExtendedSwipeViewRenderer.cs b/BrickController2/BrickController2.UWP/UI/CustomRenderers/ExtendedSwipeViewRenderer.cs
new file mode 100644
index 00000000..7b208269
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/UI/CustomRenderers/ExtendedSwipeViewRenderer.cs
@@ -0,0 +1,33 @@
+using BrickController2.Windows.UI.CustomRenderers;
+using System.Linq;
+using Windows.UI.Xaml.Input;
+using Xamarin.Forms;
+using Xamarin.Forms.Platform.UWP;
+
+[assembly: ExportRenderer(typeof(SwipeView), typeof(ExtendedSwipeViewRenderer))]
+namespace BrickController2.Windows.UI.CustomRenderers
+{
+ public class ExtendedSwipeViewRenderer : SwipeViewRenderer
+ {
+ public ExtendedSwipeViewRenderer() : base()
+ {
+ }
+
+ protected override void OnElementChanged(ElementChangedEventArgs e)
+ {
+ base.OnElementChanged(e);
+
+ if (Control != null)
+ {
+ Control.RightTapped += Control_RightTapped;
+ }
+ }
+
+ private void Control_RightTapped(object sender, RightTappedRoutedEventArgs e)
+ {
+ // invoke command of the first left item to suppport deletion (workaround for Windouws without touch controls)
+ var item = Element.LeftItems?.First();
+ item?.Command?.Execute(item?.CommandParameter);
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.UWP/UI/CustomRenderers/NavPageOverrideRenderer.cs b/BrickController2/BrickController2.UWP/UI/CustomRenderers/NavPageOverrideRenderer.cs
new file mode 100644
index 00000000..27db85e8
--- /dev/null
+++ b/BrickController2/BrickController2.UWP/UI/CustomRenderers/NavPageOverrideRenderer.cs
@@ -0,0 +1,51 @@
+using BrickController2.Windows.UI.CustomRenderers;
+using Xamarin.Forms;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Platform.UWP;
+
+[assembly: ExportRenderer(typeof(NavigationPage), typeof(NavPageOverrideRenderer))]
+namespace BrickController2.Windows.UI.CustomRenderers
+{
+ public class NavPageOverrideRenderer : NavigationPageRenderer
+ {
+ public NavPageOverrideRenderer()
+ {
+ }
+
+ private void WorkaroundHideTitle(Page page)
+ {
+ page.Title = string.Empty;
+ }
+
+ protected override void OnElementChanged(VisualElementChangedEventArgs e)
+ {
+ if (e.NewElement is NavigationPage page)
+ {
+ WorkaroundHideTitle(page.CurrentPage);
+ }
+
+ base.OnElementChanged(e);
+ }
+
+ protected override void OnPopRequested(object sender, NavigationRequestedEventArgs e)
+ {
+ WorkaroundHideTitle(e.Page);
+
+ base.OnPopRequested(sender, e);
+ }
+
+ protected override void OnPopToRootRequested(object sender, NavigationRequestedEventArgs e)
+ {
+ WorkaroundHideTitle(e.Page);
+
+ base.OnPopToRootRequested(sender, e);
+ }
+
+ protected override void OnPushRequested(object sender, NavigationRequestedEventArgs e)
+ {
+ WorkaroundHideTitle(e.Page);
+
+ base.OnPushRequested(sender, e);
+ }
+ }
+}
diff --git a/BrickController2/BrickController2/BrickController2.csproj b/BrickController2/BrickController2/BrickController2.csproj
index f87cd62c..3296968e 100644
--- a/BrickController2/BrickController2/BrickController2.csproj
+++ b/BrickController2/BrickController2/BrickController2.csproj
@@ -1,7 +1,7 @@
- netstandard2.1
+ netstandard2.0
latest
@@ -102,7 +102,8 @@
-
+
+
diff --git a/BrickController2/BrickController2/CreationManagement/CreationManager.cs b/BrickController2/BrickController2/CreationManagement/CreationManager.cs
index 4fe0a1d6..05da88ea 100644
--- a/BrickController2/BrickController2/CreationManagement/CreationManager.cs
+++ b/BrickController2/BrickController2/CreationManagement/CreationManager.cs
@@ -7,6 +7,8 @@
using System.IO;
using Newtonsoft.Json;
+using File = BrickController2.FileWorkaround;
+
namespace BrickController2.CreationManagement
{
public class CreationManager : ICreationManager
diff --git a/BrickController2/BrickController2/FileWorkaround.cs b/BrickController2/BrickController2/FileWorkaround.cs
new file mode 100644
index 00000000..b35e2d82
--- /dev/null
+++ b/BrickController2/BrickController2/FileWorkaround.cs
@@ -0,0 +1,39 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BrickController2
+{
+ internal static class FileWorkaround
+ {
+ public static async Task ReadAllTextAsync(string filePath)
+ {
+ using (FileStream sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true))
+ using (StreamReader reader = new StreamReader(sourceStream))
+ {
+ return await reader.ReadToEndAsync();
+ }
+ }
+
+ public static async Task WriteAllTextAsync(string path, string contents)
+ {
+ if (path == null)
+ throw new ArgumentNullException(nameof(path));
+
+ if (string.IsNullOrEmpty(contents))
+ {
+ new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read).Dispose();
+ return;
+ }
+
+ FileStream stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, 4096, FileOptions.Asynchronous);
+
+ using (var sw = new StreamWriter(stream, Encoding.Unicode))
+ {
+ await sw.WriteAsync(contents).ConfigureAwait(false);
+ await sw.FlushAsync().ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/BrickController2/BrickController2/Helpers/FileHelper.cs b/BrickController2/BrickController2/Helpers/FileHelper.cs
index d1c21979..53e04697 100644
--- a/BrickController2/BrickController2/Helpers/FileHelper.cs
+++ b/BrickController2/BrickController2/Helpers/FileHelper.cs
@@ -17,9 +17,7 @@ public static Dictionary EnumerateDirectoryFilesToFilenameMap(st
{
var filePaths = Directory.EnumerateFiles(directoryPath, searchPattern, SearchOption.TopDirectoryOnly);
- var filenameFilepathMap = new Dictionary(filePaths.Select(fp => KeyValuePair.Create(Path.GetFileNameWithoutExtension(fp), fp)));
-
- return filenameFilepathMap;
+ return filePaths.ToDictionary(fp => Path.GetFileNameWithoutExtension(fp), fp => fp);
}
public static bool FilenameValidator(string filename)
diff --git a/BrickController2/BrickController2/UI/Controls/ImageButton.xaml b/BrickController2/BrickController2/UI/Controls/ImageButton.xaml
index 1d86371c..7d810198 100644
--- a/BrickController2/BrickController2/UI/Controls/ImageButton.xaml
+++ b/BrickController2/BrickController2/UI/Controls/ImageButton.xaml
@@ -6,11 +6,11 @@
x:Class="BrickController2.UI.Controls.ImageButton">
-
-
+
+
-
+
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 03b2f9d2..51931f3d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# BrickController Essentials
+# BrickController Essentials
A fork of [BrickController 2](https://github.com/imurvai/brickcontroller2) which has [features](#features-and-fixes) not included in the original application yet.
@@ -8,6 +8,7 @@ Cross platform mobile application for controlling Lego creations using a bluetoo
- Android 4.3+
- iOS 8+
+- Windows 10 (experimental)
## Supported receivers