Skip to content

Commit 24c2d94

Browse files
committed
Merge remote-tracking branch 'origin/rolls' into working-vision-stuff
2 parents 9c864e7 + 3c9d9a3 commit 24c2d94

File tree

14 files changed

+169
-69
lines changed

14 files changed

+169
-69
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ vision/data/
1919
#VENV
2020
.sc/
2121
.nand/
22+
.venv/

rb_ws/src/buggy/config/nand-roll.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,16 @@ NAND:
1313
stateTopic: "self/state"
1414
trajectoryTopic: "self/cur_traj"
1515
steeringTopic: "input/steering"
16+
steerOffsetTopic: "self/steering_offset/filtered"
1617
useHeadingRate: true
1718
controllerName: "controller"
1819
useSteerOffset: false
1920
# traj_name: "buggycourse_safe.json"
2021
controller: "stanley"
22+
useSteerOffset: false
23+
24+
NAND_offset_estimator:
25+
ros__parameters:
26+
steerOffsetRawTopic: "self/steering_offset/raw"
27+
steerOffsetFilteredTopic: "self/steering_offset/filtered"
28+
steerOffsetFilterTimeConstant: 50 # in timesteps; smoothing factor alpha for low pass filter is 1 / steerOffsetFilterTimeConstant

rb_ws/src/buggy/config/sc-roll.yaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,19 @@ SC:
1313
stateTopic: "self/state"
1414
trajectoryTopic: "self/cur_traj"
1515
steeringTopic: "input/steering"
16+
steerOffsetTopic: "self/steering_offset/filtered"
1617
useHeadingRate: true
1718
controllerName: "controller"
1819
useSteerOffset: false
1920
# traj_name: "buggycourse_safe.json"
2021
controller: "stanley"
22+
useSteerOffset: false
23+
24+
SC_offset_estimator:
25+
ros__parameters:
26+
steerOffsetRawTopic: "self/steering_offset/raw"
27+
steerOffsetFilteredTopic: "self/steering_offset/filtered"
28+
steerOffsetFilterTimeConstant: 50 # in timesteps; smoothing factor alpha for low pass filter is 1 / steerOffsetFilterTimeConstant
2129

2230
SC_NAND_controller:
2331
ros__parameters:
@@ -37,4 +45,4 @@ SC:
3745

3846
SC_vision:
3947
ros__parameters:
40-
model_name: "01-15-25_no_pushbar_yolov11n.pt"
48+
model_name: "01-15-25_no_pushbar_yolov11n.pt"
Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
SC:
22
SC_sim_single:
33
ros__parameters:
4+
pose: 'Hill1_SC'
45
velocity: 12.0
5-
steering_offset: 8.0 # between 20.0 and -20.0, in degrees
6-
steering_offset_func: 'sin' # 'constant' or 'sin'
6+
steering_offset: 4.0 # between 20.0 and -20.0, in degrees
7+
steering_offset_func: 'constant' # 'constant' or 'sin'
78
steering_offset_period: 10.0 # seconds, only used when steering_offset_func is 'sin'
89
steering_delay: 0 # 1 step = 0.01s (at 100 hz steering)
9-
step_noise_std: 0.05 # standard deviation of simulated GPS noise (in meters)
10-
pose: 'Hill1_SC'
10+
11+
# noise compounds at each timestep, note: square root of time rule applies
12+
process_noise_std: 0.001 # standard deviation of process GPS noise (in meters)
13+
measurement_noise_std: 0.02 # standard deviation of measured GPS noise (in meters)
1114

1215
SC_controller:
1316
ros__parameters:
1417
dist: 0.0
1518
traj_name: 'buggycourse_sc.json'
1619
controller: 'stanley'
17-
useSteerOffset: false
20+
useSteerOffset: true
21+
22+
SC_offset_estimator:
23+
ros__parameters:
24+
steerOffsetRawTopic: "self/steering_offset/raw"
25+
steerOffsetFilteredTopic: "self/steering_offset/filtered"
26+
steerOffsetFilterTimeConstant: 50
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
<launch>
22
<arg name="config_file" default="src/buggy/config/nand-roll.yaml"/>
33
<node pkg="buggy" exec="controller_node.py" name="NAND_controller" output = "screen" namespace="NAND">
4-
<param from="$(var config_file)"/>
4+
<param from="$(var config_file)"/>
5+
</node>
6+
<node pkg="buggy" exec="steer_offset_estimator.py" name="NAND_offset_estimator" namespace="NAND">
7+
<param from="$(var config_file)"/>
58
</node>
69
<node pkg="buggy" exec="steer_offset_estimator.py" name="NAND_offset_estimator" namespace="NAND"/>
710
</launch>

rb_ws/src/buggy/launch/sc-main.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
<node pkg="buggy" exec="path_planner.py" name="SC_path_planner" namespace="SC" output = "screen">
77
<param from="$(var config_file)"/>
88
</node>
9-
<node pkg="buggy" exec="steer_offset_estimator.py" name="SC_offset_estimator" namespace="SC"/>
9+
<node pkg="buggy" exec="steer_offset_estimator.py" name="SC_offset_estimator" namespace="SC">
10+
<param from="$(var config_file)"/>
11+
</node>
1012

1113
<node pkg="buggy" exec="controller_node.py" name="SC_NAND_controller" output = "screen" namespace="SC">
1214
<param from="$(var config_file)"/>

rb_ws/src/buggy/launch/sim_2d_single.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
<node pkg="buggy" exec="controller_node.py" name="SC_controller" namespace="SC">
1212
<param from="$(var config_file)"/>
1313
</node>
14-
<node pkg="buggy" exec="steer_offset_estimator.py" name="SC_offset_estimator" namespace="SC"/>
14+
<node pkg="buggy" exec="steer_offset_estimator.py" name="SC_offset_estimator" namespace="SC">
15+
<param from="$(var config_file)"/>
16+
</node>
1517

1618
<!-- # This is the new velocity-updated code (commented out for now in case it broke something)
1719

rb_ws/src/buggy/scripts/controller/controller_node.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,32 +25,26 @@ def __init__(self):
2525
super().__init__('controller')
2626
self.get_logger().info('INITIALIZED.')
2727

28-
29-
#Parameters
30-
self.declare_parameter("dist", 0.0) #Starting Distance along path
28+
# Parameters
29+
self.declare_parameter("dist", 0.0) # Starting Distance along path
3130
start_dist = self.get_parameter("dist").value
3231

33-
self.declare_parameter("controllerName", "controller")
34-
35-
self.declare_parameter("traj_name", "buggycourse_safe.json")
36-
traj_name = self.get_parameter("traj_name").value
37-
self.cur_traj = Trajectory(json_filepath=os.environ["TRAJPATH"] + traj_name)
38-
3932
self.declare_parameter("stateTopic", "self/state")
4033
self.declare_parameter("steeringTopic", "input/steering")
4134
self.declare_parameter("rawSteeringTopic", "input/steering_raw")
4235
self.declare_parameter("trajectoryTopic", "self/cur_traj")
43-
self.declare_parameter("offsetTopic", "self/steer_offset")
36+
self.declare_parameter("steerOffsetTopic", "self/steering_offset/filtered")
4437
self.declare_parameter("useSteerOffset", False)
4538
self.use_steer_offset = self.get_parameter("useSteerOffset").value
4639

40+
self.declare_parameter("traj_name", "buggycourse_safe.json")
41+
traj_name = self.get_parameter("traj_name").value
42+
self.cur_traj = Trajectory(json_filepath=os.environ["TRAJPATH"] + traj_name)
4743
start_index = self.cur_traj.get_index_from_distance(start_dist)
48-
49-
self.declare_parameter("controller", "stanley")
50-
51-
5244
self.declare_parameter("useHeadingRate", True)
5345

46+
self.declare_parameter("controllerName", "controller")
47+
self.declare_parameter("controller", "stanley")
5448
controller_name = self.get_parameter("controller").value
5549
print(controller_name.lower())
5650
if (controller_name.lower() == "stanley"):
@@ -78,7 +72,7 @@ def __init__(self):
7872
# Subscribers
7973
self.odom_subscriber = self.create_subscription(Odometry, self.get_parameter("stateTopic").value, self.odom_listener, 1)
8074
self.traj_subscriber = self.create_subscription(TrajectoryMsg, self.get_parameter("trajectoryTopic").value, self.traj_listener, 1)
81-
self.steer_offset_subscriber = self.create_subscription(Float64, self.get_parameter("offsetTopic").value, self.offset_listener, 1)
75+
self.steer_offset_subscriber = self.create_subscription(Float64, self.get_parameter("steerOffsetTopic").value, self.offset_listener, 1)
8276

8377
self.odom = None
8478
self.passed_init = False
@@ -170,7 +164,6 @@ def loop(self):
170164
self.steer_publisher.publish(StampedFloat64Msg(header=odom.header, data=float(steering_angle_deg.item())))
171165

172166

173-
174167
def main(args=None):
175168
rclpy.init(args=args)
176169

rb_ws/src/buggy/scripts/estimation/nand_estimator.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from nav_msgs.msg import Odometry
99
from buggy.msg import StampedFloat64Msg
1010

11+
from util.constants import Constants
1112

1213
from ukf_utils import *
1314

@@ -90,7 +91,7 @@ def __init__(self):
9091
self.start = False
9192

9293
self.x_hat = None
93-
self.Sigma = np.diag([1e-4, 1e-4, 1e-2, 1e-2]) #state covariance
94+
self.Sigma = np.diag([1e-4, 1e-4, 1e-2, 1e-2]) # state covariance
9495
self.R = self.accuracy_to_mat(50)
9596
self.Q = np.diag([1e-4, 1e-4, 1e-2, 2.4e-1])
9697

@@ -148,17 +149,41 @@ def loop(self):
148149

149150
def accuracy_to_mat(self, accuracy):
150151
"""
151-
Convert a scalar accuracy to a 2x2 position covariance matrix.
152+
Convert a scalar *circular* position accuracy to a 2x2 position covariance matrix.
152153
153154
Args:
154-
accuracy: Position accuracy.
155+
accuracy: Position accuracy (radius) in millimeters.
156+
157+
We assume the position error in x and y follows a 2D Gaussian
158+
(normal) distribution that is:
159+
* centered at zero (no bias)
160+
* has the same spread in x and y
161+
* has no correlation between x and y
162+
163+
The constant 0.848867684498 is a precomputed factor that relates
164+
this circular accuracy radius to the underlying standard deviation
165+
of that Gaussian.
166+
167+
Once we have that standard deviation σ (in meters), the covariance
168+
matrix for (x, y) is simply:
169+
[ σ² 0 ]
170+
[ 0 σ² ]
155171
156172
Returns:
157-
2x2 diagonal covariance matrix for x/y position.
173+
2x2 diagonal covariance matrix for x/y position (in m²).
158174
"""
175+
# convert radius from millimeters to meters
159176
accuracy /= 1000.0
160-
sigma = (accuracy / (0.848867684498)) * (accuracy / (0.848867684498))
161-
return np.diag([sigma, sigma])
177+
178+
# accuracy is the circular error radius (meters)
179+
# k is the factor that maps between this radius and the Gaussian σ
180+
k = Constants.CEP50_to_STD
181+
sigma = accuracy / k
182+
183+
# variance in each axis (assuming isotropic uncertainty)
184+
sigma_sq = sigma * sigma
185+
186+
return np.diag([sigma_sq, sigma_sq])
162187

163188
def main(args=None):
164189
rclpy.init(args=args)

rb_ws/src/buggy/scripts/estimation/steer_offset_estimator.py

100644100755
Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from estimation import ukf_utils
1414
from util.constants import Constants
15+
from util.LowPassFilter import LowPassFilter
1516

1617
class SteerOffsetEstimator(Node):
1718
"""
@@ -95,15 +96,15 @@ def __init__(self):
9596
self.enabled = True # estimator enabled
9697
self.auton_enabled_prev = None # previous auton flag for edge detection
9798

98-
# moved to reset_filter()
99-
# self.x_hat: np.ndarray = None
100-
# self.Sigma: np.ndarray = np.diag([1e-4, 1e-4, 1e-2, 1e-2, 1.2e-3]) # state covariance
101-
# self.Q = np.diag([1e-4, 1e-4, 1e-4, 2.4e-1, 1e-6]) # init process covariance values (2.4e-1 for velocity based on 3 x std dev of 0.16)
102-
# self.R = np.diag([1e-2, 1e-2]) # init sensor covariance values
103-
# self.last_time = None
104-
10599
self.reset_filter() # initialize filter state
106100

101+
self.declare_parameter("steerOffsetFilterTimeConstant", 50)
102+
steerOffsetFilterTimeConstant = self.get_parameter("steerOffsetFilterTimeConstant").value
103+
self.lowPassFilter = LowPassFilter(alpha = 1.0 / steerOffsetFilterTimeConstant)
104+
105+
self.declare_parameter("steerOffsetRawTopic", "self/steer_offset/raw")
106+
self.declare_parameter("steerOffsetFilteredTopic", "self/steer_offset/filtered")
107+
107108
if (self.get_namespace() == "/SC"):
108109
self.wheelbase = Constants.WHEELBASE_SC
109110
self.create_subscription(SCDebugInfoMsg, "debug/firmware", self.firmware_debug_callback, 1)
@@ -112,10 +113,13 @@ def __init__(self):
112113
self.create_subscription(NANDDebugInfoMsg, "debug/firmware", self.firmware_debug_callback, 1)
113114
self.create_subscription(Odometry, "self/state", self.update_measurement, 1) # Using EKF output for simplicity
114115
self.create_subscription(StampedFloat64Msg, "input/steering", self.update_steering, 1)
115-
self.offset_publisher = self.create_publisher(Float64, "self/steer_offset", 1)
116+
117+
self.offset_publisher_raw = self.create_publisher(Float64, self.get_parameter("steerOffsetRawTopic").value, 1)
118+
self.offset_publisher_filtered = self.create_publisher(Float64, self.get_parameter("steerOffsetFilteredTopic").value, 1)
116119
self.state_publisher = self.create_publisher(Float64MultiArray, "self/offset_estimator/state", 1)
117120
self.state_covar_publisher = self.create_publisher(Float64MultiArray, "self/offset_estimator/covariance", 1)
118121

122+
119123
self.steering = 0
120124

121125
self.timer = self.create_timer(0.01, self.loop)
@@ -124,8 +128,10 @@ def reset_filter(self):
124128
"""Reset the UKF so next measurement initializes state."""
125129
self.start = False
126130
self.x_hat: np.ndarray = np.zeros((5,)) # state vector
127-
self.Sigma: np.ndarray = np.diag([1e-4, 1e-4, 1e-2, 1e-2, 1.2e-3]) # state covariance
128-
self.Q = np.diag([1e-4, 1e-4, 1e-4, 2.4e-1, 1e-6]) # init process covariance values (2.4e-1 for velocity based on 3 x std dev of 0.16)
131+
132+
# Offset variance extremely high out of caution and proof of convergence
133+
self.Sigma: np.ndarray = np.diag([1e-4, 1e-4, 1e-2, 1e-2, 5e-2]) # state covariance
134+
self.Q = np.diag([1e-4, 1e-4, 1e-4, 2.4e-1, 1e-6]) # init process covariance values (2.4e-1 for velocity based on 3 x std dev of 0.16)
129135
self.R = np.diag([1e-2, 1e-2]) # init sensor covariance values
130136
self.last_time = None
131137

@@ -180,11 +186,11 @@ def update_measurement(self, msg):
180186
msg.pose.pose.orientation.z,
181187
msg.twist.twist.linear.x,
182188
0])
183-
# extract 2x2 position covariance from the 6x6 pose covariance
184-
self.R = np.reshape(np.stack((msg.pose.covariance[:2], msg.pose.covariance[6:8]), axis=0), (2, 2))
185189

186190
# measurement vector
187191
y = [msg.pose.pose.position.x, msg.pose.pose.position.y]
192+
# extract 2x2 position covariance from the 6x6 pose covariance
193+
self.R = np.reshape(np.stack((msg.pose.covariance[:2], msg.pose.covariance[6:8]), axis=0), (2, 2))
188194
# perform measurement update
189195
self.x_hat, self.Sigma, self.debug = ukf_utils.ukf_update(self.x_hat, self.Sigma, y, self.R)
190196

@@ -197,7 +203,10 @@ def loop(self):
197203
198204
- Runs the predict step using the RK4-discretized dynamics.
199205
- Wraps heading and steering offset to keep them in valid ranges.
200-
- Publishes steer offset, full state, and covariance at 100 Hz.
206+
- Applies a low-pass filter to the steering offset.
207+
- A low-pass filter filters out high-frequency noise in the estimate at the cost of responsiveness/increased lag;
208+
this is okay since we expect the steering offset to vary slowly over time.
209+
- Publishes steer offset (raw and filtered), full state, and covariance at 100 Hz.
201210
"""
202211
if (not self.enabled) or (not self.start):
203212
return
@@ -208,10 +217,6 @@ def loop(self):
208217
self.x_hat[4] = self.wrap_angle(self.x_hat[4], np.pi/2) # wrap steer offset
209218
self.last_time = time.time()
210219

211-
# wrap the steering offset to (-pi/2, pi/2]
212-
steer_offset = np.rad2deg(self.wrap_angle(self.x_hat[4], np.pi/2))
213-
self.offset_publisher.publish(Float64(data=steer_offset))
214-
215220
state_msg = Float64MultiArray()
216221
state_msg.data = self.x_hat.tolist()
217222
state_msg.data[2] = np.rad2deg(state_msg.data[2]) # convert heading to deg
@@ -223,6 +228,20 @@ def loop(self):
223228
covar_msg.data = self.Sigma.flatten().tolist()
224229
self.state_covar_publisher.publish(covar_msg)
225230

231+
offset_variance = self.Sigma[-1, -1]
232+
233+
# Checks the offset variance is reasonable, corresponds to 6 deg std deviation.
234+
if offset_variance < Constants.OFFSET_THRESHOLD:
235+
236+
# wrap the steering offset to (-pi/2, pi/2]
237+
steer_offset = np.rad2deg(self.wrap_angle(self.x_hat[4], np.pi/2))
238+
self.offset_publisher_raw.publish(Float64(data=steer_offset))
239+
240+
# apply low-pass filter to steering offset
241+
steer_offset_filtered = self.lowPassFilter.update(steer_offset)
242+
self.offset_publisher_filtered.publish(Float64(data=steer_offset_filtered))
243+
244+
226245

227246
def main(args=None):
228247
rclpy.init(args=args)

0 commit comments

Comments
 (0)