fix: complete EM observation final review
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# EM observation MovementTest final-fix report
|
||||
|
||||
Date: 2026-08-04
|
||||
|
||||
Reviewed base/head: `d0e673b`
|
||||
|
||||
Scope: final-review fixes for the observe-only `TrajectoryObservationMovementTest`
|
||||
|
||||
## Outcome
|
||||
|
||||
All final-review findings were addressed without adding any chassis, motor, steering, brake, wheel, adapter, or geometric-controller write call. The runtime hardware access remains limited to:
|
||||
|
||||
- `DetourInterface.getCartLocation()`
|
||||
- `BasicPilotBase.Chassis.GetCarSpeed(true)`
|
||||
|
||||
The dirty worktree contained extensive unrelated changes before this fix wave. They were not edited, staged, or reverted as part of this work.
|
||||
|
||||
## Modified files
|
||||
|
||||
- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs`
|
||||
- Added the four public editable vehicle fields with Task-1 defaults.
|
||||
- Copies all vehicle fields into a validated settings snapshot before the worker starts.
|
||||
- Replaced the per-cycle planner `await` with a non-blocking observation tick coordinator.
|
||||
- Uses the freshly captured state timestamp for planning and observation.
|
||||
- Draws and session-guard logs on every configured observer tick while planning is in flight.
|
||||
- Clears all three painter layers for a current-session runtime fault under `SessionSync`.
|
||||
- Preserves the intentional bootstrap-failure diagnostic view.
|
||||
- Passes the exact GearSwitch wait state to the world presentation without changing segment `0`.
|
||||
- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs`
|
||||
- Added `CreateValidatedSnapshot()` to copy and validate all map, cadence, and vehicle settings.
|
||||
- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPipeline.cs`
|
||||
- Added `TrajectoryObservationLoop`/`TrajectoryObservationLoopTick` for one asynchronous in-flight planning cycle plus non-blocking observer ticks.
|
||||
- Added a Painter-free session-end decision model.
|
||||
- Added a pure GearSwitch runtime-state model containing the exact operator notice.
|
||||
- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationPresentation.cs`
|
||||
- Draws the exact GearSwitch wait notice in the existing World layer.
|
||||
- `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md`
|
||||
- Documents all four vehicle inputs, units, defaults, snapshot behavior, asynchronous cadence, and fault/bootstrap cleanup behavior.
|
||||
- `ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs`
|
||||
- Added delayed rolling-planner cadence, lifecycle cleanup, GearSwitch presentation, vehicle defaults/snapshot, full settings validation, fresh state-time, and valid non-empty obstacle-source coverage.
|
||||
- `.superpowers/sdd/final-fix-report.md`
|
||||
- This evidence and self-review report.
|
||||
|
||||
## Finding-by-finding resolution
|
||||
|
||||
### 1. Planner latency no longer blocks observation cadence
|
||||
|
||||
`TrajectoryObservationLoop.Tick(...)` polls a completed planning task but never waits for an incomplete one. It permits at most one planner task in flight, observes the controller's atomically published trajectory using the current tick's state and timestamp, and returns a tick record for drawing and logging. The host calls it after every configured delay, draws all layers, and invokes `LogIfCurrent(...)` for every tick.
|
||||
|
||||
The deterministic regression first establishes an existing published trajectory, starts a second planning call that is held by `ManualResetEventSlim`, and proves that a later observer tick:
|
||||
|
||||
- returns before the planner is released;
|
||||
- retains the existing published trajectory;
|
||||
- uses the later state sequence and timestamp;
|
||||
- is eligible for session-guarded logging;
|
||||
- consumes the planning result after release without reusing the pre-plan state/time.
|
||||
|
||||
There is no planner continuation that logs results. A result is consumed only by a tick after its cancellation check, and all host drawing/logging remains guarded by the active session ID.
|
||||
|
||||
### 2. Runtime faults clear stale painter layers
|
||||
|
||||
`ClearAndLogRuntimeFaultIfCurrent(...)` acquires `SessionSync`, verifies the session ID, clears World/LS/ST together through `Presentation.ClearAll()`, and then reports the fault. The pure lifecycle model specifies:
|
||||
|
||||
- bootstrap failure: preserve the diagnostic view;
|
||||
- runtime fault: clear all layers;
|
||||
- cancellation: clear all layers.
|
||||
|
||||
Bootstrap failure still follows the normal successful-task completion path after drawing its diagnostic world view, so the fault cleanup path does not erase it.
|
||||
|
||||
### 3. Vehicle inputs are editable, validated, and frozen
|
||||
|
||||
The MovementTest now exposes:
|
||||
|
||||
| Field | Default |
|
||||
| --- | ---: |
|
||||
| `VehicleLengthMeters` | `0.80d` |
|
||||
| `VehicleWidthMeters` | `0.60d` |
|
||||
| `SafetyMarginMeters` | `0.05d` |
|
||||
| `MaximumCurvaturePerMeter` | `1d / 1.20d` |
|
||||
|
||||
All four are copied into `TrajectoryObservationSettings` before `CreateValidatedSnapshot()` validates and copies every setting. Tests mutate the original settings after snapshot creation and prove that the bootstrap vehicle retains the configured values. Validation coverage includes map padding, resolution, replan period, observer period, length, width, safety margin, and maximum curvature.
|
||||
|
||||
### 4. GearSwitch wait state is painted
|
||||
|
||||
`TrajectoryObservationRuntimeState.Create(...)` enters the wait state only when a GearSwitch trajectory reaches its final `TimeFromStart`. Its `WorldNotice` is exactly:
|
||||
|
||||
`等待真实档位/方向确认;观察模式不会推进下一方向段`
|
||||
|
||||
The host passes that state to `DrawWorld(...)`, which draws it in `TrajectoryObserver.World`. The controller continues to plan and observe segment `0`; no direction-change action or segment advancement was added.
|
||||
|
||||
### 5. Coverage additions
|
||||
|
||||
The verification host now proves that a valid non-empty obstacle list creates exactly one required `trajectory-observer-manual` source, preserves the positive snapshot version, projects as `Applied`, and retains its geometry. The existing forbidden-call audit remains intact.
|
||||
|
||||
## RED/GREEN evidence
|
||||
|
||||
Command used for each focused cycle:
|
||||
|
||||
`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`
|
||||
|
||||
| Regression | RED evidence | GREEN evidence |
|
||||
| --- | --- | --- |
|
||||
| Delayed planner cadence | Exit `1`; CS0246 for missing `TrajectoryObservationLoop` and `TrajectoryObservationLoopTick`. | Exit `0`; `PASS trajectory-observation`. The strengthened version delays the second rolling plan and observes the existing first publication. |
|
||||
| Every-tick logging signal | Exit `1`; CS1061 for missing `TrajectoryObservationLoopTick.ShouldLog`. | Exit `0`; `PASS trajectory-observation`. |
|
||||
| Session cleanup decisions | Exit `1`; missing `TrajectoryObservationSessionLifecycle` and `TrajectoryObservationSessionEndReason`. | Exit `0`; `PASS trajectory-observation`. |
|
||||
| Vehicle input/snapshot contract | Exit `1`; missing four MovementTest fields and `CreateValidatedSnapshot()`; the first draft also exposed an inappropriate direct `MovementTest`/`ClumsyCore` test dependency, so the default-field check was corrected to the existing UTF-8 source seam before implementation. | Exit `0`; `PASS trajectory-observation`. Invalid-setting and valid-obstacle checks run in the same focused suite. |
|
||||
| GearSwitch world state | Exit `1`; CS0246/missing `TrajectoryObservationRuntimeState`. | Exit `0`; `PASS trajectory-observation`. |
|
||||
| Fresh observation timestamp | Exit `1`; assertion `observer host uses the fresh state snapshot time for each observation tick was false`. | Exit `0`; `PASS trajectory-observation`. |
|
||||
|
||||
## Final verification evidence
|
||||
|
||||
### Focused trajectory observation
|
||||
|
||||
Command:
|
||||
|
||||
`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- trajectory-observation`
|
||||
|
||||
Result: exit `0`, `PASS trajectory-observation`.
|
||||
|
||||
### EM all-up suite
|
||||
|
||||
Command:
|
||||
|
||||
`dotnet run --project ClumsyPilot/tests/EMPlannerVerificationHost/EMPlannerVerificationHost.csproj -- em-all`
|
||||
|
||||
Result: exit `0`. Passed foundation, segmentation, frenet, corridor, optimization, OSQP solve/loader, lateral model/integration/real OSQP, longitudinal model/integration, trajectory, EM planning service, coordinator, executor, plugin packaging, trajectory observation, rolling end-to-end, and rolling execution tail.
|
||||
|
||||
### Project build
|
||||
|
||||
Command:
|
||||
|
||||
`dotnet build ClumsyPilot/ClumsyPilot.csproj --no-restore`
|
||||
|
||||
Result: exit `0`, 0 warnings, 0 errors.
|
||||
|
||||
### Whitespace check
|
||||
|
||||
Command:
|
||||
|
||||
`git diff --check`
|
||||
|
||||
Result: exit `0`. Git emitted only line-ending conversion warnings for existing and scoped working-copy files; it reported no whitespace errors.
|
||||
|
||||
### Forbidden-call audit
|
||||
|
||||
Command pattern:
|
||||
|
||||
`SendXYThSpeed|SendMotion|SendTh|AccumulateSpeed|ComputeWheelsGeometrically|DriveStop|PredefinedDriveStop`
|
||||
|
||||
Scope: `ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/**/*.cs`
|
||||
|
||||
Result: exit `0` from the audit wrapper: `PASS forbidden-call audit: no enumerated actuator write calls found.`
|
||||
|
||||
A second scoped method-name audit for `Brake|Steer|Wheel|Motor|Adapter` invocations also passed with no matches. A live-access inspection found only the two approved read paths listed at the start of this report.
|
||||
|
||||
## Remaining limitations
|
||||
|
||||
- The verification host deliberately does not instantiate `Painter` or connect to a live vehicle. GearSwitch presentation and lifecycle cleanup are covered through pure decision/state models plus a source seam proving the World painter consumes `runtimeState.WorldNotice`. Actual colors/placement and MDCS UI rendering still require an operator environment.
|
||||
- The forbidden-call test exhaustively covers the established explicit token list inside the observation runtime directory. The additional brake/steer/wheel/motor/adapter audit is a scoped method-name heuristic. Proving an exhaustive list of every possible external adapter write API would require inventorying unrelated proprietary hardware assemblies/source outside this feature's authorized scope; this report does not guess at names that are not present in the repository sources.
|
||||
- Some fresh `dotnet run` builds emitted two pre-existing obsolete-API warnings in `MovementTests.TireFollowing.cs` and `TireFollowing.cs`. The required final project build completed with 0 warnings and 0 errors; no unrelated warning source was changed.
|
||||
|
||||
## Self-review
|
||||
|
||||
- Confirmed there is no `await controller.StartCycle(...)` in the host loop.
|
||||
- Confirmed each tick reads one fresh vehicle state, uses its capture time, observes, draws, and session-guard logs.
|
||||
- Confirmed only one planning task can be in flight and stale continuation logging does not exist.
|
||||
- Confirmed current-session runtime fault cleanup and cancellation cleanup call `ClearAll()` while the session lock is held.
|
||||
- Confirmed bootstrap failure returns normally after its diagnostic draw and is not cleared by `Finish(...)`.
|
||||
- Confirmed GearSwitch state is drawn in World and segment index remains `0`.
|
||||
- Confirmed all vehicle fields match Task-1 defaults, are copied before validation, and are included in the settings snapshot.
|
||||
- Confirmed documentation lists units/defaults and explains snapshot, cadence, and cleanup behavior.
|
||||
- Confirmed the runtime source introduces no new hardware write reference and retains only the approved live reads.
|
||||
- Confirmed unrelated dirty-worktree files remain outside the scoped diff and staging list.
|
||||
+43
-40
@@ -31,6 +31,10 @@ public sealed class TrajectoryObservationMovementTest : MovementTest
|
||||
public float MapResolutionMm = 50f;
|
||||
public double ReplanPeriodSeconds = 0.20d;
|
||||
public double ObserverPeriodSeconds = 0.05d;
|
||||
public double VehicleLengthMeters = 0.80d;
|
||||
public double VehicleWidthMeters = 0.60d;
|
||||
public double SafetyMarginMeters = 0.05d;
|
||||
public double MaximumCurvaturePerMeter = 1d / 1.20d;
|
||||
|
||||
public override void Test()
|
||||
{
|
||||
@@ -55,8 +59,12 @@ public sealed class TrajectoryObservationMovementTest : MovementTest
|
||||
MapResolutionMillimeters = MapResolutionMm,
|
||||
ReplanPeriodSeconds = ReplanPeriodSeconds,
|
||||
ObserverPeriodSeconds = ObserverPeriodSeconds,
|
||||
VehicleLengthMeters = VehicleLengthMeters,
|
||||
VehicleWidthMeters = VehicleWidthMeters,
|
||||
SafetyMarginMeters = SafetyMarginMeters,
|
||||
MaximumCurvaturePerMeter = MaximumCurvaturePerMeter,
|
||||
};
|
||||
settings.Validate();
|
||||
settings = settings.CreateValidatedSnapshot();
|
||||
TimeSpan observerPeriod = TimeSpan.FromSeconds(settings.ObserverPeriodSeconds);
|
||||
IReadOnlyList<TrajectoryObservationObstacle> obstacles = ReadManualObstacles();
|
||||
long obstacleSnapshotVersion = obstacles.Count == 0
|
||||
@@ -157,9 +165,6 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
{
|
||||
private const string StatusChannel = "TrajectoryObserver";
|
||||
private const string ObserveOnlyNotice = "OBSERVE_ONLY: no chassis command is sent.";
|
||||
private const string GearSwitchWaitingNotice =
|
||||
"等待真实档位/方向确认;观察模式不会推进下一方向段";
|
||||
private static readonly TimeSpan StatusPeriod = TimeSpan.FromSeconds(1d);
|
||||
private static readonly object SessionSync = new object();
|
||||
private static readonly TrajectoryObservationPresentation Presentation =
|
||||
new TrajectoryObservationPresentation();
|
||||
@@ -212,6 +217,8 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
activeCancellation = null;
|
||||
activeTask = null;
|
||||
activeSessionId = 0L;
|
||||
if (TrajectoryObservationSessionLifecycle.ShouldClearLayers(
|
||||
TrajectoryObservationSessionEndReason.Cancellation))
|
||||
Presentation.ClearAll();
|
||||
PrintStatus("Observation stop requested; all observer layers were cleared.");
|
||||
}
|
||||
@@ -241,7 +248,7 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
bootstrapTimer.Stop();
|
||||
if (!bootstrap.Succeeded)
|
||||
{
|
||||
DrawIfCurrent(sessionId, bootstrap, null, null);
|
||||
DrawIfCurrent(sessionId, bootstrap, null, null, null);
|
||||
LogIfCurrent(sessionId, "Planning bootstrap failed after " +
|
||||
bootstrapTimer.Elapsed.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) +
|
||||
" ms: " + bootstrap.FailureReason);
|
||||
@@ -253,44 +260,33 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
var controller = new TrajectoryObservationController(bootstrap, settings,
|
||||
new EmPlanningService(new OsqpNativeSolver()),
|
||||
"trajectory-observer-" + sessionId.ToString(CultureInfo.InvariantCulture));
|
||||
var observationLoop = new TrajectoryObservationLoop(controller);
|
||||
DirectionSegmentView segment = bootstrap.Segments[0];
|
||||
DateTimeOffset nextStatusAt = DateTimeOffset.MinValue;
|
||||
PlanningCycleResult latestCycle = null;
|
||||
TimeSpan latestPlanningElapsed = TimeSpan.Zero;
|
||||
bool gearSwitchWaitingPrinted = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(observerPeriod, token).ConfigureAwait(false);
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
VehicleMotionState state = ReadVehicleState();
|
||||
if (controller.ShouldStartCycle(now))
|
||||
{
|
||||
var planningTimer = Stopwatch.StartNew();
|
||||
latestCycle = await controller.StartCycle(now, state, token).ConfigureAwait(false);
|
||||
planningTimer.Stop();
|
||||
latestPlanningElapsed = planningTimer.Elapsed;
|
||||
}
|
||||
|
||||
TrajectoryObservationObservation observation = controller.Observe(now, state);
|
||||
DateTimeOffset now = state.CapturedAtUtc;
|
||||
TrajectoryObservationLoopTick tick = observationLoop.Tick(now, state, token);
|
||||
TrajectoryObservationObservation observation = tick.Observation;
|
||||
TrajectoryObservationCharts charts = observation.PublishedTrajectory == null
|
||||
? null
|
||||
: TrajectoryObservationCharts.Build(observation.PublishedTrajectory, segment,
|
||||
settings.MapPaddingMeters);
|
||||
DrawIfCurrent(sessionId, bootstrap, observation, charts);
|
||||
bool waitingAtGearSwitch = HasReachedGearSwitchFinal(now, observation.PublishedTrajectory);
|
||||
if (waitingAtGearSwitch && !gearSwitchWaitingPrinted)
|
||||
TrajectoryObservationRuntimeState runtimeState = TrajectoryObservationRuntimeState.Create(
|
||||
now, observation.PublishedTrajectory);
|
||||
DrawIfCurrent(sessionId, bootstrap, observation, charts, runtimeState);
|
||||
if (runtimeState.WaitingAtGearSwitch && !gearSwitchWaitingPrinted)
|
||||
{
|
||||
LogIfCurrent(sessionId, GearSwitchWaitingNotice);
|
||||
LogIfCurrent(sessionId, runtimeState.WorldNotice);
|
||||
gearSwitchWaitingPrinted = true;
|
||||
}
|
||||
|
||||
if (now >= nextStatusAt)
|
||||
{
|
||||
if (tick.ShouldLog)
|
||||
LogIfCurrent(sessionId, CreateTickStatus(sessionId, observation, charts,
|
||||
latestCycle, latestPlanningElapsed, waitingAtGearSwitch));
|
||||
nextStatusAt = now + StatusPeriod;
|
||||
}
|
||||
tick.LatestCycle, tick.LatestPlanningElapsed, runtimeState.WaitingAtGearSwitch));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,14 +301,6 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
speed.Vx, null, DateTimeOffset.UtcNow, Interlocked.Increment(ref stateSequence));
|
||||
}
|
||||
|
||||
private static bool HasReachedGearSwitchFinal(DateTimeOffset now, EmTrajectory trajectory)
|
||||
{
|
||||
if (trajectory == null || trajectory.Metadata.TerminalType != EmTerminalType.GearSwitch)
|
||||
return false;
|
||||
EmTrajectoryPoint finalPoint = trajectory.Points[trajectory.Points.Count - 1];
|
||||
return (now - trajectory.Metadata.EffectiveAtUtc).TotalSeconds >= finalPoint.TimeFromStart;
|
||||
}
|
||||
|
||||
private static string CreateBootstrapStatus(long sessionId, TrajectoryObservationBootstrapResult bootstrap,
|
||||
TrajectoryObservationSettings settings, int obstacleCount, TimeSpan elapsed)
|
||||
{
|
||||
@@ -356,7 +344,9 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
string projectionFailures = charts == null
|
||||
? "unavailable"
|
||||
: charts.FailedProjectionCount.ToString(CultureInfo.InvariantCulture);
|
||||
string waiting = waitingAtGearSwitch ? "\n" + GearSwitchWaitingNotice : string.Empty;
|
||||
string waiting = waitingAtGearSwitch
|
||||
? "\n" + TrajectoryObservationRuntimeState.GearSwitchWaitingNotice
|
||||
: string.Empty;
|
||||
|
||||
return "Session " + sessionId.ToString(CultureInfo.InvariantCulture) + ".\n" +
|
||||
"pose=(" + Format(observation.VehicleState.Pose.X) + ", " +
|
||||
@@ -370,12 +360,13 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
}
|
||||
|
||||
private static void DrawIfCurrent(long sessionId, TrajectoryObservationBootstrapResult bootstrap,
|
||||
TrajectoryObservationObservation observation, TrajectoryObservationCharts charts)
|
||||
TrajectoryObservationObservation observation, TrajectoryObservationCharts charts,
|
||||
TrajectoryObservationRuntimeState runtimeState)
|
||||
{
|
||||
lock (SessionSync)
|
||||
{
|
||||
if (activeSessionId != sessionId) return;
|
||||
Presentation.DrawWorld(bootstrap, observation);
|
||||
Presentation.DrawWorld(bootstrap, observation, runtimeState);
|
||||
Presentation.DrawLs(charts);
|
||||
Presentation.DrawSt(charts);
|
||||
}
|
||||
@@ -406,8 +397,7 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
LogIfCurrent(sessionId, "Observation session failed: " +
|
||||
exception.GetType().Name + ": " + exception.Message);
|
||||
ClearAndLogRuntimeFaultIfCurrent(sessionId, exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -423,6 +413,19 @@ internal static class TrajectoryObservationMovementTestRunner
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearAndLogRuntimeFaultIfCurrent(long sessionId, Exception exception)
|
||||
{
|
||||
lock (SessionSync)
|
||||
{
|
||||
if (activeSessionId != sessionId) return;
|
||||
if (TrajectoryObservationSessionLifecycle.ShouldClearLayers(
|
||||
TrajectoryObservationSessionEndReason.RuntimeFault))
|
||||
Presentation.ClearAll();
|
||||
PrintStatus("Observation session failed; all observer layers were cleared: " +
|
||||
exception.GetType().Name + ": " + exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CancelWithoutWaiting(CancellationTokenSource cancellation)
|
||||
{
|
||||
if (cancellation == null) return;
|
||||
|
||||
@@ -21,10 +21,17 @@ not create or dispatch a direction-change action.
|
||||
| `MapResolutionMm` | mm | `50` | Local occupancy-grid resolution. |
|
||||
| `ReplanPeriodSeconds` | s | `0.20` | Minimum interval between EM planning cycles. |
|
||||
| `ObserverPeriodSeconds` | s | `0.05` | Live-state sampling and redraw interval. |
|
||||
| `VehicleLengthMeters` | m | `0.80` | Vehicle envelope length supplied to coarse, smoothing, and EM planning. |
|
||||
| `VehicleWidthMeters` | m | `0.60` | Vehicle envelope width supplied to planning. |
|
||||
| `SafetyMarginMeters` | m | `0.05` | Additional planning clearance outside the vehicle envelope. |
|
||||
| `MaximumCurvaturePerMeter` | 1/m | `1 / 1.20` | Maximum allowed vehicle curvature (about `0.8333 1/m`). |
|
||||
|
||||
The planning snapshot also uses the setup defaults: vehicle length `0.80 m`, vehicle width `0.60 m`, safety margin
|
||||
`0.05 m`, and maximum curvature `1 / 1.20 m` (about `0.8333 1/m`). All fields and manual obstacles are read and frozen
|
||||
before the background session starts. The live pose and actual longitudinal speed are then read once per observer tick.
|
||||
The four vehicle fields are public editable MovementTest inputs. They, the map/timing fields, goal, and manual obstacles
|
||||
are validated and copied into a frozen input snapshot before the background session starts. Later edits cannot change an
|
||||
active session. The live pose and actual longitudinal speed are then read once per observer tick.
|
||||
EM planning runs asynchronously with at most one planning cycle in flight. Every observer tick still captures fresh
|
||||
state, samples the currently published trajectory, redraws all three layers, and emits a session-guarded status; a slow
|
||||
planner therefore does not reduce the configured observation cadence.
|
||||
|
||||
## Manual obstacles
|
||||
|
||||
@@ -70,4 +77,6 @@ command is diagnostic information only and must not be copied into a vehicle-con
|
||||
|
||||
To stop, use the vehicle UI's normal MovementTest stop action. Confirm the status says
|
||||
`Observation stop requested; all observer layers were cleared.` The cancellation request stops observer work and clears
|
||||
all three layers. Stopping, running, or starting this test must not issue chassis, motor, steering, brake, or gear output.
|
||||
all three layers. A current-session runtime fault also clears all three layers so stale active diagnostics are not left
|
||||
on screen; an intentional bootstrap failure preserves its diagnostic world view. Stopping, running, or starting this
|
||||
test must not issue chassis, motor, steering, brake, or gear output.
|
||||
|
||||
+17
@@ -17,6 +17,23 @@ public sealed class TrajectoryObservationSettings
|
||||
public double SafetyMarginMeters { get; set; } = 0.05d;
|
||||
public double MaximumCurvaturePerMeter { get; set; } = 1d / 1.20d;
|
||||
|
||||
public TrajectoryObservationSettings CreateValidatedSnapshot()
|
||||
{
|
||||
var snapshot = new TrajectoryObservationSettings
|
||||
{
|
||||
MapPaddingMeters = MapPaddingMeters,
|
||||
MapResolutionMillimeters = MapResolutionMillimeters,
|
||||
ReplanPeriodSeconds = ReplanPeriodSeconds,
|
||||
ObserverPeriodSeconds = ObserverPeriodSeconds,
|
||||
VehicleLengthMeters = VehicleLengthMeters,
|
||||
VehicleWidthMeters = VehicleWidthMeters,
|
||||
SafetyMarginMeters = SafetyMarginMeters,
|
||||
MaximumCurvaturePerMeter = MaximumCurvaturePerMeter,
|
||||
};
|
||||
snapshot.Validate();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
EnsurePositiveFinite(MapPaddingMeters, nameof(MapPaddingMeters));
|
||||
|
||||
+116
@@ -252,6 +252,122 @@ public sealed class TrajectoryObservationController
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TrajectoryObservationLoopTick
|
||||
{
|
||||
internal TrajectoryObservationLoopTick(TrajectoryObservationObservation observation,
|
||||
PlanningCycleResult latestCycle, TimeSpan latestPlanningElapsed, bool planningInFlight)
|
||||
{
|
||||
Observation = observation ?? throw new ArgumentNullException(nameof(observation));
|
||||
LatestCycle = latestCycle;
|
||||
LatestPlanningElapsed = latestPlanningElapsed;
|
||||
PlanningInFlight = planningInFlight;
|
||||
}
|
||||
|
||||
public TrajectoryObservationObservation Observation { get; }
|
||||
|
||||
public PlanningCycleResult LatestCycle { get; }
|
||||
|
||||
public TimeSpan LatestPlanningElapsed { get; }
|
||||
|
||||
public bool PlanningInFlight { get; }
|
||||
|
||||
public bool ShouldLog => true;
|
||||
}
|
||||
|
||||
public sealed class TrajectoryObservationLoop
|
||||
{
|
||||
private readonly TrajectoryObservationController controller;
|
||||
private Task<PlanningCycleResult> planningTask;
|
||||
private DateTimeOffset planningStartedAtUtc;
|
||||
private PlanningCycleResult latestCycle;
|
||||
private TimeSpan latestPlanningElapsed;
|
||||
|
||||
public TrajectoryObservationLoop(TrajectoryObservationController controller)
|
||||
{
|
||||
this.controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
public TrajectoryObservationLoopTick Tick(DateTimeOffset now, VehicleMotionState state,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
ConsumeCompletedPlanning(now);
|
||||
if (planningTask == null && controller.ShouldStartCycle(now))
|
||||
{
|
||||
planningStartedAtUtc = now;
|
||||
planningTask = controller.StartCycle(now, state, cancellationToken);
|
||||
ConsumeCompletedPlanning(now);
|
||||
}
|
||||
|
||||
TrajectoryObservationObservation observation = controller.Observe(now, state);
|
||||
return new TrajectoryObservationLoopTick(observation, latestCycle, latestPlanningElapsed,
|
||||
planningTask != null);
|
||||
}
|
||||
|
||||
private void ConsumeCompletedPlanning(DateTimeOffset observedAtUtc)
|
||||
{
|
||||
Task<PlanningCycleResult> completed = planningTask;
|
||||
if (completed == null || !completed.IsCompleted) return;
|
||||
|
||||
latestCycle = completed.GetAwaiter().GetResult();
|
||||
TimeSpan elapsed = observedAtUtc - planningStartedAtUtc;
|
||||
latestPlanningElapsed = elapsed < TimeSpan.Zero ? TimeSpan.Zero : elapsed;
|
||||
planningTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrajectoryObservationSessionEndReason
|
||||
{
|
||||
BootstrapFailure,
|
||||
RuntimeFault,
|
||||
Cancellation,
|
||||
}
|
||||
|
||||
public static class TrajectoryObservationSessionLifecycle
|
||||
{
|
||||
public static bool ShouldClearLayers(TrajectoryObservationSessionEndReason reason)
|
||||
{
|
||||
switch (reason)
|
||||
{
|
||||
case TrajectoryObservationSessionEndReason.BootstrapFailure:
|
||||
return false;
|
||||
case TrajectoryObservationSessionEndReason.RuntimeFault:
|
||||
case TrajectoryObservationSessionEndReason.Cancellation:
|
||||
return true;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TrajectoryObservationRuntimeState
|
||||
{
|
||||
public const string GearSwitchWaitingNotice =
|
||||
"等待真实档位/方向确认;观察模式不会推进下一方向段";
|
||||
|
||||
private TrajectoryObservationRuntimeState(bool waitingAtGearSwitch)
|
||||
{
|
||||
WaitingAtGearSwitch = waitingAtGearSwitch;
|
||||
WorldNotice = waitingAtGearSwitch ? GearSwitchWaitingNotice : string.Empty;
|
||||
}
|
||||
|
||||
public bool WaitingAtGearSwitch { get; }
|
||||
|
||||
public string WorldNotice { get; }
|
||||
|
||||
public static TrajectoryObservationRuntimeState Create(DateTimeOffset now, EmTrajectory trajectory)
|
||||
{
|
||||
if (trajectory == null || trajectory.Metadata.TerminalType != EmTerminalType.GearSwitch)
|
||||
return new TrajectoryObservationRuntimeState(false);
|
||||
|
||||
EmTrajectoryPoint finalPoint = trajectory.Points[trajectory.Points.Count - 1];
|
||||
bool waiting = (now - trajectory.Metadata.EffectiveAtUtc).TotalSeconds >= finalPoint.TimeFromStart;
|
||||
return new TrajectoryObservationRuntimeState(waiting);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TrajectoryObservationLsSample
|
||||
{
|
||||
public TrajectoryObservationLsSample(double pathS, double lateralOffset)
|
||||
|
||||
+7
-1
@@ -88,7 +88,7 @@ public sealed class TrajectoryObservationPresentation
|
||||
private readonly Painter stPainter = UI.GetPainter("TrajectoryObserver.ST", true);
|
||||
|
||||
public void DrawWorld(TrajectoryObservationBootstrapResult bootstrap,
|
||||
TrajectoryObservationObservation observation)
|
||||
TrajectoryObservationObservation observation, TrajectoryObservationRuntimeState runtimeState)
|
||||
{
|
||||
worldPainter.Clear();
|
||||
if (bootstrap == null)
|
||||
@@ -104,6 +104,12 @@ public sealed class TrajectoryObservationPresentation
|
||||
DrawLocalG2Path(bootstrap.SmoothedPath == null ? null : bootstrap.SmoothedPath.Path);
|
||||
DrawPose(observation == null || observation.VehicleState == null ? null : observation.VehicleState.Pose,
|
||||
Color.DeepSkyBlue, "real pose");
|
||||
if (runtimeState != null && runtimeState.WorldNotice.Length > 0)
|
||||
{
|
||||
float noticeX = bootstrap.Map == null ? 0f : bootstrap.Map.Bounds.XMin + 100f;
|
||||
float noticeY = bootstrap.Map == null ? 0f : bootstrap.Map.Bounds.YMax - 200f;
|
||||
worldPainter.DrawText(Color.OrangeRed, runtimeState.WorldNotice, noticeX, noticeY);
|
||||
}
|
||||
|
||||
EmTrajectory trajectory = observation == null ? null : observation.PublishedTrajectory;
|
||||
if (trajectory == null || trajectory.Points == null || trajectory.Points.Count == 0)
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Threading;
|
||||
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
||||
using MultiWheelC.TrajectoryPlanning.CoarsePath.Facade;
|
||||
using MultiWheelC.TrajectoryPlanning.EMPlanner;
|
||||
using MultiWheelC.TrajectoryPlanning.Mapping;
|
||||
using MultiWheelC.TrajectoryPlanning.PathSmoothing;
|
||||
using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade;
|
||||
using MultiWheelC.TrajectoryPlanning.TrajectoryObservation;
|
||||
@@ -19,8 +20,14 @@ internal static class TrajectoryObservationChecks
|
||||
VerifiesObservationSourceHasNoActuatorCalls();
|
||||
VerifiesObservationSourceUsesRequiredOperatorText();
|
||||
VerifiesOperatorDocumentationUsesExactUiEntry();
|
||||
VerifiesMovementTestVehicleInputsAndSettingsSnapshot();
|
||||
RejectsInvalidObservationSettings();
|
||||
VerifiesStartGoalBoundsUseOnlyConfiguredPadding();
|
||||
VerifiesValidNonEmptyObstacleSource();
|
||||
RejectsObstacleOutsideConfiguredBounds();
|
||||
VerifiesObserverTicksWhilePlanningIsDelayed();
|
||||
VerifiesSessionLayerCleanupDecisions();
|
||||
VerifiesGearSwitchWaitStateForWorldPresentation();
|
||||
VerifiesLsAndStUsePublishedTrajectoryData();
|
||||
VerifiesPresentationTextDescribesObservationWithoutSendingCommand();
|
||||
VerifiesLsPresentationUsesPathSOnHorizontalAxis();
|
||||
@@ -61,8 +68,9 @@ internal static class TrajectoryObservationChecks
|
||||
|
||||
Verification.True(source.Contains("[MovementTest(name = \"EM轨迹规划观察闭环测试\")]"),
|
||||
"observation MovementTest uses required Chinese display name");
|
||||
Verification.True(source.Contains("\"等待真实档位/方向确认;观察模式不会推进下一方向段\""),
|
||||
"observation MovementTest uses required gear-switch notice");
|
||||
Verification.Equal("等待真实档位/方向确认;观察模式不会推进下一方向段",
|
||||
TrajectoryObservationRuntimeState.GearSwitchWaitingNotice,
|
||||
"observation runtime uses required gear-switch notice");
|
||||
}
|
||||
|
||||
private static void VerifiesOperatorDocumentationUsesExactUiEntry()
|
||||
@@ -81,6 +89,82 @@ internal static class TrajectoryObservationChecks
|
||||
"observation README has the exact UI entry");
|
||||
}
|
||||
|
||||
private static void VerifiesMovementTestVehicleInputsAndSettingsSnapshot()
|
||||
{
|
||||
string movementTestSource = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot",
|
||||
"ParkrobTrajplanner", "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs");
|
||||
string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(movementTestSource));
|
||||
Verification.True(source.Contains("public double VehicleLengthMeters = 0.80d;"),
|
||||
"observer MovementTest exposes vehicle length with Task-1 default");
|
||||
Verification.True(source.Contains("public double VehicleWidthMeters = 0.60d;"),
|
||||
"observer MovementTest exposes vehicle width with Task-1 default");
|
||||
Verification.True(source.Contains("public double SafetyMarginMeters = 0.05d;"),
|
||||
"observer MovementTest exposes safety margin with Task-1 default");
|
||||
Verification.True(source.Contains("public double MaximumCurvaturePerMeter = 1d / 1.20d;"),
|
||||
"observer MovementTest exposes maximum curvature with Task-1 default");
|
||||
Verification.True(source.Contains("VehicleLengthMeters = VehicleLengthMeters,"),
|
||||
"observer MovementTest copies vehicle length into settings");
|
||||
Verification.True(source.Contains("VehicleWidthMeters = VehicleWidthMeters,"),
|
||||
"observer MovementTest copies vehicle width into settings");
|
||||
Verification.True(source.Contains("SafetyMarginMeters = SafetyMarginMeters,"),
|
||||
"observer MovementTest copies safety margin into settings");
|
||||
Verification.True(source.Contains("MaximumCurvaturePerMeter = MaximumCurvaturePerMeter,"),
|
||||
"observer MovementTest copies maximum curvature into settings");
|
||||
string normalizedSource = source.Replace("\r\n", "\n");
|
||||
Verification.True(normalizedSource.Contains(
|
||||
"VehicleMotionState state = ReadVehicleState();\n DateTimeOffset now = state.CapturedAtUtc;"),
|
||||
"observer host uses the fresh state snapshot time for each observation tick");
|
||||
|
||||
var configured = new TrajectoryObservationSettings
|
||||
{
|
||||
VehicleLengthMeters = 1.10d,
|
||||
VehicleWidthMeters = 0.70d,
|
||||
SafetyMarginMeters = 0.08d,
|
||||
MaximumCurvaturePerMeter = 0.55d,
|
||||
};
|
||||
TrajectoryObservationSettings snapshot = configured.CreateValidatedSnapshot();
|
||||
configured.VehicleLengthMeters = 9.10d;
|
||||
configured.VehicleWidthMeters = 9.20d;
|
||||
configured.SafetyMarginMeters = 9.30d;
|
||||
configured.MaximumCurvaturePerMeter = 9.40d;
|
||||
|
||||
Verification.NearlyEqual(1.10d, snapshot.VehicleLengthMeters,
|
||||
"observer settings snapshot freezes vehicle length");
|
||||
Verification.NearlyEqual(0.70d, snapshot.VehicleWidthMeters,
|
||||
"observer settings snapshot freezes vehicle width");
|
||||
Verification.NearlyEqual(0.08d, snapshot.SafetyMarginMeters,
|
||||
"observer settings snapshot freezes safety margin");
|
||||
Verification.NearlyEqual(0.55d, snapshot.MaximumCurvaturePerMeter,
|
||||
"observer settings snapshot freezes maximum curvature");
|
||||
|
||||
CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
|
||||
new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), snapshot,
|
||||
Array.Empty<TrajectoryObservationObstacle>(), 0L);
|
||||
AssertVehicleSnapshot(job.Vehicle, 1.10d, 0.70d, 0.08d, 0.55d, null,
|
||||
"observer configured bootstrap vehicle");
|
||||
}
|
||||
|
||||
private static void RejectsInvalidObservationSettings()
|
||||
{
|
||||
AssertInvalidSetting(settings => settings.MapPaddingMeters = 0d, "map padding");
|
||||
AssertInvalidSetting(settings => settings.MapResolutionMillimeters = float.NaN, "map resolution");
|
||||
AssertInvalidSetting(settings => settings.ReplanPeriodSeconds = 0d, "replan period");
|
||||
AssertInvalidSetting(settings => settings.ObserverPeriodSeconds = double.PositiveInfinity,
|
||||
"observer period");
|
||||
AssertInvalidSetting(settings => settings.VehicleLengthMeters = 0d, "vehicle length");
|
||||
AssertInvalidSetting(settings => settings.VehicleWidthMeters = double.NaN, "vehicle width");
|
||||
AssertInvalidSetting(settings => settings.SafetyMarginMeters = 0d, "safety margin");
|
||||
AssertInvalidSetting(settings => settings.MaximumCurvaturePerMeter = double.PositiveInfinity,
|
||||
"maximum curvature");
|
||||
}
|
||||
|
||||
private static void AssertInvalidSetting(Action<TrajectoryObservationSettings> mutate, string name)
|
||||
{
|
||||
var settings = new TrajectoryObservationSettings();
|
||||
mutate(settings);
|
||||
Verification.True(Throws(settings.Validate), "observer rejects invalid " + name);
|
||||
}
|
||||
|
||||
private static void VerifiesStartGoalBoundsUseOnlyConfiguredPadding()
|
||||
{
|
||||
var settings = new TrajectoryObservationSettings
|
||||
@@ -99,6 +183,29 @@ internal static class TrajectoryObservationChecks
|
||||
Verification.NearlyEqual(50d, job.MapRequest.ResolutionMm, "observer map resolution");
|
||||
}
|
||||
|
||||
private static void VerifiesValidNonEmptyObstacleSource()
|
||||
{
|
||||
CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
|
||||
new Pose2D(0d, 0d, 0d), new Pose2D(1d, 1d, 0d), new TrajectoryObservationSettings(),
|
||||
new[] { TrajectoryObservationObstacle.Circle(500d, 500d, 100d) }, 23L);
|
||||
|
||||
Verification.True(!job.MapRequest.AllowExplicitEmptyMap,
|
||||
"observer non-empty obstacle map is not explicitly empty");
|
||||
Verification.Equal(1, job.MapRequest.ObstacleSources.Count,
|
||||
"observer non-empty map has exactly one obstacle source");
|
||||
IMapObstacleSource source = job.MapRequest.ObstacleSources[0];
|
||||
Verification.Equal("trajectory-observer-manual", source.SourceId,
|
||||
"observer manual obstacle source ID");
|
||||
Verification.Equal(23L, source.SourceVersion,
|
||||
"observer manual obstacle source version");
|
||||
Verification.True(source.IsRequired, "observer manual obstacle source is required");
|
||||
ObstacleProjectionResult projection = source.ProjectToWorld();
|
||||
Verification.Equal(ObstacleSourceStatus.Applied, projection.Status,
|
||||
"observer valid manual obstacle source applies");
|
||||
Verification.Equal(1, projection.Obstacles.Count,
|
||||
"observer valid manual obstacle source preserves geometry");
|
||||
}
|
||||
|
||||
private static void RejectsObstacleOutsideConfiguredBounds()
|
||||
{
|
||||
Verification.True(Throws(() => TrajectoryObservationSetupFactory.CreateBootstrapJob(
|
||||
@@ -107,6 +214,144 @@ internal static class TrajectoryObservationChecks
|
||||
"observer obstacle outside configured bounds");
|
||||
}
|
||||
|
||||
private static void VerifiesObserverTicksWhilePlanningIsDelayed()
|
||||
{
|
||||
DateTimeOffset startedAt = new DateTimeOffset(2026, 8, 4, 3, 0, 0, TimeSpan.Zero);
|
||||
var settings = new TrajectoryObservationSettings();
|
||||
CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
|
||||
new Pose2D(0d, 0d, 0d), new Pose2D(1d, 0d, 0d), settings,
|
||||
Array.Empty<TrajectoryObservationObstacle>(), 0L);
|
||||
TrajectoryObservationBootstrapResult bootstrap = new TrajectoryObservationBootstrapper()
|
||||
.Bootstrap(job, CancellationToken.None);
|
||||
Verification.True(bootstrap.Succeeded, "observer delayed-planner bootstrap succeeds");
|
||||
|
||||
EmTrajectory published = CreatePublishedTrajectory(startedAt);
|
||||
var planningService = new DelayedTrajectoryPlanningService(published, 2);
|
||||
try
|
||||
{
|
||||
var controller = new TrajectoryObservationController(
|
||||
bootstrap, settings, planningService, "cadence-check");
|
||||
var loop = new TrajectoryObservationLoop(controller);
|
||||
var firstState = new VehicleMotionState(
|
||||
new Pose2D(0.10d, 0d, 0d), 0.10d, null, startedAt, 10L);
|
||||
|
||||
TrajectoryObservationLoopTick firstTick = loop.Tick(startedAt, firstState, CancellationToken.None);
|
||||
|
||||
Verification.True(firstTick.ShouldLog,
|
||||
"observer first cadence tick is eligible for session-guarded logging");
|
||||
Verification.Equal(10L, firstTick.Observation.VehicleState.SequenceId,
|
||||
"observer first tick uses first fresh state");
|
||||
|
||||
TrajectoryObservationLoopTick? initialPublicationTick = null;
|
||||
bool initiallyPublished = SpinWait.SpinUntil(() =>
|
||||
{
|
||||
var currentState = new VehicleMotionState(
|
||||
new Pose2D(0.15d, 0d, 0d), 0.15d, null,
|
||||
startedAt.AddSeconds(0.10d), 11L);
|
||||
initialPublicationTick = loop.Tick(startedAt.AddSeconds(0.10d), currentState,
|
||||
CancellationToken.None);
|
||||
return !initialPublicationTick.PlanningInFlight;
|
||||
}, TimeSpan.FromSeconds(5d));
|
||||
Verification.True(initiallyPublished && initialPublicationTick != null &&
|
||||
ReferenceEquals(published, initialPublicationTick.Observation.PublishedTrajectory),
|
||||
"observer establishes a published trajectory before delayed rolling planning");
|
||||
|
||||
DateTimeOffset replanAt = startedAt.AddSeconds(settings.ReplanPeriodSeconds);
|
||||
var replanState = new VehicleMotionState(
|
||||
new Pose2D(0.20d, 0d, 0d), 0.20d, null, replanAt, 12L);
|
||||
TrajectoryObservationLoopTick replanTick = loop.Tick(replanAt, replanState,
|
||||
CancellationToken.None);
|
||||
Verification.True(replanTick.PlanningInFlight,
|
||||
"observer delayed rolling planning remains in flight after replan tick");
|
||||
Verification.True(planningService.WaitUntilEntered(TimeSpan.FromSeconds(5d)),
|
||||
"observer delayed planner enters planning service");
|
||||
|
||||
DateTimeOffset secondAt = replanAt.AddSeconds(settings.ObserverPeriodSeconds);
|
||||
var secondState = new VehicleMotionState(
|
||||
new Pose2D(0.25d, 0d, 0d), 0.25d, null, secondAt, 13L);
|
||||
TrajectoryObservationLoopTick secondTick = loop.Tick(secondAt, secondState, CancellationToken.None);
|
||||
|
||||
Verification.True(secondTick.PlanningInFlight,
|
||||
"observer second tick does not wait for delayed planning");
|
||||
Verification.True(secondTick.ShouldLog,
|
||||
"observer second cadence tick is eligible for session-guarded logging");
|
||||
Verification.True(ReferenceEquals(published, secondTick.Observation.PublishedTrajectory),
|
||||
"observer keeps observing the existing publication during delayed rolling planning");
|
||||
Verification.Equal(13L, secondTick.Observation.VehicleState.SequenceId,
|
||||
"observer second tick uses second fresh state");
|
||||
Verification.Equal(secondAt, secondTick.Observation.ObservedAtUtc,
|
||||
"observer second tick observes at its own time");
|
||||
|
||||
planningService.Release();
|
||||
TrajectoryObservationLoopTick? completedTick = null;
|
||||
bool completed = SpinWait.SpinUntil(() =>
|
||||
{
|
||||
var currentState = new VehicleMotionState(
|
||||
new Pose2D(0.30d, 0d, 0d), 0.30d, null,
|
||||
replanAt.AddSeconds(0.10d), 14L);
|
||||
completedTick = loop.Tick(replanAt.AddSeconds(0.10d), currentState,
|
||||
CancellationToken.None);
|
||||
return !completedTick.PlanningInFlight;
|
||||
}, TimeSpan.FromSeconds(5d));
|
||||
|
||||
Verification.True(completed, "observer delayed planning completes deterministically");
|
||||
Verification.True(completedTick != null, "observer delayed planning produces a completion tick");
|
||||
TrajectoryObservationLoopTick finalTick = completedTick!;
|
||||
Verification.True(finalTick.LatestCycle != null && finalTick.LatestCycle.Published,
|
||||
"observer delayed planning result is consumed without a continuation");
|
||||
Verification.Equal(14L, finalTick.Observation.VehicleState.SequenceId,
|
||||
"observer post-plan observation does not reuse pre-plan state");
|
||||
Verification.Equal(replanAt.AddSeconds(0.10d), finalTick.Observation.ObservedAtUtc,
|
||||
"observer post-plan observation does not reuse pre-plan time");
|
||||
}
|
||||
finally
|
||||
{
|
||||
planningService.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void VerifiesSessionLayerCleanupDecisions()
|
||||
{
|
||||
Verification.True(!TrajectoryObservationSessionLifecycle.ShouldClearLayers(
|
||||
TrajectoryObservationSessionEndReason.BootstrapFailure),
|
||||
"observer bootstrap failure preserves the diagnostic world view");
|
||||
Verification.True(TrajectoryObservationSessionLifecycle.ShouldClearLayers(
|
||||
TrajectoryObservationSessionEndReason.RuntimeFault),
|
||||
"observer runtime fault clears all painter layers");
|
||||
Verification.True(TrajectoryObservationSessionLifecycle.ShouldClearLayers(
|
||||
TrajectoryObservationSessionEndReason.Cancellation),
|
||||
"observer cancellation clears all painter layers");
|
||||
}
|
||||
|
||||
private static void VerifiesGearSwitchWaitStateForWorldPresentation()
|
||||
{
|
||||
DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 4, 4, 0, 0, TimeSpan.Zero);
|
||||
EmTrajectory trajectory = CreatePublishedTrajectory(effectiveAt, EmTerminalType.GearSwitch);
|
||||
|
||||
TrajectoryObservationRuntimeState beforeFinal = TrajectoryObservationRuntimeState.Create(
|
||||
effectiveAt.AddSeconds(0.99d), trajectory);
|
||||
Verification.True(!beforeFinal.WaitingAtGearSwitch,
|
||||
"observer does not paint gear-switch wait state before final time");
|
||||
Verification.Equal(string.Empty, beforeFinal.WorldNotice,
|
||||
"observer has no gear-switch world notice before final time");
|
||||
|
||||
TrajectoryObservationRuntimeState atFinal = TrajectoryObservationRuntimeState.Create(
|
||||
effectiveAt.AddSeconds(1d), trajectory);
|
||||
Verification.True(atFinal.WaitingAtGearSwitch,
|
||||
"observer enters gear-switch wait state at final time");
|
||||
Verification.Equal("等待真实档位/方向确认;观察模式不会推进下一方向段", atFinal.WorldNotice,
|
||||
"observer exposes the exact gear-switch state to the world painter");
|
||||
Verification.Equal(0, trajectory.Metadata.SegmentIndex,
|
||||
"observer gear-switch wait state remains on segment zero");
|
||||
|
||||
string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot",
|
||||
"ParkrobTrajplanner", "tarjplanner_movementtest", "TrajectoryObservationPresentation.cs");
|
||||
string presentationSource = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(presentationPath));
|
||||
Verification.True(presentationSource.Contains(
|
||||
"worldPainter.DrawText(Color.OrangeRed, runtimeState.WorldNotice"),
|
||||
"observer world painter draws the exact runtime wait state");
|
||||
}
|
||||
|
||||
private static void VerifiesLsAndStUsePublishedTrajectoryData()
|
||||
{
|
||||
DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 4, 0, 0, 0, TimeSpan.Zero);
|
||||
@@ -333,10 +578,11 @@ internal static class TrajectoryObservationChecks
|
||||
new ReferenceBoundary(0, 2d, EmBoundaryType.Goal, 12d), 10d);
|
||||
}
|
||||
|
||||
private static EmTrajectory CreatePublishedTrajectory(DateTimeOffset effectiveAt)
|
||||
private static EmTrajectory CreatePublishedTrajectory(DateTimeOffset effectiveAt,
|
||||
EmTerminalType terminalType = EmTerminalType.Goal)
|
||||
{
|
||||
var metadata = new EmTrajectoryMetadata("observer-published", effectiveAt, effectiveAt, 1L,
|
||||
"observer-reference", 1L, string.Empty, 0, TravelDirection.Forward, EmTerminalType.Goal);
|
||||
"observer-reference", 1L, string.Empty, 0, TravelDirection.Forward, terminalType);
|
||||
return new EmTrajectory(metadata, new[]
|
||||
{
|
||||
new EmTrajectoryPoint(0.25d, 0.10d, 0d, 0.20d, 0d, 0d, 0, 0.25d, 4d,
|
||||
@@ -364,6 +610,41 @@ internal static class TrajectoryObservationChecks
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class DelayedTrajectoryPlanningService : IEmPlanningService
|
||||
{
|
||||
private readonly EmTrajectory trajectory;
|
||||
private readonly int delayedCall;
|
||||
private readonly ManualResetEventSlim entered = new ManualResetEventSlim(false);
|
||||
private readonly ManualResetEventSlim release = new ManualResetEventSlim(false);
|
||||
private int callCount;
|
||||
|
||||
public DelayedTrajectoryPlanningService(EmTrajectory trajectory, int delayedCall)
|
||||
{
|
||||
this.trajectory = trajectory;
|
||||
this.delayedCall = delayedCall;
|
||||
}
|
||||
|
||||
public EmPlanningResult Plan(EmPlanningRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Interlocked.Increment(ref callCount) == delayedCall)
|
||||
{
|
||||
entered.Set();
|
||||
release.Wait(cancellationToken);
|
||||
}
|
||||
return new EmPlanningResult(EmPlanningStatus.Success, trajectory, string.Empty);
|
||||
}
|
||||
|
||||
public bool WaitUntilEntered(TimeSpan timeout)
|
||||
{
|
||||
return entered.Wait(timeout);
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
release.Set();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Throws(Action action)
|
||||
{
|
||||
try { action(); return false; }
|
||||
|
||||
Reference in New Issue
Block a user