Skip to content

Commit aed6e86

Browse files
committed
add contact-aware odometry source
1 parent 9c2d19b commit aed6e86

23 files changed

Lines changed: 1371 additions & 69 deletions

File tree

Cargo.lock

Lines changed: 21 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ members = [
6767
"crates/nodes/microphone_recorder",
6868
"crates/nodes/motor_commands_collector",
6969
"crates/nodes/obstacle_filter",
70+
"crates/nodes/odometry",
7071
"crates/nodes/odometer_bridge",
7172
"crates/nodes/player_states_receiver",
7273
"crates/nodes/primary_state_filter",
@@ -271,6 +272,7 @@ num-derive = "0.4.2"
271272
num-dual = "0.13.2"
272273
num-traits = "0.2"
273274
obstacle_filter = { path = "crates/nodes/obstacle_filter" }
275+
odometry = { path = "crates/nodes/odometry" }
274276
odometer_bridge = { path = "crates/nodes/odometer_bridge" }
275277
once_cell = "1.20.3"
276278
openvino = { version = "0.8.0", features = ["runtime-linking"] }

crates/coordinate_systems/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ generate_coordinate_system!(
4141
/// Origin: center between [LeftSole] and [RightSole], projected onto the ground.
4242
/// X axis pointing forward
4343
Ground,
44+
/// 2D odometry frame fixed at the startup pose of one odometry source.
45+
///
46+
/// Origin: robot [Ground] frame when the odometry source starts.
47+
/// X axis pointing along the startup ground-frame X axis.
48+
/// Y axis pointing along the startup ground-frame Y axis.
49+
Odometry,
4450
/// coordinate system used to express feet positions in the walking engine.
4551
///
4652
/// Origin: below the robot's hip

crates/hulk_ros_z/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ message_handler = { workspace = true }
4242
microphone_recorder = { workspace = true }
4343
motor_commands_collector = { workspace = true }
4444
obstacle_filter = { workspace = true }
45+
odometry = { workspace = true }
4546
odometer_bridge = { workspace = true }
4647
player_states_receiver = { workspace = true }
4748
primary_state_filter = { workspace = true }

crates/hulk_ros_z/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ async fn spawn_all(ctx: Arc<Context>, log_path: Option<PathBuf>) -> Result<Runni
166166
join_set.spawn(motor_commands_collector::run_boxed(ctx.clone()));
167167
join_set.spawn(obstacle_filter::run_boxed(ctx.clone()));
168168
join_set.spawn(odometer_bridge::run_boxed(ctx.clone()));
169+
join_set.spawn(odometry::run_boxed(ctx.clone()));
169170
join_set.spawn(player_states_receiver::run_boxed(ctx.clone()));
170171
join_set.spawn(primary_state_filter::run_boxed(ctx.clone()));
171172
join_set.spawn(visual_kick_ball_selector::run_boxed(ctx.clone()));

crates/kinematics/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pub mod joints_velocity;
44
pub mod robot_dimensions;
55
pub mod robot_kinematics;
66
pub mod robot_masses;
7+
pub mod sole_contact;
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use coordinate_systems::Robot;
2+
use linear_algebra::{Isometry3, Orientation3, Point3, Vector2, nalgebra};
3+
4+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5+
pub enum SoleSide {
6+
Left,
7+
Right,
8+
}
9+
10+
#[derive(Clone, Copy, Debug)]
11+
pub struct SoleContact {
12+
pub min_z: f32,
13+
pub contact_xy_in_leveled_robot: Vector2<Robot>,
14+
pub candidate_count: usize,
15+
}
16+
17+
#[derive(Clone, Copy, Debug)]
18+
pub struct SupportSelectionParameters {
19+
pub double_support_deadband: f32,
20+
pub support_switch_hysteresis: f32,
21+
}
22+
23+
pub fn estimate_sole_contact<Sole>(
24+
sole_to_robot: Isometry3<Sole, Robot>,
25+
roll: f32,
26+
pitch: f32,
27+
vertices: &[Point3<Sole>],
28+
contact_height_epsilon: f32,
29+
) -> Option<SoleContact> {
30+
if vertices.is_empty() {
31+
return None;
32+
}
33+
34+
struct Level;
35+
36+
let level_to_robot =
37+
Isometry3::<Level, Robot>::from(Orientation3::from_euler_angles(roll, pitch, 0.0).mirror());
38+
let robot_to_level = level_to_robot.inverse();
39+
40+
let vertices_in_level = vertices
41+
.iter()
42+
.map(|vertex| robot_to_level * (sole_to_robot * *vertex))
43+
.collect::<Vec<_>>();
44+
let min_z = vertices_in_level
45+
.iter()
46+
.map(|vertex| vertex.z())
47+
.fold(f32::INFINITY, f32::min);
48+
49+
let candidates = vertices_in_level
50+
.iter()
51+
.filter(|vertex| vertex.z() <= min_z + contact_height_epsilon)
52+
.collect::<Vec<_>>();
53+
let candidate_count = candidates.len();
54+
if candidate_count == 0 {
55+
return None;
56+
}
57+
58+
let contact_xy_sum = candidates
59+
.iter()
60+
.map(|vertex| vertex.xy().coords().inner)
61+
.sum::<nalgebra::Vector2<f32>>();
62+
let contact_xy = contact_xy_sum / candidate_count as f32;
63+
64+
Some(SoleContact {
65+
min_z,
66+
contact_xy_in_leveled_robot: Vector2::wrap(contact_xy),
67+
candidate_count,
68+
})
69+
}
70+
71+
pub fn select_support_side(
72+
left_contact: SoleContact,
73+
right_contact: SoleContact,
74+
previous_support_side: Option<SoleSide>,
75+
parameters: SupportSelectionParameters,
76+
) -> Option<SoleSide> {
77+
let height_difference = left_contact.min_z - right_contact.min_z;
78+
79+
if height_difference.abs() < parameters.double_support_deadband {
80+
return None;
81+
}
82+
83+
match previous_support_side {
84+
Some(SoleSide::Left) if height_difference < parameters.support_switch_hysteresis => {
85+
Some(SoleSide::Left)
86+
}
87+
Some(SoleSide::Right) if height_difference > -parameters.support_switch_hysteresis => {
88+
Some(SoleSide::Right)
89+
}
90+
_ if height_difference < 0.0 => Some(SoleSide::Left),
91+
_ => Some(SoleSide::Right),
92+
}
93+
}
94+
95+
#[cfg(test)]
96+
mod tests {
97+
use super::*;
98+
use linear_algebra::{IntoTransform, nalgebra, point, vector};
99+
100+
#[derive(Clone, Copy, Debug)]
101+
struct Sole;
102+
103+
fn vertices() -> [Point3<Sole>; 4] {
104+
[
105+
point![<Sole>, 0.1, 0.05, 0.0],
106+
point![<Sole>, 0.1, -0.05, 0.0],
107+
point![<Sole>, -0.1, 0.05, 0.0],
108+
point![<Sole>, -0.1, -0.05, 0.0],
109+
]
110+
}
111+
112+
#[test]
113+
fn flat_sole_uses_all_vertices_and_center_contact() {
114+
let contact = estimate_sole_contact(Isometry3::identity(), 0.0, 0.0, &vertices(), 0.001)
115+
.expect("flat sole has a contact");
116+
117+
assert_eq!(contact.candidate_count, 4);
118+
assert!((contact.contact_xy_in_leveled_robot.x() - 0.0).abs() < 1.0e-6);
119+
assert!((contact.contact_xy_in_leveled_robot.y() - 0.0).abs() < 1.0e-6);
120+
}
121+
122+
#[test]
123+
fn toe_down_pitch_uses_front_vertices() {
124+
let sole_to_robot = nalgebra::Isometry3::from_parts(
125+
nalgebra::Translation3::identity(),
126+
nalgebra::UnitQuaternion::from_euler_angles(0.0, 0.2, 0.0),
127+
)
128+
.framed_transform();
129+
130+
let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001)
131+
.expect("toe-down sole has a contact");
132+
133+
assert_eq!(contact.candidate_count, 2);
134+
assert!(contact.contact_xy_in_leveled_robot.x() > 0.09);
135+
}
136+
137+
#[test]
138+
fn heel_down_pitch_uses_rear_vertices() {
139+
let sole_to_robot = nalgebra::Isometry3::from_parts(
140+
nalgebra::Translation3::identity(),
141+
nalgebra::UnitQuaternion::from_euler_angles(0.0, -0.2, 0.0),
142+
)
143+
.framed_transform();
144+
145+
let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001)
146+
.expect("heel-down sole has a contact");
147+
148+
assert_eq!(contact.candidate_count, 2);
149+
assert!(contact.contact_xy_in_leveled_robot.x() < -0.09);
150+
}
151+
152+
#[test]
153+
fn side_down_roll_uses_left_vertices() {
154+
let sole_to_robot = nalgebra::Isometry3::from_parts(
155+
nalgebra::Translation3::identity(),
156+
nalgebra::UnitQuaternion::from_euler_angles(0.2, 0.0, 0.0),
157+
)
158+
.framed_transform();
159+
160+
let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001)
161+
.expect("side-down sole has a contact");
162+
163+
assert_eq!(contact.candidate_count, 2);
164+
assert!(contact.contact_xy_in_leveled_robot.y().abs() > 0.04);
165+
}
166+
167+
#[test]
168+
fn corner_down_combined_roll_and_pitch_uses_one_vertex() {
169+
let sole_to_robot = nalgebra::Isometry3::from_parts(
170+
nalgebra::Translation3::identity(),
171+
nalgebra::UnitQuaternion::from_euler_angles(0.2, 0.2, 0.0),
172+
)
173+
.framed_transform();
174+
175+
let contact = estimate_sole_contact(sole_to_robot, 0.0, 0.0, &vertices(), 0.001)
176+
.expect("corner-down sole has a contact");
177+
178+
assert_eq!(contact.candidate_count, 1);
179+
assert!(contact.contact_xy_in_leveled_robot.x() > 0.09);
180+
assert!(contact.contact_xy_in_leveled_robot.y().abs() > 0.04);
181+
}
182+
183+
#[test]
184+
fn double_support_deadband_returns_none() {
185+
let left = SoleContact {
186+
min_z: 0.0,
187+
contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0],
188+
candidate_count: 4,
189+
};
190+
let right = SoleContact {
191+
min_z: 0.002,
192+
contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0],
193+
candidate_count: 4,
194+
};
195+
196+
let side = select_support_side(
197+
left,
198+
right,
199+
None,
200+
SupportSelectionParameters {
201+
double_support_deadband: 0.003,
202+
support_switch_hysteresis: 0.004,
203+
},
204+
);
205+
206+
assert_eq!(side, None);
207+
}
208+
209+
#[test]
210+
fn hysteresis_keeps_previous_side_near_switch_threshold() {
211+
let left = SoleContact {
212+
min_z: 0.003,
213+
contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0],
214+
candidate_count: 2,
215+
};
216+
let right = SoleContact {
217+
min_z: 0.0,
218+
contact_xy_in_leveled_robot: vector![<Robot>, 0.0, 0.0],
219+
candidate_count: 2,
220+
};
221+
222+
let side = select_support_side(
223+
left,
224+
right,
225+
Some(SoleSide::Left),
226+
SupportSelectionParameters {
227+
double_support_deadband: 0.001,
228+
support_switch_hysteresis: 0.004,
229+
},
230+
);
231+
232+
assert_eq!(side, Some(SoleSide::Left));
233+
}
234+
}

crates/nodes/ball_filter/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ license.workspace = true
66
homepage.workspace = true
77

88
[dependencies]
9-
booster.workspace = true
109
color-eyre = { workspace = true }
1110
coordinate_systems = { workspace = true }
1211
filtering = { workspace = true }

0 commit comments

Comments
 (0)