diff --git a/powered-up.sln b/powered-up.sln index bcddc01..5dd7711 100644 --- a/powered-up.sln +++ b/powered-up.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31112.23 MinimumVisualStudioVersion = 15.0.26124.0 @@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpBrick.PoweredUp.Mobile EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpBrick.PoweredUp.BlueGigaBLE", "src\SharpBrick.PoweredUp.BlueGigaBLE\SharpBrick.PoweredUp.BlueGigaBLE.csproj", "{E0DC0096-D7F1-4995-833D-9A6C0C3F8F98}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpBrick.PoweredUp.TestScript", "test\SharpBrick.PoweredUp.TestScript\SharpBrick.PoweredUp.TestScript.csproj", "{2A100817-6E86-4E58-8183-0EA7F49C0848}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -116,6 +118,18 @@ Global {E0DC0096-D7F1-4995-833D-9A6C0C3F8F98}.Release|x64.Build.0 = Release|Any CPU {E0DC0096-D7F1-4995-833D-9A6C0C3F8F98}.Release|x86.ActiveCfg = Release|Any CPU {E0DC0096-D7F1-4995-833D-9A6C0C3F8F98}.Release|x86.Build.0 = Release|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Debug|x64.ActiveCfg = Debug|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Debug|x64.Build.0 = Debug|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Debug|x86.ActiveCfg = Debug|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Debug|x86.Build.0 = Debug|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Release|Any CPU.Build.0 = Release|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Release|x64.ActiveCfg = Release|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Release|x64.Build.0 = Release|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Release|x86.ActiveCfg = Release|Any CPU + {2A100817-6E86-4E58-8183-0EA7F49C0848}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -128,6 +142,7 @@ Global {E2D7D98D-9F20-4761-B507-D379A530E77D} = {62C31C3D-8ACF-4ED3-A3D8-225536F3AC6D} {F66A2B09-84B6-477D-9B15-926E771C7D80} = {62C31C3D-8ACF-4ED3-A3D8-225536F3AC6D} {E0DC0096-D7F1-4995-833D-9A6C0C3F8F98} = {62C31C3D-8ACF-4ED3-A3D8-225536F3AC6D} + {2A100817-6E86-4E58-8183-0EA7F49C0848} = {39B30145-497F-4AEB-A014-BBF27DA0651A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {34AA1641-ACED-43ED-A0DD-BA88E43A67A8} diff --git a/src/SharpBrick.PoweredUp/Deployment/HubExtensions.cs b/src/SharpBrick.PoweredUp/Deployment/HubExtensions.cs index 56e2462..ee41fb6 100644 --- a/src/SharpBrick.PoweredUp/Deployment/HubExtensions.cs +++ b/src/SharpBrick.PoweredUp/Deployment/HubExtensions.cs @@ -24,6 +24,18 @@ public static async Task VerifyDeploymentModelAsync(this Hub self, Action + /// Verifies the deployment model and waits till it reaches zero deployment errors. + /// + /// + /// Builder infrastructure for the deployment model + /// + public static async Task VerifyDeploymentModelAsync(this Hub self, DeploymentModel model) + { + var awaitable = self.VerifyObservable(model) .Do(LogErrors(self)) .Where(x => x.Length == 0) diff --git a/src/SharpBrick.PoweredUp/PoweredUpHost.cs b/src/SharpBrick.PoweredUp/PoweredUpHost.cs index 16a941d..47cae1e 100644 --- a/src/SharpBrick.PoweredUp/PoweredUpHost.cs +++ b/src/SharpBrick.PoweredUp/PoweredUpHost.cs @@ -67,15 +67,23 @@ public void Discover(Func onDiscovery, CancellationToken token = defa }, token); } - public async Task DiscoverAsync(CancellationToken token = default) + public async Task DiscoverAsync(CancellationToken token = default) where THub : class + => await DiscoverInternalAsync(typeof(THub), token) as THub; + + public async Task DiscoverAsync(Type hubType, CancellationToken token = default) + => await DiscoverInternalAsync(hubType, token); + + private async Task DiscoverInternalAsync(Type hubType, CancellationToken token) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); Discover(hub => { - if (hub is THub tHub) + var currentHubType = hub.GetType(); + + if (currentHubType == hubType) { - tcs.SetResult(tHub); + tcs.SetResult(hub); } return Task.CompletedTask; @@ -83,8 +91,7 @@ public async Task DiscoverAsync(CancellationToken token = default) var hub = await tcs.Task; - _logger.LogInformation($"End DiscoveryAsync for {typeof(THub).Name}"); - + _logger.LogInformation($"End DiscoveryAsync for {hubType.Name}"); return hub; } diff --git a/test/SharpBrick.PoweredUp.TestScript/ITestScript.cs b/test/SharpBrick.PoweredUp.TestScript/ITestScript.cs new file mode 100644 index 0000000..b1b8a09 --- /dev/null +++ b/test/SharpBrick.PoweredUp.TestScript/ITestScript.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using SharpBrick.PoweredUp.Deployment; + +namespace SharpBrick.PoweredUp.TestScript +{ + public interface ITestScript + { + void DefineDeploymentModel(DeploymentModelBuilder builder); + Task ExecuteScriptAsync(Hub hub, TestScriptExecutionContext context); + } +} diff --git a/test/SharpBrick.PoweredUp.TestScript/Program.cs b/test/SharpBrick.PoweredUp.TestScript/Program.cs new file mode 100644 index 0000000..ae5a062 --- /dev/null +++ b/test/SharpBrick.PoweredUp.TestScript/Program.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SharpBrick.PoweredUp.Deployment; +using SharpBrick.PoweredUp.Hubs; + +namespace SharpBrick.PoweredUp.TestScript +{ + class Program + { + static async Task Main(string[] args) + { + var serviceProvider = new ServiceCollection() + .AddLogging(builder => + { + builder.AddConsole(); + }) + .AddPoweredUp() + .AddWinRTBluetooth() // using WinRT Bluetooth on Windows (separate NuGet SharpBrick.PoweredUp.WinRT) + .BuildServiceProvider(); + + var host = serviceProvider.GetService(); + + IEnumerable scripts = new ITestScript[] { + new TechnicMotorTestScript(), + }; + + var context = new TestScriptExecutionContext(serviceProvider.GetService>()); + + foreach (var script in scripts) + { + // Test Script + context.Log.LogInformation($"Execute Script {script.GetType().Name}"); + + // build deployment model + var model = BbuildDeploymentModel(script); + PrintModel(context, model); + + // Accept to execute Test Script + var executeTest = await context.ConfirmAsync("> Confirm to execute Test Script"); + + if (executeTest) + { + context.Log.LogInformation("> Discovering & Connecting Hub"); + var hubType = HubFactory.GetTypeFromSystemType(model.Hubs[0].HubType ?? throw new InvalidOperationException("Specify the hub type in the test script.")); + using var hub = await host.DiscoverAsync(hubType); + await hub.ConnectAsync(); + + context.Log.LogInformation("> Verifying Deployment Model (fix it if necessary)"); + await hub.VerifyDeploymentModelAsync(model); + + context.Log.LogInformation("> Start Test Script"); + await script.ExecuteScriptAsync(hub, context); + + context.Log.LogInformation("> Switch Off Hub"); + await hub.SwitchOffAsync(); + } + else + { + context.Log.LogWarning($"> User decided not to execute Test Script"); + } + } + + } + + private static void PrintModel(TestScriptExecutionContext context, DeploymentModel model) + { + context.Log.LogInformation($"> Deployment Model of Test Script"); + + foreach (var hub in model.Hubs) + { + context.Log.LogInformation($" > Hub: {hub.HubType}"); + + foreach (var device in hub.Devices) + { + context.Log.LogInformation($" > Device: {device.DeviceType} @ {device.PortId}"); + } + } + } + + private static DeploymentModel BbuildDeploymentModel(ITestScript script) + { + var builder = new DeploymentModelBuilder(); + script.DefineDeploymentModel(builder); + var model = builder.Build(); + return model; + } + } +} diff --git a/test/SharpBrick.PoweredUp.TestScript/SharpBrick.PoweredUp.TestScript.csproj b/test/SharpBrick.PoweredUp.TestScript/SharpBrick.PoweredUp.TestScript.csproj new file mode 100644 index 0000000..53509f9 --- /dev/null +++ b/test/SharpBrick.PoweredUp.TestScript/SharpBrick.PoweredUp.TestScript.csproj @@ -0,0 +1,18 @@ + + + + Exe + net5.0-windows10.0.19041.0 + + + + + + + + + + + + + diff --git a/test/SharpBrick.PoweredUp.TestScript/TechnicMotorTestScript.cs b/test/SharpBrick.PoweredUp.TestScript/TechnicMotorTestScript.cs new file mode 100644 index 0000000..0c6001a --- /dev/null +++ b/test/SharpBrick.PoweredUp.TestScript/TechnicMotorTestScript.cs @@ -0,0 +1,181 @@ +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpBrick.PoweredUp.Deployment; + +namespace SharpBrick.PoweredUp.TestScript +{ + public class TechnicMotorTestScript : ITestScript where TTechnicMotor : AbsoluteMotor, IPoweredUpDevice + { + public void DefineDeploymentModel(DeploymentModelBuilder builder) + => builder.AddHub(hubBuilder => + { + hubBuilder.AddDevice(0); + }); + + public async Task ExecuteScriptAsync(Hub hub, TestScriptExecutionContext context) + { + var motor = hub.Port(0).GetDevice(); + + await motor.GotoRealZeroAsync(); + await Task.Delay(2000); + await motor.SetZeroAsync(); + + // TestCase: AbsoluteMotorReportsAbsolutePosition + await motor.TryLockDeviceForCombinedModeNotificationSetupAsync(motor.ModeIndexAbsolutePosition, motor.ModeIndexPosition); + await motor.SetupNotificationAsync(motor.ModeIndexPosition, true, 2); + await motor.SetupNotificationAsync(motor.ModeIndexAbsolutePosition, true, 2); + await motor.UnlockFromCombinedModeNotificationSetupAsync(true); + + await context.ConfirmAsync("AbsoluteMotor.GotoRealZeroAsync: Is in zero position? Adjust Beam to 0°?"); + + await TestCase1_TachoMotorPositionByDegreesAsync(context, motor); + + await TestCase2_TachoMotorExplicitPositionAsync(context, motor); + + await TestCase3_TachoMotorHighSpeedAndFloatingAsync(context, motor); + + await TestCase4_TachoMotorPositiveNegativeSpeedForTimeAsync(context, motor); + + await TestCase5_TachoMotorAccelerationAsync(context, motor); + + await TestCase6_BasicMotorAsync(context, motor); + + await TestCase7_InputAsync(context, motor); + } + + private static async Task TestCase1_TachoMotorPositionByDegreesAsync(TestScriptExecutionContext context, TTechnicMotor motor) + { + context.Log.LogInformation("TachoMotor: Testing positioning by degress"); + + await motor.StartSpeedForDegreesAsync(45, 10, 100); + await Task.Delay(2000); + context.Assert(motor.AbsolutePosition, 42, 48); + context.Assert(motor.Position, 42, 48); + + await motor.StartSpeedForDegreesAsync(45, 10, 100); + await Task.Delay(2000); + context.Assert(motor.AbsolutePosition, 87, 93); + context.Assert(motor.Position, 87, 93); + + await motor.StartSpeedForDegreesAsync(45, 10, 100); + await Task.Delay(2000); + context.Assert(motor.AbsolutePosition, 132, 138); + context.Assert(motor.Position, 132, 138); + + await motor.StartSpeedForDegreesAsync(45, 10, 100); + await Task.Delay(2000); + context.Assert(Math.Abs(motor.AbsolutePosition), 177, 180); + context.Assert(motor.Position, 177, 183); + + await context.ConfirmAsync("TachoMotor.StartSpeedForDegreesAsync: Has moved 40 times CW each by 45°?"); + } + + private static async Task TestCase2_TachoMotorExplicitPositionAsync(TestScriptExecutionContext context, TTechnicMotor motor) + { + context.Log.LogInformation("TachoMotor: Testing explicit position from TachoMotor.SetZero"); + + await motor.GotoPositionAsync(360, 10, 100); + await Task.Delay(2000); + context.Assert(motor.AbsolutePosition, -3, 3); + context.Assert(motor.Position, 357, 363); + + await context.ConfirmAsync("TachoMotor.GotoPositionAsync: has moved CW to zero position?"); + + await motor.GotoPositionAsync(810, 10, 100); + await Task.Delay(4000); + context.Assert(motor.AbsolutePosition, 87, 93); + context.Assert(motor.Position, 807, 813); + + await context.ConfirmAsync("TachoMotor.GotoPositionAsync: Motor has moved CW by 360° + 90° and is exaclty 90° off zero?"); + } + + private static async Task TestCase3_TachoMotorHighSpeedAndFloatingAsync(TestScriptExecutionContext context, TTechnicMotor motor) + { + context.Log.LogInformation("TachoMotor: High different Speed and Floating"); + + await motor.StartSpeedForDegreesAsync(810, -100, 100, SpecialSpeed.Float); + await Task.Delay(2000); + + await context.ConfirmAsync("TachoMotor.StartSpeedForDegreesAsync: High speed CCW turn with floating end state?"); + + await ResetToZeroAsync(context, motor, 2000); + } + + private static async Task TestCase4_TachoMotorPositiveNegativeSpeedForTimeAsync(TestScriptExecutionContext context, TTechnicMotor motor) + { + context.Log.LogInformation("TachoMotor: Positive and Negative Speeds and for Time"); + + await motor.StartSpeedForTimeAsync(1000, 10, 100); + await Task.Delay(2000); + + await context.ConfirmAsync("TachoMotor.StartSpeedForTimeAsync: CW for 1s?"); + + await motor.StartSpeedForTimeAsync(1000, -10, 100); + await Task.Delay(2000); + + await context.ConfirmAsync("TachoMotor.StartSpeedForTimeAsync: CCW for 1s?"); + + await ResetToZeroAsync(context, motor, 3000); + } + + private static async Task TestCase5_TachoMotorAccelerationAsync(TestScriptExecutionContext context, TTechnicMotor motor) + { + context.Log.LogInformation("TachoMotor: Acceleration Profiles"); + + await motor.SetAccelerationTimeAsync(500); + await motor.SetDecelerationTimeAsync(500); + + await motor.StartSpeedForTimeAsync(2000, 50, 100, SpecialSpeed.Brake, SpeedProfiles.AccelerationProfile | SpeedProfiles.DecelerationProfile); + await Task.Delay(4000); + + await context.ConfirmAsync("TachoMotor.SetAccelerationTimeAsync: CW 0.5s each slow start and end?"); + + await ResetToZeroAsync(context, motor, 20_000); + } + + private static async Task TestCase6_BasicMotorAsync(TestScriptExecutionContext context, TTechnicMotor motor) + { + context.Log.LogInformation("TachoMotor: Start Speed & Power"); + + await motor.StartPowerAsync(100); + await Task.Delay(1000); + await motor.StopByBrakeAsync(); + await motor.StartPowerAsync(-100); + await Task.Delay(1000); + await motor.StopByFloatAsync(); + + await context.ConfirmAsync("TachoMotor.StartPowerAsync: CW for 1s, brake, CCW for 1s, floating?"); + + await ResetToZeroAsync(context, motor, 3000); + } + + private async Task TestCase7_InputAsync(TestScriptExecutionContext context, TTechnicMotor motor) + { + context.Log.LogInformation("AbsoluteMotor: Input on Absolute and relative Position"); + + context.Log.LogInformation("Turn 90° clockwise"); + + await motor.AbsolutePositionObservable.Where(x => x.SI > 85 && x.SI < 95).FirstAsync().GetAwaiter(); + + context.Log.LogInformation("Turn 180° counter-clockwise"); + + await motor.AbsolutePositionObservable.Where(x => x.SI < -85 && x.SI > -95).FirstAsync().GetAwaiter(); + + context.Log.LogInformation("Turn 90° counter-clockwise"); + + await motor.PositionObservable.Where(x => x.SI < -175 && x.SI > -185).FirstAsync().GetAwaiter(); + } + + private static async Task ResetToZeroAsync(TestScriptExecutionContext context, TTechnicMotor motor, int expectedTime) + { + await motor.GotoPositionAsync(0, 10, 100); + await Task.Delay(expectedTime); + context.Assert(motor.AbsolutePosition, -3, 3); + context.Assert(motor.Position, -3, 3); + + await context.ConfirmAsync("TachoMotor.GotoPositionAsync: has moved to zero position?"); + } + } +} diff --git a/test/SharpBrick.PoweredUp.TestScript/TestScriptExecutionContext.cs b/test/SharpBrick.PoweredUp.TestScript/TestScriptExecutionContext.cs new file mode 100644 index 0000000..260483f --- /dev/null +++ b/test/SharpBrick.PoweredUp.TestScript/TestScriptExecutionContext.cs @@ -0,0 +1,47 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace SharpBrick.PoweredUp.TestScript +{ + public class TestScriptExecutionContext + { + public TestScriptExecutionContext(ILogger log) + { + Log = log ?? throw new ArgumentNullException(nameof(log)); + } + + public ILogger Log { get; } + public Hub CurrentHub { get; set; } + + public async Task ConfirmAsync(string question) + { + Console.Write(question + " (Y/n)"); + + var confirm = Console.ReadLine().Trim().ToUpperInvariant() != "N"; + + if (confirm) + { + Log.LogInformation($"[OK] by User: {question}"); + } + else + { + Log.LogError($"[FAIL] by User {question}"); + } + + return confirm; + } + + public void Assert(int value, int min, int max) + { + if (value >= min && value <= max) + { + Log.LogInformation($"[OK] by Assert within expectations: {min} <= {value} <= {max}"); + } + else + { + Log.LogError($"[FAIL] by Assert outside expectations: {min} <= {value} <= {max}"); + } + } + } +}