Repository navigation
fix(trajectory_planner): resume from the applied acceleration after a stop; per-axis jerk; reverse speed limit (#850, #852) - #851
Conversation
… hand-offs; per-axis jerk update() only wrote state_.a_v / a_w in the jerk-limited branch, so after a trapezoidal stop the S-curve resumed from the last driving acceleration: the first step after a stop jumped straight to it, and a reversal from standstill first accelerated the wrong way (issue #850). A profile jerk-limited on one axis only also fed the other axis through the S-curve logic with a huge fake jerk, whose decision block then never left zero acceleration. The per-axis maths now lives in detail/trajectory_planner_math.hpp (standard-library only) and update() steps each axis with step_axis(): the S-curve where that axis' jerk is > 0, the trapezoidal rate limiter where it is 0, and in both cases the acceleration actually applied, (v_new - v_old) / dt after the velocity clamp, is kept in the state. An acceleration inherited from a profile with a higher limit (a firm stop interrupted by the stick) is wound back into the new profile's range at its jerk limit instead of being clamped in one step. Host test (components/trajectory_planner/test) runs the issue's scenario -- 0.5 s driving, 0.5 s trapezoidal stop, reverse -- and checks the stop leaves a = 0, the first reverse step is at the jerk limit and backwards, the jerk limit holds over whole stop-and-go cycles including an interrupted stop, the all-trapezoidal case is unchanged, and the mixed-axis profile moves. Fixes #850 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
|
✅Static analysis result - no issues found! ✅ |
Powered wheelchair controllers run reverse slower than forward (the R-Net recording in #852: 71 %), and scaling the stick outside the planner puts chassis behaviour where it does not belong. Config gains max_reverse_linear_velocity (m/s at full reverse stick; 0 = the same as max_linear_velocity, so nothing changes by default): set_target() scales a negative linear input by it, get_target() normalizes by it, and the linear axis is clamped to [-reverse limit, +max_linear_velocity] through the per-axis step's new asymmetric bounds. Validated (>= 0), shown by the Config formatter, documented in the README; the python bindings and stub carry the new field and constructor argument (mirrored by hand, litgen is not installed here; same order as the struct). Verified through the python package against the planner's own timer: full reverse settles at the reverse limit and reads back as -1, full forward at the forward limit, the default stays symmetric. Fixes #852 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
|
zoomin on the espp fixes |
…n bit-exact; reject a stopping profile with no acceleration Self-review of the per-axis step: recomputing the S-curve acceleration from the velocity delta even when the velocity clamp did not bite fed float rounding noise into update_accel()'s step-count truncation, which can flip a boundary case. The planned value is now kept bit for bit unless the clamp engaged (the velocity sits on a bound: the acceleration applied is what the bound allowed, 0 once pinned), with a host test for both. set_config() validated only the driving profile's accelerations; a stopping profile with 0 acceleration could never bring the chassis to rest, so it is rejected the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Test evidence (at 88aef29)Host test ( Checks: the #850 scenario with the report's numbers (v = 0.410 / a = 1.0 after 0.5 s driving; a = 0 after the 0.5 s trapezoidal stop; first reverse step a = -0.1 and v < 0; v never > 0 over 0.5 s of reverse; first forward step after a stop a = 0.1), jerk limit held over whole stop-and-go cycles including a stop interrupted after 0.1 s (a goes -1.6 -> -1.5 -> ... never a 1.6 -> 1.0 jump), all-trapezoidal unchanged, mixed-axis profile moves (it used to stay at 0), an unclamped S-curve step keeps the planned acceleration bit for bit, an S-curve pinned at v_max records a = 0 and leaves the bound from 0, asymmetric bounds (#852) settle full reverse at the reverse limit, the S-curve alone lands without overshoot. ESP32 build of Host lib + python package ( Python, #850 scenario through the public API against the planner's own 50 Hz timer (S-curve driving 1.0 m/s^2 / 5 m/s^3, trapezoidal stop 1.6 m/s^2; drive 0.5 s, stop 0.5 s, full reverse 0.5 s, stop, forward): Python, #852 (forward 1.8 m/s, reverse limit 1.0 m/s): cppcheck ( Self-review notesChanges made as a result of re-reading the diff and the rest of the planner:
Pre-existing, left alone: |
Summary
Fixes #850 and #852, both in
TrajectoryPlanner::update()/set_target().#850: stale acceleration across the profile hand-off
update()only wrotestate_.a_v/a_win the jerk-limited branch, so after a trapezoidal stop the S-curve resumed from the last driving acceleration: the first step after a stop jumped straight to it, and a reversal from standstill first accelerated the wrong way. Reading the same code turned up a second problem: a profile jerk-limited on one axis only fed the other axis through the S-curve logic with a huge fake jerk, and that logic's decision block then never left zero acceleration.include/detail/trajectory_planner_math.hpp(standard-library only), andupdate()steps each axis withstep_axis(): the S-curve where that axis' jerk is > 0, the trapezoidal rate limiter where it is 0. In both cases the acceleration actually applied,(v_new - v_old) / dtafter the velocity clamp, is kept in the state, so a jerk-limited step always resumes from the real value (0 once a stop has completed, the deceleration in force if the stop is interrupted). This is the reporter's proposed fix, applied to both branches.State::a_v/a_wand theMotionProfiledocs plus the README describe the per-axis choice and the continuous hand-off.#852: separate maximum reverse linear velocity
Config::max_reverse_linear_velocity(m/s at full reverse stick; 0 = the same asmax_linear_velocity, so nothing changes by default).set_target()scales a negative linear input by it,get_target()normalizes by it (so -1 is full reverse stick whatever the limits), and the linear axis is clamped to[-reverse limit, +max_linear_velocity]through the per-axis step's new asymmetric bounds. Validated (>= 0), shown by theConfigformatter, documented in the README and theset_target()docs.Host platform / python
lib/espp.cmakealready compiled the planner and added its include dir, so the newdetail/header needs no lib changes. The python bindings (pybind_espp.cpp) and the stub carry the newConfigfield and constructor argument, mirrored by hand in struct order (litgen is not installed here; the generated file's own docstrings are updated to match).Test plan
components/trajectory_planner/test/trajectory_planner_host_test.cpp(c++ -std=c++23 -Wall -Wextra -Werror): the trajectory_planner: stale acceleration after trapezoidal stopping profile breaks jerk limit and can move the wrong way #850 scenario (0.5 s driving, 0.5 s trapezoidal stop, full reverse; same numbers as the report, v = 0.410 / a = 1.0 after driving) leaves a = 0 after the stop, the first reverse step is backwards at the jerk limit, the jerk limit holds over whole stop-and-go cycles including an interrupted stop, the all-trapezoidal configuration is unchanged, the mixed-axis profile moves, the S-curve alone lands without overshoot, and the asymmetric bounds settle full reverse at the reverse limit.components/trajectory_planner/examplebuilds for esp32 (IDF 6.1)lib/, cmake + pybind11, macOS); through the python API against the planner's own 50 Hz timer: the trajectory_planner: stale acceleration after trapezoidal stopping profile breaks jerk limit and can move the wrong way #850 scenario passes (first output after reversal already negative, never positive; first forward step after a stop 0.002 m/s, i.e. jerk-limited, not the 0.02 a stale 1.0 m/s² gives), and for trajectory_planner: support a separate max reverse linear velocity #852 full reverse settles at -1.0 with a 1.0 m/s reverse limit and 1.8 m/s forward limit, reads back as -1, full forward at 1.8, the default stays symmetric, and the constructor keyword works.🤖 Generated with Claude Code
https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU