# EM Closed-Loop Movement Test Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add an `EM闭环测试` MovementTest that plans one complete EM direction segment once, freezes it, adapts it to `Trajectory2D`, and tracks it with the imported Stanley/PID/GCP controller until that segment stops. **Architecture:** Keep the planning and controller models separate. Restore the reference controller's smallest portable runtime dependency set inside `ClumsyPilot`, add a pure `EmTrajectory`-to-`Trajectory2D` adapter, then make a session runner reuse the existing trajectory-observation bootstrap and one `TrajectoryObservationController.StartCycle` call before handing the frozen trajectory to `TrajectoryTrackingMovement`. **Tech Stack:** C# 10, .NET Standard 2.0 plugin, .NET 10 Windows verification host, Clumsy `MovementTest`/`DriveTask`, existing EM planner and OSQP runtime. ## Global Constraints - The MovementTest registration name is exactly `EM闭环测试`. - Plan exactly once in `EmPlanningScope.FullDirectionSegment`; do not replan or replace the trajectory while driving. - Execute only the first direction segment. A gear-switch terminal stops and ends this test. - Shared/control boundaries use SI units: m, m/s, rad, rad/s; body X is forward, body Y is left, counter-clockwise is positive. - Preserve signed longitudinal velocity: forward positive and reverse negative. - Do not change velocity/steering signs, CAN IDs, remote mappings, mechanical limits, or mode-switch policy. - Do not overwrite unrelated dirty files. Stage only the exact files named by each task. - Real-vehicle validation must remain low-speed, short-distance, in an open area, with hardware emergency stop available. --- ## File Map ### Existing user files retained and brought under version control - `ClumsyPilot/Control/**/*.cs` — Stanley/PID/GCP controller implementation already copied from the reference project. - `ClumsyPilot/Trajectory/**/*.cs` — controller-side trajectory and projection implementation already copied from the reference project. ### Portable runtime dependencies copied from the reference project - `ClumsyPilot/Shared/Mathematics/AngleMath.cs` — angle normalization and conversion. - `ClumsyPilot/Shared/Mathematics/InterpolationMath.cs` — scalar interpolation. - `ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs` — world/body transforms. - `ClumsyPilot/Shared/Models/ChassisCommand.cs` — `Pose2D`, `Twist2D`, and chassis command value types. - `ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs` — SI/GCP boundary over the existing `MultiWheelChassis`. - `ClumsyPilot/StateEstimation/IVehicleStateProvider.cs` — controller state-source boundary. - `ClumsyPilot/StateEstimation/VehicleState.cs` — immutable controller state. - `ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs` — velocity filter primitive. - `ClumsyPilot/StateEstimation/VelocityEstimator2D.cs` — Detour velocity estimation. - `ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs` — live Detour state source. - `ClumsyPilot/Movements/TrajectoryTrackingMovement.cs` — `DriveTask` movement wrapper around the geometric controller. ### New EM closed-loop files - `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs` — pure model adapter. - `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs` — UI input, session ownership, one-shot planning, controller handoff, and stop behavior. - `ClumsyPilot/tests/EMPlannerVerificationHost/EmControlTrajectoryAdapterChecks.cs` — adapter behavior checks. - `ClumsyPilot/tests/verify_em_closed_loop_movement.ps1` — MovementTest registration and one-shot lifecycle checks. - `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` — add the `em-control-adapter` verification command. - `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` — operator procedure and test boundary. --- ### Task 1: Restore the controller's portable runtime dependency chain **Files:** - Add and track: `ClumsyPilot/Control/**/*.cs` - Add and track: `ClumsyPilot/Trajectory/**/*.cs` - Create: `ClumsyPilot/Shared/Mathematics/AngleMath.cs` - Create: `ClumsyPilot/Shared/Mathematics/InterpolationMath.cs` - Create: `ClumsyPilot/Shared/Mathematics/FrameTransform2D.cs` - Create: `ClumsyPilot/Shared/Models/ChassisCommand.cs` - Create: `ClumsyPilot/Shared/Chassis/MultiWheelChassisAdapter.cs` - Create: `ClumsyPilot/StateEstimation/IVehicleStateProvider.cs` - Create: `ClumsyPilot/StateEstimation/VehicleState.cs` - Create: `ClumsyPilot/StateEstimation/FirstOrderLowPassFilter.cs` - Create: `ClumsyPilot/StateEstimation/VelocityEstimator2D.cs` - Create: `ClumsyPilot/StateEstimation/DetourVehicleStateProvider.cs` - Create: `ClumsyPilot/Movements/TrajectoryTrackingMovement.cs` **Interfaces:** - Produces: `MyParking.Shared.Pose2D`, `Twist2D`, `AngleMath`, `MultiWheelChassisAdapter`. - Produces: `MultiWheelC.StateEstimation.IVehicleStateProvider` and `DetourVehicleStateProvider`. - Produces: `MultiWheelC.Trajectory.Trajectory2D` and `TrajectoryPoint`. - Produces: `MultiWheelC.TrajectoryTrackingMovement : MovementDefinition` with public `Trajectory`, `StateProvider`, `CycleObserver`, and safety-limit fields. - [ ] **Step 1: Re-run the failing integration build** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore ``` Expected: FAIL with missing `MyParking.Shared`, `MultiWheelC.StateEstimation`, `VehicleState`, and `MultiWheelChassisAdapter`. This proves the partial controller import is not silently excluded from the plugin build. - [ ] **Step 2: Add the exact portable reference sources** Read each source from: ```text D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\Shared\... D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\MultiWheelC\StateEstimation\... D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\MultiWheelC\Movements\TrajectoryTrackingMovement.cs ``` Add the files at the destination paths in this task without changing namespaces or logic. Do not import `FleetKinematics`, experiment recorders, composite movements, rotation movements, or wheel-feedback providers because `TrajectoryTrackingMovement` does not consume them. - [ ] **Step 3: Prove existing controller/trajectory files still match the reference implementation** Run: ```powershell $reference = 'D:\Users\Desktop\项目\prakrobot\停车机器人-合并测试\parkr_shen\MultiWheelC' $local = (Resolve-Path '.\ClumsyPilot').Path Get-ChildItem '.\ClumsyPilot\Control','.\ClumsyPilot\Trajectory' -File -Recurse | ForEach-Object { $relative = $_.FullName.Substring($local.Length + 1) $source = Join-Path $reference $relative if (-not (Test-Path $source) -or (Get-FileHash $_.FullName).Hash -ne (Get-FileHash $source).Hash) { throw "Reference mismatch: $relative" } } ``` Expected: exit 0 and no mismatch. - [ ] **Step 4: Run the build to verify the dependency chain compiles** Run: ```powershell dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore ``` Expected: PASS with zero errors. Record warnings verbatim; do not hide them. - [ ] **Step 5: Commit only the controller runtime integration** ```powershell git add -- ClumsyPilot/Control ClumsyPilot/Trajectory ClumsyPilot/Shared ClumsyPilot/StateEstimation ClumsyPilot/Movements/TrajectoryTrackingMovement.cs git diff --cached --check git commit -m "feat: integrate trajectory tracking controller runtime" ``` --- ### Task 2: Adapt a frozen EM trajectory to `Trajectory2D` **Files:** - Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs` - Create: `ClumsyPilot/tests/EMPlannerVerificationHost/EmControlTrajectoryAdapterChecks.cs` - Modify: `ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs` **Interfaces:** - Consumes: `EmTrajectory` with validated SI-unit `EmTrajectoryPoint` values. - Produces: `public sealed class EmControlTrajectoryAdapter`. - Produces: `public Trajectory2D Create(EmTrajectory trajectory)`. - Duplicate-position threshold: `1e-6 m`, matching `Trajectory2D`'s minimum valid projection segment. - [ ] **Step 1: Register the adapter check before implementing the adapter** Extend `Program.cs` argument validation with `em-control-adapter`, and add: ```csharp if (args[0] == "em-control-adapter" || args[0] == "em-all") { EmControlTrajectoryAdapterChecks.Run(); Console.WriteLine("PASS em-control-adapter"); } ``` Create `EmControlTrajectoryAdapterChecks.cs` with checks that build real `EmTrajectory` objects and assert: ```csharp internal static void Run() { MapsFieldsAndRebuildsGeometricArcLength(); KeepsReverseVelocityNegative(); CollapsesTerminalHoldAndKeepsItsZeroSpeed(); RejectsFewerThanTwoDistinctPositions(); } ``` The forward mapping check must assert all of these exact relations: ```csharp Trajectory2D actual = new EmControlTrajectoryAdapter().Create(source); Require(actual.Count == 3, "adapter point count"); RequireClose(actual[0].ArcLengthMeters, 0d, "first arc"); RequireClose(actual[1].ArcLengthMeters, 0.5d, "second arc"); RequireClose(actual[2].ArcLengthMeters, 1.0d, "third arc"); RequireClose(actual[1].PoseInWorld.XMeters, source.Points[1].X, "x"); RequireClose(actual[1].PoseInWorld.YMeters, source.Points[1].Y, "y"); RequireClose(actual[1].PoseInWorld.YawRadians, source.Points[1].Yaw, "yaw"); RequireClose(actual[1].CurvaturePerMeter, source.Points[1].VehicleCurvature, "curvature"); RequireClose(actual[1].ReferenceSpeedMetersPerSecond, source.Points[1].SignedLongitudinalVelocity, "signed speed"); ``` For the terminal-hold case, provide two final EM samples at the same pose where the later sample has zero speed, then assert that the output contains one terminal position and `EndPoint.ReferenceSpeedMetersPerSecond == 0d`. - [ ] **Step 2: Run the new check and verify RED** Run: ```powershell dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- em-control-adapter ``` Expected: FAIL to compile because `EmControlTrajectoryAdapter` does not exist. - [ ] **Step 3: Implement the minimum adapter** Implement this public surface and algorithm: ```csharp public sealed class EmControlTrajectoryAdapter { private const double MinimumSegmentLengthMeters = 1e-6; public Trajectory2D Create(EmTrajectory trajectory) { if (trajectory == null) throw new ArgumentNullException(nameof(trajectory)); var points = new List(trajectory.Points.Count); double arcLength = 0d; for (int index = 0; index < trajectory.Points.Count; index++) { EmTrajectoryPoint source = trajectory.Points[index]; var pose = new MyParking.Shared.Pose2D(source.X, source.Y, source.Yaw); var converted = new TrajectoryPoint( arcLength, pose, source.VehicleCurvature, source.SignedLongitudinalVelocity); if (points.Count == 0) { points.Add(converted); continue; } TrajectoryPoint previous = points[points.Count - 1]; double dx = pose.XMeters - previous.PoseInWorld.XMeters; double dy = pose.YMeters - previous.PoseInWorld.YMeters; double distance = Math.Sqrt(dx * dx + dy * dy); if (distance < MinimumSegmentLengthMeters) { points[points.Count - 1] = new TrajectoryPoint( previous.ArcLengthMeters, pose, source.VehicleCurvature, source.SignedLongitudinalVelocity); continue; } arcLength += distance; points.Add(new TrajectoryPoint( arcLength, pose, source.VehicleCurvature, source.SignedLongitudinalVelocity)); } if (points.Count < 2) throw new ArgumentException( "EM轨迹至少需要包含两个不同位置的有效控制点。", nameof(trajectory)); return new Trajectory2D(points); } } ``` Include `/// ` comments on the class and public method, explicitly stating the unit and signed-speed boundary. - [ ] **Step 4: Run adapter checks and the existing EM suite** Run: ```powershell dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- em-control-adapter dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- trajectory-observation ``` Expected: both commands PASS. - [ ] **Step 5: Commit the adapter and checks** ```powershell git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/EmControlTrajectoryAdapter.cs ClumsyPilot/tests/EMPlannerVerificationHost/EmControlTrajectoryAdapterChecks.cs ClumsyPilot/tests/EMPlannerVerificationHost/Program.cs git diff --cached --check git commit -m "feat: adapt EM trajectory for geometric controller" ``` --- ### Task 3: Add the one-shot, single-direction `EM闭环测试` **Files:** - Create: `ClumsyPilot/tests/verify_em_closed_loop_movement.ps1` - Create: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs` **Interfaces:** - Consumes: `TrajectoryObservationSetupFactory.CreateBootstrapJob(...)`. - Consumes: `TrajectoryObservationBootstrapper.Bootstrap(...)`. - Consumes exactly one call to `TrajectoryObservationController.StartCycle(...)`. - Consumes: `EmControlTrajectoryAdapter.Create(...)`. - Produces: `[MovementTest(name = "EM闭环测试")] public sealed class EmClosedLoopMovementTest : MovementTest`. - Produces: `Test()` for session start and `TestStop()` for idempotent cancellation/stop. - [ ] **Step 1: Write the structural/lifecycle verification first** Create `verify_em_closed_loop_movement.ps1` so it reads the new source file and fails unless all conditions hold: ```powershell $sourcePath = Join-Path $PSScriptRoot '..\ParkrobTrajplanner\tarjplanner_movementtest\MovementTest.EmClosedLoopTest.cs' if (-not (Test-Path -LiteralPath $sourcePath)) { throw 'EM closed-loop MovementTest source is missing.' } $source = Get-Content -Raw -Encoding UTF8 -LiteralPath $sourcePath $required = @( '\[MovementTest\(name = "EM闭环测试"\)\]', 'EmPlanningScope\.FullDirectionSegment', 'TrajectoryObservationBootstrapper', '\.StartCycle\(', 'EmControlTrajectoryAdapter', 'TrajectoryTrackingMovement', 'public override void TestStop\(\)', '\.Cancel\(\)', '\.Stop\(\)' ) foreach ($pattern in $required) { if ($source -notmatch $pattern) { throw "Missing required pattern: $pattern" } } if ([regex]::Matches($source, '\.StartCycle\(').Count -ne 1) { throw 'EM closed-loop test must contain exactly one planning-cycle call site.' } if ($source -match 'TrajectoryObservationLoop|ReplanPeriod|while\s*\(true\).*StartCycle') { throw 'EM closed-loop test must not contain a replanning loop.' } Write-Output 'EM closed-loop MovementTest checks passed.' ``` - [ ] **Step 2: Run the verification and confirm RED** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_em_closed_loop_movement.ps1 ``` Expected: FAIL with `EM closed-loop MovementTest source is missing.` - [ ] **Step 3: Implement the MovementTest input surface** Add `EmClosedLoopMovementTest` with the same goal, map, obstacle, vehicle, solver, and output-step defaults used by `TrajectoryObservationMovementTest`. Set control defaults explicitly: ```csharp public double MaximumCommandSpeedMetersPerSecond = 0.20d; public double MaximumDistanceToTrajectoryMeters = 0.30d; public double ExecutionTimeoutSeconds = 120d; public float WheelAlignmentToleranceDegrees = 2f; ``` `Test()` must validate inputs, create a validated settings snapshot with: ```csharp PlanningScope = EmPlanningScope.FullDirectionSegment ``` and start one session through `EmClosedLoopMovementTestRunner.Start(...)`. `TestStop()` must call only: ```csharp EmClosedLoopMovementTestRunner.Stop(); ``` Use the existing observation test's culture-aware finite-number and bounded-obstacle input behavior. Keep those helpers private to the new test so the user's modified observation file is not edited. - [ ] **Step 4: Implement idempotent session ownership and one-shot planning** Use a static runner guarded by one lock. Store one `CancellationTokenSource`, one background `Task`, one active `DriveTask`, and a monotonically increasing session ID. The planning body must follow this exact sequence: ```csharp VehicleMotionState initialState = ReadPlanningState(); CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob( initialState.Pose, goal, settings, obstacles, obstacleSnapshotVersion); TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper().Bootstrap(job, token); if (!bootstrap.Succeeded) throw new InvalidOperationException(bootstrap.FailureReason); var controller = new TrajectoryObservationController( bootstrap, settings, new EmPlanningService(new OsqpNativeSolver()), "em-closed-loop-" + sessionId.ToString(CultureInfo.InvariantCulture)); PlanningCycleResult cycle = await controller.StartCycle( initialState.CapturedAtUtc, initialState, token).ConfigureAwait(false); if (!cycle.Published || controller.PublishedTrajectory == null) throw new InvalidOperationException( string.IsNullOrWhiteSpace(cycle.Result.FailureReason) ? cycle.Diagnostic : cycle.Result.FailureReason); Trajectory2D trajectory = new EmControlTrajectoryAdapter().Create(controller.PublishedTrajectory); ``` There must be no observation loop and no second `StartCycle` call. Log the EM point count, controller point count, geometric length, segment index, direction, and terminal type before control starts. Create `EmClosedLoopWheelSafety.AreWheelsForward(float toleranceDegrees)` in the same file using `MultiWheelChassisAdapter.AreParallelWheelsAligned(0d, toleranceRadians)`. If this check fails, throw before constructing `DriveTask`. Create and run the controller movement as: ```csharp var movement = new TrajectoryTrackingMovement { Trajectory = trajectory, MaximumCommandSpeedMetersPerSecond = maximumCommandSpeedMetersPerSecond, MaximumDistanceToTrajectoryMeters = maximumDistanceToTrajectoryMeters, ExecutionTimeoutSeconds = executionTimeoutSeconds }; var driveTask = new DriveTask(movement.Get()); ``` Publish `driveTask` under the session lock only if the session is still current and not cancelled. Otherwise stop it immediately. For a current session call `driveTask.Wait()` on the background worker, then report that the first direction segment ended. A `GearSwitch` terminal is reported as “stopped at gear-switch boundary; next segment was not started.” `Stop()` must atomically clear the active references, call `CancellationTokenSource.Cancel()`, and call `DriveTask.Stop()`. The completion continuation must clear only references that still belong to its own session. - [ ] **Step 5: Run structural verification and compile** Run: ```powershell powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_em_closed_loop_movement.ps1 dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore ``` Expected: structural check PASS; build PASS with zero errors. - [ ] **Step 6: Commit the MovementTest** ```powershell git add -- ClumsyPilot/tests/verify_em_closed_loop_movement.ps1 ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.EmClosedLoopTest.cs git diff --cached --check git commit -m "feat: add one-shot EM closed-loop movement test" ``` --- ### Task 4: Document operation and run the complete non-hardware verification **Files:** - Modify: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md` **Interfaces:** - Documents: test selection, inputs, one-shot/single-direction boundary, `TestStop`, low-speed first-run procedure, and limits of automated validation. - [ ] **Step 1: Add the operator-facing section** Add a section named `EM闭环测试` containing these exact operational facts: ```text - 启动时只规划一次,并冻结首次成功的完整方向段轨迹。 - 控制执行期间不重规划、不切换轨迹。 - 到达目标或换向边界后停车;本测试不启动下一方向段。 - 默认控制速度上限为 0.20 m/s。 - 规划、适配或车轮方向检查失败时不会启动底盘控制。 - TestStop 会同时取消规划并停止活动控制任务。 - 首次实车测试使用空旷环境、短距离单方向目标,并确保硬件急停可用。 ``` - [ ] **Step 2: Run all targeted checks** Run: ```powershell dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- em-control-adapter dotnet run --project .\ClumsyPilot\tests\EMPlannerVerificationHost\EMPlannerVerificationHost.csproj -- trajectory-observation powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_em_closed_loop_movement.ps1 dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore ``` Expected: every command exits 0. Report warning counts from the build. - [ ] **Step 3: Verify scope and source hygiene** Run: ```powershell git diff --check git status --short git diff --no-ext-diff -- ClumsyPilot/Control ClumsyPilot/Trajectory ClumsyPilot/Shared ClumsyPilot/StateEstimation ClumsyPilot/Movements ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest ClumsyPilot/tests ``` Expected: no whitespace errors; only files named by this plan are staged or changed by this work. Existing unrelated modifications remain untouched. - [ ] **Step 4: Commit documentation** ```powershell git add -- ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md git diff --cached --check git commit -m "docs: explain EM closed-loop test operation" ``` - [ ] **Step 5: Hand off real-vehicle validation without claiming it passed** Report separately: ```text Automated status: adapter checks, observation regressions, structural lifecycle check, and plugin build. Hardware status: not run by automated verification; requires a low-speed, short, single-direction field test with emergency stop available. ```