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

refactor: optimize based on SDK analysis and conversation_app reference

Browse files

Key improvements:
1. Remove unnecessary caching in reachy_controller.py
- SDK's get_current_head_pose() and get_current_joint_positions() are
non-blocking (return cached Zenoh data), no extra caching needed
- Keep status caching only for get_status() which may trigger I/O

2. Increase control loop frequency from 5Hz to 20Hz
- Safe because SDK state queries are non-blocking
- Better responsiveness for motion feedback

3. Reduce pose change threshold from 0.01 to 0.002 rad
- Smoother motion transitions
- 0.002 rad 0.11 degrees

Reference: reachy_mini_conversation_app uses 100Hz with similar architecture

reachy_mini_ha_voice/movement_manager.py CHANGED
@@ -37,7 +37,10 @@ logger = logging.getLogger(__name__)
37
  # Constants (borrowed from conversation_app)
38
  # =============================================================================
39
 
40
- CONTROL_LOOP_FREQUENCY_HZ = 5 # 5Hz control loop (reduced from 10Hz to prevent daemon serial port overload)
 
 
 
41
  TARGET_PERIOD = 1.0 / CONTROL_LOOP_FREQUENCY_HZ
42
 
43
  # Speech sway parameters (from conversation_app SwayRollRT)
@@ -348,11 +351,11 @@ class MovementManager:
348
  self._audio_loudness_db: float = -100.0
349
  self._audio_lock = threading.Lock()
350
 
351
- # Pose change detection (prevent unnecessary commands)
 
 
352
  self._last_sent_pose: Optional[Dict[str, float]] = None
353
- # Increased threshold to reduce command frequency
354
- # 0.01 rad ≈ 0.57 degrees, prevents micro-movements from triggering commands
355
- self._pose_change_threshold = 0.01
356
 
357
  # Face tracking offsets (from camera worker)
358
  self._face_tracking_offsets: Tuple[float, float, float, float, float, float] = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
 
37
  # Constants (borrowed from conversation_app)
38
  # =============================================================================
39
 
40
+ CONTROL_LOOP_FREQUENCY_HZ = 20 # 20Hz control loop (increased from 5Hz based on SDK analysis)
41
+ # SDK's get_current_head_pose() and get_current_joint_positions() are non-blocking
42
+ # (they return cached Zenoh data), so higher frequency is safe.
43
+ # Using 20Hz as a balance between responsiveness and stability.
44
  TARGET_PERIOD = 1.0 / CONTROL_LOOP_FREQUENCY_HZ
45
 
46
  # Speech sway parameters (from conversation_app SwayRollRT)
 
351
  self._audio_loudness_db: float = -100.0
352
  self._audio_lock = threading.Lock()
353
 
354
+ # Pose change detection threshold
355
+ # 0.002 rad ≈ 0.11 degrees - small enough for smooth motion
356
+ # SDK's set_target() is the only method that sends Zenoh messages
357
  self._last_sent_pose: Optional[Dict[str, float]] = None
358
+ self._pose_change_threshold = 0.002
 
 
359
 
360
  # Face tracking offsets (from camera worker)
361
  self._face_tracking_offsets: Tuple[float, float, float, float, float, float] = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
reachy_mini_ha_voice/reachy_controller.py CHANGED
@@ -48,19 +48,12 @@ class ReachyController:
48
  self.reachy = reachy_mini
49
  self._speaker_volume = 100 # Default volume
50
 
51
- # State caching to reduce daemon load
52
- # Increased TTL to 1 second to prevent overwhelming the daemon
53
- # when Home Assistant subscribes to all entities at once
54
  self._state_cache: Dict[str, Any] = {}
55
- self._cache_ttl = 1.0 # 1 second cache TTL (was 100ms)
56
  self._last_status_query = 0.0
57
- self._last_pose_query = 0.0
58
- self._last_joints_query = 0.0
59
-
60
- # Request throttling to prevent daemon overload
61
- self._min_request_interval = 0.1 # Minimum 100ms between SDK requests
62
- self._last_sdk_request = 0.0
63
- self._request_lock = __import__('threading').Lock()
64
 
65
  # Thread lock for ReSpeaker USB access to prevent conflicts with GStreamer audio pipeline
66
  self._respeaker_lock = __import__('threading').Lock()
@@ -73,7 +66,12 @@ class ReachyController:
73
  # ========== Phase 1: Basic Status & Volume ==========
74
 
75
  def _get_cached_status(self) -> Optional[Dict]:
76
- """Get cached daemon status to reduce query frequency."""
 
 
 
 
 
77
  now = time.time()
78
  if now - self._last_status_query < self._cache_ttl:
79
  return self._state_cache.get('status')
@@ -81,13 +79,6 @@ class ReachyController:
81
  if not self.is_available:
82
  return None
83
 
84
- # Throttle SDK requests to prevent daemon overload
85
- with self._request_lock:
86
- if now - self._last_sdk_request < self._min_request_interval:
87
- # Return cached value if we're requesting too fast
88
- return self._state_cache.get('status')
89
- self._last_sdk_request = now
90
-
91
  try:
92
  status = self.reachy.client.get_status(wait=False)
93
  self._state_cache['status'] = status
@@ -336,53 +327,35 @@ class ReachyController:
336
 
337
  # ========== Phase 3: Pose Control ==========
338
 
339
- def _get_cached_head_pose(self) -> Optional[np.ndarray]:
340
- """Get cached head pose to reduce query frequency."""
341
- now = time.time()
342
- if now - self._last_pose_query < self._cache_ttl:
343
- return self._state_cache.get('head_pose')
344
 
 
 
 
345
  if not self.is_available:
346
  return None
347
 
348
- # Throttle SDK requests to prevent daemon overload
349
- with self._request_lock:
350
- if now - self._last_sdk_request < self._min_request_interval:
351
- return self._state_cache.get('head_pose')
352
- self._last_sdk_request = now
353
-
354
  try:
355
- pose = self.reachy.get_current_head_pose()
356
- self._state_cache['head_pose'] = pose
357
- self._last_pose_query = now
358
- return pose
359
  except Exception as e:
360
  logger.error(f"Error getting head pose: {e}")
361
- return self._state_cache.get('head_pose') # Return stale cache on error
362
 
363
- def _get_cached_joint_positions(self) -> Optional[tuple]:
364
- """Get cached joint positions to reduce query frequency."""
365
- now = time.time()
366
- if now - self._last_joints_query < self._cache_ttl:
367
- return self._state_cache.get('joint_positions')
368
 
 
 
 
369
  if not self.is_available:
370
  return None
371
 
372
- # Throttle SDK requests to prevent daemon overload
373
- with self._request_lock:
374
- if now - self._last_sdk_request < self._min_request_interval:
375
- return self._state_cache.get('joint_positions')
376
- self._last_sdk_request = now
377
-
378
  try:
379
- joints = self.reachy.get_current_joint_positions()
380
- self._state_cache['joint_positions'] = joints
381
- self._last_joints_query = now
382
- return joints
383
  except Exception as e:
384
  logger.error(f"Error getting joint positions: {e}")
385
- return self._state_cache.get('joint_positions') # Return stale cache on error
386
 
387
  def _extract_pose_from_matrix(self, pose_matrix: np.ndarray) -> tuple:
388
  """
@@ -409,7 +382,7 @@ class ReachyController:
409
 
410
  def get_head_x(self) -> float:
411
  """Get head X position in mm with caching."""
412
- pose = self._get_cached_head_pose()
413
  if pose is None:
414
  return 0.0
415
  try:
@@ -439,7 +412,7 @@ class ReachyController:
439
 
440
  def get_head_y(self) -> float:
441
  """Get head Y position in mm with caching."""
442
- pose = self._get_cached_head_pose()
443
  if pose is None:
444
  return 0.0
445
  try:
@@ -467,7 +440,7 @@ class ReachyController:
467
 
468
  def get_head_z(self) -> float:
469
  """Get head Z position in mm with caching."""
470
- pose = self._get_cached_head_pose()
471
  if pose is None:
472
  return 0.0
473
  try:
@@ -495,7 +468,7 @@ class ReachyController:
495
 
496
  def get_head_roll(self) -> float:
497
  """Get head roll angle in degrees with caching."""
498
- pose = self._get_cached_head_pose()
499
  if pose is None:
500
  return 0.0
501
  try:
@@ -526,7 +499,7 @@ class ReachyController:
526
 
527
  def get_head_pitch(self) -> float:
528
  """Get head pitch angle in degrees with caching."""
529
- pose = self._get_cached_head_pose()
530
  if pose is None:
531
  return 0.0
532
  try:
@@ -556,7 +529,7 @@ class ReachyController:
556
 
557
  def get_head_yaw(self) -> float:
558
  """Get head yaw angle in degrees with caching."""
559
- pose = self._get_cached_head_pose()
560
  if pose is None:
561
  return 0.0
562
  try:
@@ -586,7 +559,7 @@ class ReachyController:
586
 
587
  def get_body_yaw(self) -> float:
588
  """Get body yaw angle in degrees with caching."""
589
- joints = self._get_cached_joint_positions()
590
  if joints is None:
591
  return 0.0
592
  try:
@@ -611,7 +584,7 @@ class ReachyController:
611
 
612
  def get_antenna_left(self) -> float:
613
  """Get left antenna angle in degrees with caching."""
614
- joints = self._get_cached_joint_positions()
615
  if joints is None:
616
  return 0.0
617
  try:
@@ -638,7 +611,7 @@ class ReachyController:
638
 
639
  def get_antenna_right(self) -> float:
640
  """Get right antenna angle in degrees with caching."""
641
- joints = self._get_cached_joint_positions()
642
  if joints is None:
643
  return 0.0
644
  try:
@@ -886,7 +859,7 @@ class ReachyController:
886
  return _ReSpeakerContext(None, self._respeaker_lock)
887
 
888
  # ========== Phase 11: LED Control (DISABLED - LEDs are inside the robot and not visible) ==========
889
- # According to PROJECT_PLAN.md principle 8: "LED都被隐藏在了机器人内部,所有的LED控制全部都忽略"
890
  # The following LED methods are kept but commented out for reference.
891
  # They are not registered as entities in entity_registry.py.
892
 
 
48
  self.reachy = reachy_mini
49
  self._speaker_volume = 100 # Default volume
50
 
51
+ # Status caching - only for get_status() which may trigger I/O
52
+ # Note: get_current_head_pose() and get_current_joint_positions() are
53
+ # non-blocking in the SDK (they return cached Zenoh data), so no caching needed
54
  self._state_cache: Dict[str, Any] = {}
55
+ self._cache_ttl = 1.0 # 1 second cache TTL for status queries
56
  self._last_status_query = 0.0
 
 
 
 
 
 
 
57
 
58
  # Thread lock for ReSpeaker USB access to prevent conflicts with GStreamer audio pipeline
59
  self._respeaker_lock = __import__('threading').Lock()
 
66
  # ========== Phase 1: Basic Status & Volume ==========
67
 
68
  def _get_cached_status(self) -> Optional[Dict]:
69
+ """Get cached daemon status to reduce query frequency.
70
+
71
+ Note: get_status() may trigger I/O, so we cache it.
72
+ Unlike get_current_head_pose() and get_current_joint_positions()
73
+ which are non-blocking in the SDK.
74
+ """
75
  now = time.time()
76
  if now - self._last_status_query < self._cache_ttl:
77
  return self._state_cache.get('status')
 
79
  if not self.is_available:
80
  return None
81
 
 
 
 
 
 
 
 
82
  try:
83
  status = self.reachy.client.get_status(wait=False)
84
  self._state_cache['status'] = status
 
327
 
328
  # ========== Phase 3: Pose Control ==========
329
 
330
+ def _get_head_pose(self) -> Optional[np.ndarray]:
331
+ """Get current head pose from SDK.
 
 
 
332
 
333
+ Note: SDK's get_current_head_pose() is non-blocking - it returns
334
+ cached data from Zenoh subscriptions, so no throttling needed.
335
+ """
336
  if not self.is_available:
337
  return None
338
 
 
 
 
 
 
 
339
  try:
340
+ return self.reachy.get_current_head_pose()
 
 
 
341
  except Exception as e:
342
  logger.error(f"Error getting head pose: {e}")
343
+ return None
344
 
345
+ def _get_joint_positions(self) -> Optional[tuple]:
346
+ """Get current joint positions from SDK.
 
 
 
347
 
348
+ Note: SDK's get_current_joint_positions() is non-blocking - it returns
349
+ cached data from Zenoh subscriptions, so no throttling needed.
350
+ """
351
  if not self.is_available:
352
  return None
353
 
 
 
 
 
 
 
354
  try:
355
+ return self.reachy.get_current_joint_positions()
 
 
 
356
  except Exception as e:
357
  logger.error(f"Error getting joint positions: {e}")
358
+ return None
359
 
360
  def _extract_pose_from_matrix(self, pose_matrix: np.ndarray) -> tuple:
361
  """
 
382
 
383
  def get_head_x(self) -> float:
384
  """Get head X position in mm with caching."""
385
+ pose = self._get_head_pose()
386
  if pose is None:
387
  return 0.0
388
  try:
 
412
 
413
  def get_head_y(self) -> float:
414
  """Get head Y position in mm with caching."""
415
+ pose = self._get_head_pose()
416
  if pose is None:
417
  return 0.0
418
  try:
 
440
 
441
  def get_head_z(self) -> float:
442
  """Get head Z position in mm with caching."""
443
+ pose = self._get_head_pose()
444
  if pose is None:
445
  return 0.0
446
  try:
 
468
 
469
  def get_head_roll(self) -> float:
470
  """Get head roll angle in degrees with caching."""
471
+ pose = self._get_head_pose()
472
  if pose is None:
473
  return 0.0
474
  try:
 
499
 
500
  def get_head_pitch(self) -> float:
501
  """Get head pitch angle in degrees with caching."""
502
+ pose = self._get_head_pose()
503
  if pose is None:
504
  return 0.0
505
  try:
 
529
 
530
  def get_head_yaw(self) -> float:
531
  """Get head yaw angle in degrees with caching."""
532
+ pose = self._get_head_pose()
533
  if pose is None:
534
  return 0.0
535
  try:
 
559
 
560
  def get_body_yaw(self) -> float:
561
  """Get body yaw angle in degrees with caching."""
562
+ joints = self._get_joint_positions()
563
  if joints is None:
564
  return 0.0
565
  try:
 
584
 
585
  def get_antenna_left(self) -> float:
586
  """Get left antenna angle in degrees with caching."""
587
+ joints = self._get_joint_positions()
588
  if joints is None:
589
  return 0.0
590
  try:
 
611
 
612
  def get_antenna_right(self) -> float:
613
  """Get right antenna angle in degrees with caching."""
614
+ joints = self._get_joint_positions()
615
  if joints is None:
616
  return 0.0
617
  try:
 
859
  return _ReSpeakerContext(None, self._respeaker_lock)
860
 
861
  # ========== Phase 11: LED Control (DISABLED - LEDs are inside the robot and not visible) ==========
862
+ # According to PROJECT_PLAN.md principle 8: "LED都被隐藏在了机器人内部,所有的LED控制全部都忽�?
863
  # The following LED methods are kept but commented out for reference.
864
  # They are not registered as entities in entity_registry.py.
865