diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs index 4cdc8e7..4c089e8 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/MovementTest.TrajectoryObservationTest.cs @@ -31,16 +31,26 @@ public sealed class TrajectoryObservationMovementTest : MovementTest public float MapResolutionMm = 50f; public double ReplanPeriodSeconds = 0.20d; public double ObserverPeriodSeconds = 0.05d; - public double SolverTimeoutSeconds = 0.50d; - public int MaximumOsqpIterations = 12000; - /// 单次 ST 轨迹覆盖的未来时长,单位 s;默认 6s,不等于观察循环周期。 - public double TimeHorizonSeconds = 6d; + public double SolverTimeoutSeconds = 5.0d; + public int MaximumOsqpIterations = 100000; + /// 单次 ST 轨迹覆盖的未来时长,单位 s;默认 2s,不等于观察循环周期。 + public double TimeHorizonSeconds = 2d; /// Trajectory 相邻 TimeFromStart 时间戳的间隔,单位 s;默认 0.10s,不等于观察循环周期。 public double OutputTimeStepSeconds = 0.10d; public double VehicleLengthMeters = 0.80d; public double VehicleWidthMeters = 0.60d; public double SafetyMarginMeters = 0.05d; public double MaximumCurvaturePerMeter = 1d / 1.20d; + public bool EnableWebVisualization = false; + public bool AutoOpenWebVisualization = true; + public int WebVisualizationPort = 0; + public double WebRefreshRateHz = 10d; + public int VisualizationHistoryCycleLimit = 60; + public bool EnableNativePainterVisualization = false; + public double DirectionConfirmationSpeedMetersPerSecond = 0.02d; + public int DirectionConfirmationSamples = 3; + public double GearSwitchProjectionToleranceMeters = 0.50d; + public double GearSwitchStopHoldSeconds = 0.20d; public override void Test() { @@ -73,6 +83,16 @@ public sealed class TrajectoryObservationMovementTest : MovementTest VehicleWidthMeters = VehicleWidthMeters, SafetyMarginMeters = SafetyMarginMeters, MaximumCurvaturePerMeter = MaximumCurvaturePerMeter, + EnableWebVisualization = EnableWebVisualization, + AutoOpenWebVisualization = AutoOpenWebVisualization, + WebVisualizationPort = WebVisualizationPort, + WebRefreshRateHz = WebRefreshRateHz, + VisualizationHistoryCycleLimit = VisualizationHistoryCycleLimit, + EnableNativePainterVisualization = EnableNativePainterVisualization, + DirectionConfirmationSpeedMetersPerSecond = DirectionConfirmationSpeedMetersPerSecond, + DirectionConfirmationSamples = DirectionConfirmationSamples, + GearSwitchProjectionToleranceMeters = GearSwitchProjectionToleranceMeters, + GearSwitchStopHoldSeconds = GearSwitchStopHoldSeconds, }; settings = settings.CreateValidatedSnapshot(); TimeSpan observerPeriod = TimeSpan.FromSeconds(settings.ObserverPeriodSeconds); diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md index 0bda336..2c3d463 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/README.md @@ -27,10 +27,24 @@ single S-T output duration. Only a real Goal or gear-switch boundary may publish | `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. | +| `SolverTimeoutSeconds` | s | `5.0` | EM solver timeout frozen for the session. | +| `MaximumOsqpIterations` | iterations | `100000` | OSQP iteration limit frozen for the session. | +| `TimeHorizonSeconds` | s | `2.0` | Single ST trajectory horizon, not the observer period. | +| `OutputTimeStepSeconds` | s | `0.10` | Published trajectory timestamp spacing. | | `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`). | +| `EnableWebVisualization` | bool | `false` | Enables the local web dashboard session. | +| `AutoOpenWebVisualization` | bool | `true` | Opens the tokenized local dashboard URI when web output is enabled. | +| `WebVisualizationPort` | port | `0` | `0` selects an ephemeral port; otherwise use `1024` through `65535`. | +| `WebRefreshRateHz` | Hz | `10` | Maximum dynamic web snapshot publication rate. | +| `VisualizationHistoryCycleLimit` | cycles | `60` | Bounded web history length. | +| `EnableNativePainterVisualization` | bool | `false` | Enables the optional native Painter fallback. | +| `DirectionConfirmationSpeedMetersPerSecond` | m/s | `0.02` | Minimum signed-speed magnitude used to confirm the next direction. | +| `DirectionConfirmationSamples` | samples | `3` | Strictly increasing matching-direction samples required after the stop hold. | +| `GearSwitchProjectionToleranceMeters` | m | `0.50` | Maximum switch-point projection distance for both adjacent segments. | +| `GearSwitchStopHoldSeconds` | s | `0.20` | Continuous real-stop duration required before direction samples count. | 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 diff --git a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs index ac544e6..2436512 100644 --- a/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs +++ b/ClumsyPilot/ParkrobTrajplanner/tarjplanner_movementtest/TrajectoryObservationContracts.cs @@ -12,16 +12,26 @@ public sealed class TrajectoryObservationSettings public float MapResolutionMillimeters { get; set; } = 50f; public double ReplanPeriodSeconds { get; set; } = 0.20d; public double ObserverPeriodSeconds { get; set; } = 0.05d; - public double SolverTimeoutSeconds { get; set; } = 0.50d; - public int MaximumOsqpIterations { get; set; } = 12000; - /// 单次 ST 轨迹覆盖的未来时长,单位 s;不等于观察循环周期。 - public double TimeHorizonSeconds { get; set; } = 6d; + public double SolverTimeoutSeconds { get; set; } = 5d; + public int MaximumOsqpIterations { get; set; } = 100000; + /// 单次 ST 轨迹覆盖的未来时长,单位 s;默认 2 s,不等于观察循环周期。 + public double TimeHorizonSeconds { get; set; } = 2d; /// 发布 Trajectory 相邻 TimeFromStart 时间戳的间隔,单位 s;不等于观察循环周期。 public double OutputTimeStepSeconds { get; set; } = 0.10d; public double VehicleLengthMeters { get; set; } = 0.80d; public double VehicleWidthMeters { get; set; } = 0.60d; public double SafetyMarginMeters { get; set; } = 0.05d; public double MaximumCurvaturePerMeter { get; set; } = 1d / 1.20d; + public bool EnableWebVisualization { get; set; } = false; + public bool AutoOpenWebVisualization { get; set; } = true; + public int WebVisualizationPort { get; set; } = 0; + public double WebRefreshRateHz { get; set; } = 10d; + public int VisualizationHistoryCycleLimit { get; set; } = 60; + public bool EnableNativePainterVisualization { get; set; } = false; + public double DirectionConfirmationSpeedMetersPerSecond { get; set; } = 0.02d; + public int DirectionConfirmationSamples { get; set; } = 3; + public double GearSwitchProjectionToleranceMeters { get; set; } = 0.50d; + public double GearSwitchStopHoldSeconds { get; set; } = 0.20d; public TrajectoryObservationSettings CreateValidatedSnapshot() { @@ -39,6 +49,16 @@ public sealed class TrajectoryObservationSettings VehicleWidthMeters = VehicleWidthMeters, SafetyMarginMeters = SafetyMarginMeters, MaximumCurvaturePerMeter = MaximumCurvaturePerMeter, + EnableWebVisualization = EnableWebVisualization, + AutoOpenWebVisualization = AutoOpenWebVisualization, + WebVisualizationPort = WebVisualizationPort, + WebRefreshRateHz = WebRefreshRateHz, + VisualizationHistoryCycleLimit = VisualizationHistoryCycleLimit, + EnableNativePainterVisualization = EnableNativePainterVisualization, + DirectionConfirmationSpeedMetersPerSecond = DirectionConfirmationSpeedMetersPerSecond, + DirectionConfirmationSamples = DirectionConfirmationSamples, + GearSwitchProjectionToleranceMeters = GearSwitchProjectionToleranceMeters, + GearSwitchStopHoldSeconds = GearSwitchStopHoldSeconds, }; snapshot.Validate(); return snapshot; @@ -62,6 +82,18 @@ public sealed class TrajectoryObservationSettings EnsurePositiveFinite(VehicleWidthMeters, nameof(VehicleWidthMeters)); EnsurePositiveFinite(SafetyMarginMeters, nameof(SafetyMarginMeters)); EnsurePositiveFinite(MaximumCurvaturePerMeter, nameof(MaximumCurvaturePerMeter)); + if (WebVisualizationPort != 0 && (WebVisualizationPort < 1024 || WebVisualizationPort > 65535)) + throw new ArgumentOutOfRangeException(nameof(WebVisualizationPort), + "Port must be zero or in the inclusive range 1024 through 65535."); + EnsurePositiveFinite(WebRefreshRateHz, nameof(WebRefreshRateHz)); + if (VisualizationHistoryCycleLimit <= 0) + throw new ArgumentOutOfRangeException(nameof(VisualizationHistoryCycleLimit), "Value must be positive."); + EnsurePositiveFinite(DirectionConfirmationSpeedMetersPerSecond, + nameof(DirectionConfirmationSpeedMetersPerSecond)); + if (DirectionConfirmationSamples <= 0) + throw new ArgumentOutOfRangeException(nameof(DirectionConfirmationSamples), "Value must be positive."); + EnsurePositiveFinite(GearSwitchProjectionToleranceMeters, nameof(GearSwitchProjectionToleranceMeters)); + EnsurePositiveFinite(GearSwitchStopHoldSeconds, nameof(GearSwitchStopHoldSeconds)); } public VehicleParameters CreateVehicle() diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs index a47fde0..deaa55b 100644 --- a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationChecks.cs @@ -17,6 +17,7 @@ internal static class TrajectoryObservationChecks { public static void Run() { + TrajectoryObservationSettingsChecks.Run(); VerifiesObservationSourceHasNoActuatorCalls(); VerifiesObservationSourceUsesRequiredOperatorText(); VerifiesOperatorDocumentationUsesExactUiEntry(); @@ -138,12 +139,6 @@ internal static class TrajectoryObservationChecks Verification.True(normalizedSource.Contains( "VehicleMotionState state = ReadVehicleState();\n DateTimeOffset now = state.CapturedAtUtc;"), "observer host uses the fresh state snapshot time for each observation tick"); - Verification.NearlyEqual(0.50d, new TrajectoryObservationSettings().SolverTimeoutSeconds, - "observer settings use the test solver timeout default"); - Verification.Equal(12000, new TrajectoryObservationSettings().MaximumOsqpIterations, - "observer settings use the test OSQP iteration default"); - Verification.NearlyEqual(6d, new TrajectoryObservationSettings().TimeHorizonSeconds, - "observer settings use the ST time-horizon default"); Verification.NearlyEqual(0.10d, new TrajectoryObservationSettings().OutputTimeStepSeconds, "observer settings use the ST timestamp-spacing default"); diff --git a/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs new file mode 100644 index 0000000..d3189fa --- /dev/null +++ b/ClumsyPilot/tests/EMPlannerVerificationHost/TrajectoryObservationSettingsChecks.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +using System.Text; +using MultiWheelC.TrajectoryPlanning.TrajectoryObservation; + +namespace EMPlannerVerificationHost; + +internal static class TrajectoryObservationSettingsChecks +{ + public static void Run() + { + VerifiesDefaults(); + VerifiesValidatedSnapshotFreezesVisualizationSettings(); + RejectsInvalidVisualizationSettings(); + VerifiesMovementTestMapsEveryFrozenSetting(); + } + + private static void VerifiesDefaults() + { + var defaults = new TrajectoryObservationSettings(); + + Verification.NearlyEqual(5d, defaults.SolverTimeoutSeconds, "observer solver timeout default"); + Verification.Equal(100000, defaults.MaximumOsqpIterations, "observer OSQP default"); + Verification.NearlyEqual(2d, defaults.TimeHorizonSeconds, "observer ST horizon default"); + Verification.True(!defaults.EnableWebVisualization, "web defaults off"); + Verification.True(defaults.AutoOpenWebVisualization, "web auto-open defaults on"); + Verification.True(!defaults.EnableNativePainterVisualization, "Painter defaults off"); + Verification.Equal(0, defaults.WebVisualizationPort, "dynamic port default"); + Verification.NearlyEqual(10d, defaults.WebRefreshRateHz, "web refresh default"); + Verification.Equal(60, defaults.VisualizationHistoryCycleLimit, "history default"); + Verification.NearlyEqual(0.02d, defaults.DirectionConfirmationSpeedMetersPerSecond, + "direction speed threshold"); + Verification.Equal(3, defaults.DirectionConfirmationSamples, "direction sample default"); + Verification.NearlyEqual(0.50d, defaults.GearSwitchProjectionToleranceMeters, + "gear projection default"); + Verification.NearlyEqual(0.20d, defaults.GearSwitchStopHoldSeconds, "gear stop hold default"); + } + + private static void VerifiesValidatedSnapshotFreezesVisualizationSettings() + { + var source = new TrajectoryObservationSettings + { + EnableWebVisualization = true, + AutoOpenWebVisualization = false, + WebVisualizationPort = 2048, + WebRefreshRateHz = 15d, + VisualizationHistoryCycleLimit = 17, + EnableNativePainterVisualization = true, + DirectionConfirmationSpeedMetersPerSecond = 0.03d, + DirectionConfirmationSamples = 4, + GearSwitchProjectionToleranceMeters = 0.6d, + GearSwitchStopHoldSeconds = 0.3d, + }; + TrajectoryObservationSettings snapshot = source.CreateValidatedSnapshot(); + + source.EnableWebVisualization = false; + source.AutoOpenWebVisualization = true; + source.WebVisualizationPort = 0; + source.WebRefreshRateHz = 12d; + source.VisualizationHistoryCycleLimit = 18; + source.EnableNativePainterVisualization = false; + source.DirectionConfirmationSpeedMetersPerSecond = 0.04d; + source.DirectionConfirmationSamples = 5; + source.GearSwitchProjectionToleranceMeters = 0.7d; + source.GearSwitchStopHoldSeconds = 0.4d; + + Verification.True(snapshot.EnableWebVisualization, "snapshot freezes web switch"); + Verification.True(!snapshot.AutoOpenWebVisualization, "snapshot freezes web auto-open switch"); + Verification.Equal(2048, snapshot.WebVisualizationPort, "snapshot freezes web port"); + Verification.NearlyEqual(15d, snapshot.WebRefreshRateHz, "snapshot freezes web refresh"); + Verification.Equal(17, snapshot.VisualizationHistoryCycleLimit, "snapshot freezes history limit"); + Verification.True(snapshot.EnableNativePainterVisualization, "snapshot freezes Painter switch"); + Verification.NearlyEqual(0.03d, snapshot.DirectionConfirmationSpeedMetersPerSecond, + "snapshot freezes direction speed"); + Verification.Equal(4, snapshot.DirectionConfirmationSamples, "snapshot freezes direction samples"); + Verification.NearlyEqual(0.6d, snapshot.GearSwitchProjectionToleranceMeters, + "snapshot freezes gear projection tolerance"); + Verification.NearlyEqual(0.3d, snapshot.GearSwitchStopHoldSeconds, "snapshot freezes gear stop hold"); + } + + private static void RejectsInvalidVisualizationSettings() + { + AssertInvalid(settings => settings.WebVisualizationPort = -1, "negative web port"); + AssertInvalid(settings => settings.WebVisualizationPort = 1, "low reserved web port"); + AssertInvalid(settings => settings.WebVisualizationPort = 1023, "last reserved web port"); + AssertInvalid(settings => settings.WebVisualizationPort = 65536, "overflow web port"); + AssertInvalid(settings => settings.WebRefreshRateHz = 0d, "zero web refresh"); + AssertInvalid(settings => settings.VisualizationHistoryCycleLimit = 0, "zero history limit"); + AssertInvalid(settings => settings.DirectionConfirmationSpeedMetersPerSecond = 0d, + "zero direction speed threshold"); + AssertInvalid(settings => settings.DirectionConfirmationSamples = 0, "zero direction samples"); + AssertInvalid(settings => settings.GearSwitchProjectionToleranceMeters = 0d, + "zero gear projection tolerance"); + AssertInvalid(settings => settings.GearSwitchStopHoldSeconds = 0d, "zero gear stop hold"); + } + + private static void VerifiesMovementTestMapsEveryFrozenSetting() + { + string path = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot", "ParkrobTrajplanner", + "tarjplanner_movementtest", "MovementTest.TrajectoryObservationTest.cs"); + string source = new UTF8Encoding(false, true).GetString(File.ReadAllBytes(path)); + foreach (string field in new[] + { + "EnableWebVisualization", "AutoOpenWebVisualization", "WebVisualizationPort", "WebRefreshRateHz", + "VisualizationHistoryCycleLimit", "EnableNativePainterVisualization", + "DirectionConfirmationSpeedMetersPerSecond", "DirectionConfirmationSamples", + "GearSwitchProjectionToleranceMeters", "GearSwitchStopHoldSeconds", + }) + { + Verification.True(source.Contains("public ") && source.Contains(" " + field + " ="), + "MovementTest exposes " + field); + Verification.True(source.Contains(field + " = " + field + ","), + "MovementTest maps " + field + " into settings"); + } + } + + private static void AssertInvalid(Action mutate, string name) + { + var settings = new TrajectoryObservationSettings(); + mutate(settings); + bool threw = false; + try { settings.Validate(); } + catch (ArgumentOutOfRangeException) { threw = true; } + Verification.True(threw, "observer rejects invalid " + name); + } +}