Skip to content

Commit f10f863

Browse files
committed
feat: add terminal control logic to Stanley controller
- Added is_goal_reached() to detect proximity to path end - Applied smooth deceleration near trajectory endpoint - Replaced hard stop with gradual slowdown behavior - Improved safety in absence of path (safe return control)
1 parent 72ce36c commit f10f863

1 file changed

Lines changed: 50 additions & 11 deletions

File tree

GEMstack/onboard/planning/stanley.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,19 @@ def compute(self, state: VehicleState, component: Component = None):
7474
curr_x = state.pose.x
7575
curr_y = state.pose.y
7676
curr_yaw = state.pose.yaw if state.pose.yaw is not None else 0.0
77-
speed = state.v
78-
77+
78+
# speed = state.v
79+
# Protect against divide-by-zero issues
80+
# I think there is arctan(k * e / v) in Stanley
81+
speed = max(state.v, 0.01)
82+
83+
# if self.path is None:
84+
# # just stop
85+
# accel = self.pid_speed.advance(0.0, t)
86+
# raise RuntimeError("Behavior without path not implemented")
7987
if self.path is None:
80-
# just stop
81-
accel = self.pid_speed.advance(0.0, t)
82-
raise RuntimeError("Behavior without path not implemented")
88+
print("[StanleyController] Warning: No path set. Sending zero commands.")
89+
return (0.0, 0.0)
8390

8491
if self.path.frame != state.pose.frame:
8592
print("Transforming path from", self.path.frame.name, "to", state.pose.frame.name)
@@ -121,12 +128,25 @@ def compute(self, state: VehicleState, component: Component = None):
121128
f_delta = theta_e + crosstrack_term
122129
f_delta = np.clip(f_delta, self.wheel_angle_range[0], self.wheel_angle_range[1])
123130

131+
# # Debug outputs
132+
# print("Crosstrack Error: " + str(round(e, 3)))
133+
# print("Heading Error: " + str(round(np.degrees(theta_e), 2)) + " degrees")
134+
# print("Front wheel angle: " + str(round(np.degrees(f_delta), 2)) + " degrees")
135+
# steering_angle = np.clip(front2steer(f_delta), self.steering_angle_range[0], self.steering_angle_range[1])
136+
# print("Steering wheel angle: " + str(round(np.degrees(steering_angle), 2)) + " degrees")
137+
124138
# Debug outputs
125-
print("Crosstrack Error: " + str(round(e, 3)))
126-
print("Heading Error: " + str(round(np.degrees(theta_e), 2)) + " degrees")
127-
print("Front wheel angle: " + str(round(np.degrees(f_delta), 2)) + " degrees")
128139
steering_angle = np.clip(front2steer(f_delta), self.steering_angle_range[0], self.steering_angle_range[1])
129-
print("Steering wheel angle: " + str(round(np.degrees(steering_angle), 2)) + " degrees")
140+
debug_info = {
141+
"Crosstrack Error (m)": round(e, 3),
142+
"Heading Error (deg)": round(np.degrees(theta_e), 2),
143+
"Front Wheel Angle (deg)": round(np.degrees(f_delta), 2),
144+
"Steering Angle (deg)": round(np.degrees(steering_angle), 2),
145+
"Desired Speed (m/s)": round(desired_speed, 2),
146+
"Output Acceleration (m/s^2)": round(output_accel, 2)
147+
}
148+
for k, v in debug_info.items():
149+
print(f"{k}: {v}")
130150

131151
# Speed control (same as PurePursuit)
132152
desired_speed = self.desired_speed
@@ -135,8 +155,14 @@ def compute(self, state: VehicleState, component: Component = None):
135155
if len(self.trajectory.points) < 2 or self.current_path_parameter >= self.path.domain()[1]:
136156
if component is not None:
137157
component.debug_event('Past the end of trajectory')
138-
desired_speed = 0.0
139-
feedforward_accel = -2.0
158+
# desired_speed = 0.0
159+
# feedforward_accel = -2.0
160+
161+
# Try smooth deceleration
162+
desired_speed *= 0.5
163+
if desired_speed < 0.1:
164+
desired_speed = 0.0
165+
feedforward_accel = -2.0 # Full stop
140166
else:
141167
if self.desired_speed_source == 'path':
142168
current_trajectory_time = self.trajectory.parameter_to_time(self.current_path_parameter)
@@ -206,3 +232,16 @@ def update(self, vehicle: VehicleState, trajectory: Trajectory):
206232

207233
def healthy(self):
208234
return self.stanley.path is not None
235+
236+
237+
def is_goal_reached(self, state: VehicleState, threshold=0.5):
238+
"""
239+
Checks if the vehicle is near the end of the path.
240+
Used to trigger stop, mode switch, or task completion.
241+
"""
242+
if self.path is None:
243+
return False
244+
end_point = self.path.eval(self.path.domain()[1])
245+
dx = state.pose.x - end_point[0]
246+
dy = state.pose.y - end_point[1]
247+
return np.hypot(dx, dy) < threshold

0 commit comments

Comments
 (0)