Desmond-Dong commited on
Commit
a946134
·
1 Parent(s): f9facd8

refactor(movement): use SDK compose_world_offset for proper pose composition

Browse files
reachy_mini_ha_voice/movement_manager.py CHANGED
@@ -5,14 +5,24 @@ This module provides a centralized control system for robot movements,
5
  inspired by the reachy_mini_conversation_app architecture.
6
 
7
  Key features:
8
- - Single 10Hz control loop (reduced from 100Hz to prevent daemon crashes)
9
  - Command queue pattern (thread-safe external API)
10
  - Error throttling (prevents log explosion)
11
- - Speech-driven head sway
12
- - Breathing animation during idle
13
  - Graceful shutdown
14
  - Pose change detection (skip sending if no significant change)
15
  - Robust connection recovery (faster reconnection attempts)
 
 
 
 
 
 
 
 
 
 
16
  """
17
 
18
  import logging
@@ -30,6 +40,15 @@ from scipy.spatial.transform import Rotation as R
30
  if TYPE_CHECKING:
31
  from reachy_mini import ReachyMini
32
 
 
 
 
 
 
 
 
 
 
33
  logger = logging.getLogger(__name__)
34
 
35
 
@@ -665,36 +684,82 @@ class MovementManager:
665
  logger.debug("Error getting face tracking offsets: %s", e)
666
 
667
  def _compose_final_pose(self) -> Dict[str, float]:
668
- """Compose final pose from all sources."""
669
- # Primary pose (from actions)
670
- pitch = self.state.target_pitch
671
- yaw = self.state.target_yaw
672
- roll = self.state.target_roll
673
- x = self.state.target_x
674
- y = self.state.target_y
675
- z = self.state.target_z
676
-
677
- # Add speech sway (secondary) - rotation and translation
678
- pitch += self.state.speech_pitch
679
- yaw += self.state.speech_yaw
680
- roll += self.state.speech_roll
681
- x += self.state.speech_x
682
- y += self.state.speech_y
683
- z += self.state.speech_z
684
-
685
- # Add breathing
686
- z += self.state.breathing_z
687
 
688
- # Add face tracking offsets (from camera worker)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  with self._face_tracking_lock:
690
  face_offsets = self._face_tracking_offsets
691
 
692
- x += face_offsets[0]
693
- y += face_offsets[1]
694
- z += face_offsets[2]
695
- roll += face_offsets[3]
696
- pitch += face_offsets[4]
697
- yaw += face_offsets[5]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
 
699
  # Antenna pose with freeze blending
700
  target_antenna_left = self.state.target_antenna_left + self.state.breathing_antenna_left
@@ -713,12 +778,12 @@ class MovementManager:
713
  antenna_right = target_antenna_right
714
 
715
  return {
716
- "pitch": pitch,
717
- "yaw": yaw,
718
- "roll": roll,
719
- "x": x,
720
- "y": y,
721
- "z": z,
722
  "antenna_left": antenna_left,
723
  "antenna_right": antenna_right,
724
  "body_yaw": self.state.target_body_yaw,
 
5
  inspired by the reachy_mini_conversation_app architecture.
6
 
7
  Key features:
8
+ - Single 20Hz control loop (balanced between responsiveness and stability)
9
  - Command queue pattern (thread-safe external API)
10
  - Error throttling (prevents log explosion)
11
+ - Speech-driven head sway (based on conversation_app's SwayRollRT)
12
+ - Breathing animation during idle (based on conversation_app's BreathingMove)
13
  - Graceful shutdown
14
  - Pose change detection (skip sending if no significant change)
15
  - Robust connection recovery (faster reconnection attempts)
16
+ - Proper pose composition using SDK's compose_world_offset (same as conversation_app)
17
+ - Antenna freeze during listening mode with smooth blend back
18
+
19
+ SDK Analysis Notes:
20
+ - get_current_head_pose() and get_current_joint_positions() are non-blocking
21
+ (they return cached Zenoh data from subscriptions)
22
+ - set_target() is the only method that sends Zenoh messages
23
+ - get_status() may trigger I/O, so it's cached in reachy_controller.py
24
+
25
+ Reference: reachy_mini_conversation_app/src/reachy_mini_conversation_app/moves.py
26
  """
27
 
28
  import logging
 
40
  if TYPE_CHECKING:
41
  from reachy_mini import ReachyMini
42
 
43
+ # Import SDK utilities for pose composition (same as conversation_app)
44
+ try:
45
+ from reachy_mini.utils import create_head_pose
46
+ from reachy_mini.utils.interpolation import compose_world_offset
47
+ SDK_UTILS_AVAILABLE = True
48
+ except ImportError:
49
+ SDK_UTILS_AVAILABLE = False
50
+ logger.warning("SDK utils not available, using fallback pose composition")
51
+
52
  logger = logging.getLogger(__name__)
53
 
54
 
 
684
  logger.debug("Error getting face tracking offsets: %s", e)
685
 
686
  def _compose_final_pose(self) -> Dict[str, float]:
687
+ """Compose final pose from all sources.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
 
689
+ Uses SDK's compose_world_offset for proper pose composition (same as conversation_app).
690
+ Primary pose comes from actions, secondary offsets come from speech sway and face tracking.
691
+ """
692
+ # Build primary head pose matrix (from actions)
693
+ if SDK_UTILS_AVAILABLE:
694
+ primary_head = create_head_pose(
695
+ x=self.state.target_x,
696
+ y=self.state.target_y,
697
+ z=self.state.target_z,
698
+ roll=self.state.target_roll,
699
+ pitch=self.state.target_pitch,
700
+ yaw=self.state.target_yaw,
701
+ degrees=False, # Our state is in radians
702
+ mm=False, # Our state is in meters
703
+ )
704
+ else:
705
+ # Fallback: build matrix manually
706
+ rotation = R.from_euler('xyz', [
707
+ self.state.target_roll,
708
+ self.state.target_pitch,
709
+ self.state.target_yaw,
710
+ ])
711
+ primary_head = np.eye(4)
712
+ primary_head[:3, :3] = rotation.as_matrix()
713
+ primary_head[0, 3] = self.state.target_x
714
+ primary_head[1, 3] = self.state.target_y
715
+ primary_head[2, 3] = self.state.target_z
716
+
717
+ # Build secondary offset pose (speech sway + face tracking + breathing)
718
+ # Get face tracking offsets
719
  with self._face_tracking_lock:
720
  face_offsets = self._face_tracking_offsets
721
 
722
+ # Combine all secondary offsets
723
+ secondary_x = self.state.speech_x + face_offsets[0]
724
+ secondary_y = self.state.speech_y + face_offsets[1]
725
+ secondary_z = self.state.speech_z + self.state.breathing_z + face_offsets[2]
726
+ secondary_roll = self.state.speech_roll + face_offsets[3]
727
+ secondary_pitch = self.state.speech_pitch + face_offsets[4]
728
+ secondary_yaw = self.state.speech_yaw + face_offsets[5]
729
+
730
+ if SDK_UTILS_AVAILABLE:
731
+ secondary_head = create_head_pose(
732
+ x=secondary_x,
733
+ y=secondary_y,
734
+ z=secondary_z,
735
+ roll=secondary_roll,
736
+ pitch=secondary_pitch,
737
+ yaw=secondary_yaw,
738
+ degrees=False,
739
+ mm=False,
740
+ )
741
+ # Compose using SDK utility (same as conversation_app)
742
+ combined_head = compose_world_offset(primary_head, secondary_head, reorthonormalize=True)
743
+ else:
744
+ # Fallback: simple addition (less accurate but works)
745
+ secondary_rotation = R.from_euler('xyz', [secondary_roll, secondary_pitch, secondary_yaw])
746
+ secondary_head = np.eye(4)
747
+ secondary_head[:3, :3] = secondary_rotation.as_matrix()
748
+ secondary_head[0, 3] = secondary_x
749
+ secondary_head[1, 3] = secondary_y
750
+ secondary_head[2, 3] = secondary_z
751
+
752
+ # Simple composition: R_final = R_secondary @ R_primary, t_final = t_primary + t_secondary
753
+ combined_head = np.eye(4)
754
+ combined_head[:3, :3] = secondary_head[:3, :3] @ primary_head[:3, :3]
755
+ combined_head[:3, 3] = primary_head[:3, 3] + secondary_head[:3, 3]
756
+
757
+ # Extract final pose values from combined matrix
758
+ final_rotation = R.from_matrix(combined_head[:3, :3])
759
+ final_roll, final_pitch, final_yaw = final_rotation.as_euler('xyz')
760
+ final_x = combined_head[0, 3]
761
+ final_y = combined_head[1, 3]
762
+ final_z = combined_head[2, 3]
763
 
764
  # Antenna pose with freeze blending
765
  target_antenna_left = self.state.target_antenna_left + self.state.breathing_antenna_left
 
778
  antenna_right = target_antenna_right
779
 
780
  return {
781
+ "pitch": final_pitch,
782
+ "yaw": final_yaw,
783
+ "roll": final_roll,
784
+ "x": final_x,
785
+ "y": final_y,
786
+ "z": final_z,
787
  "antenna_left": antenna_left,
788
  "antenna_right": antenna_right,
789
  "body_yaw": self.state.target_body_yaw,