Desmond-Dong commited on
Commit
7edbf67
·
1 Parent(s): 29b7fc2

v0.2.21: Fix daemon crash - reduce control loop to 2Hz, pause during audio

Browse files
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "reachy_mini_ha_voice"
7
- version = "0.2.20"
8
  description = "Home Assistant Voice Assistant for Reachy Mini"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
 
4
 
5
  [project]
6
  name = "reachy_mini_ha_voice"
7
+ version = "0.2.21"
8
  description = "Home Assistant Voice Assistant for Reachy Mini"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
reachy_mini_ha_voice/__init__.py CHANGED
@@ -11,7 +11,7 @@ Key features:
11
  - Reachy Mini motion control integration
12
  """
13
 
14
- __version__ = "0.2.20"
15
  __author__ = "Desmond Dong"
16
 
17
  # Don't import main module here to avoid runpy warning
 
11
  - Reachy Mini motion control integration
12
  """
13
 
14
+ __version__ = "0.2.21"
15
  __author__ = "Desmond Dong"
16
 
17
  # Don't import main module here to avoid runpy warning
reachy_mini_ha_voice/movement_manager.py CHANGED
@@ -37,7 +37,7 @@ 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)
@@ -351,8 +351,14 @@ class MovementManager:
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)
@@ -429,6 +435,17 @@ class MovementManager:
429
  )
430
  self._command_queue.put(("action", action))
431
 
 
 
 
 
 
 
 
 
 
 
 
432
  def set_camera_server(self, camera_server) -> None:
433
  """Set the camera server for face tracking offsets.
434
 
@@ -501,6 +518,13 @@ class MovementManager:
501
  amplitude_deg, duration = payload
502
  self._do_shake(amplitude_deg, duration)
503
 
 
 
 
 
 
 
 
504
  def _start_action(self, action: PendingAction) -> None:
505
  """Start a new motion action."""
506
  self._pending_action = action
@@ -730,6 +754,16 @@ class MovementManager:
730
  if self.robot is None:
731
  return
732
 
 
 
 
 
 
 
 
 
 
 
733
  # Check if pose changed significantly (prevent unnecessary commands)
734
  if self._last_sent_pose is not None:
735
  max_diff = max(
@@ -739,8 +773,6 @@ class MovementManager:
739
  if max_diff < self._pose_change_threshold:
740
  # No significant change, skip sending command
741
  return
742
-
743
- now = self._now()
744
 
745
  # Check if we should skip due to connection loss (but always try periodically)
746
  if self._connection_lost:
@@ -774,6 +806,7 @@ class MovementManager:
774
 
775
  # Command succeeded - update connection health and cache
776
  self._last_successful_command = now
 
777
  self._last_sent_pose = pose.copy() # Cache sent pose
778
  self._consecutive_errors = 0 # Reset error counter
779
 
 
37
  # Constants (borrowed from conversation_app)
38
  # =============================================================================
39
 
40
+ CONTROL_LOOP_FREQUENCY_HZ = 2 # 2Hz control loop (reduced from 5Hz to prevent daemon serial port overload during audio)
41
  TARGET_PERIOD = 1.0 / CONTROL_LOOP_FREQUENCY_HZ
42
 
43
  # Speech sway parameters (from conversation_app SwayRollRT)
 
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.02 rad ≈ 1.15 degrees, prevents micro-movements from triggering commands
355
+ self._pose_change_threshold = 0.02
356
+
357
+ # Audio activity pause (prevent serial port overload during wake word/TTS)
358
+ self._audio_active = False
359
+ self._audio_pause_commands = True # Skip commands during audio activity
360
+ self._last_command_time = 0.0
361
+ self._min_command_interval = 0.5 # Minimum 0.5s between commands (2Hz max)
362
 
363
  # Face tracking offsets (from camera worker)
364
  self._face_tracking_offsets: Tuple[float, float, float, float, float, float] = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
 
435
  )
436
  self._command_queue.put(("action", action))
437
 
438
+ def set_audio_active(self, active: bool) -> None:
439
+ """Thread-safe: Set audio activity state to pause/resume commands.
440
+
441
+ When audio is active (wake word detection, TTS playback), movement
442
+ commands are paused to prevent serial port buffer overflow.
443
+
444
+ Args:
445
+ active: True when audio is playing/processing, False otherwise
446
+ """
447
+ self._command_queue.put(("audio_active", active))
448
+
449
  def set_camera_server(self, camera_server) -> None:
450
  """Set the camera server for face tracking offsets.
451
 
 
518
  amplitude_deg, duration = payload
519
  self._do_shake(amplitude_deg, duration)
520
 
521
+ elif cmd == "audio_active":
522
+ self._audio_active = payload
523
+ if payload:
524
+ logger.debug("Audio active - pausing movement commands")
525
+ else:
526
+ logger.debug("Audio inactive - resuming movement commands")
527
+
528
  def _start_action(self, action: PendingAction) -> None:
529
  """Start a new motion action."""
530
  self._pending_action = action
 
754
  if self.robot is None:
755
  return
756
 
757
+ now = self._now()
758
+
759
+ # Skip commands during audio activity to prevent serial port overload
760
+ if self._audio_active and self._audio_pause_commands:
761
+ return
762
+
763
+ # Enforce minimum command interval (additional throttling)
764
+ if now - self._last_command_time < self._min_command_interval:
765
+ return
766
+
767
  # Check if pose changed significantly (prevent unnecessary commands)
768
  if self._last_sent_pose is not None:
769
  max_diff = max(
 
773
  if max_diff < self._pose_change_threshold:
774
  # No significant change, skip sending command
775
  return
 
 
776
 
777
  # Check if we should skip due to connection loss (but always try periodically)
778
  if self._connection_lost:
 
806
 
807
  # Command succeeded - update connection health and cache
808
  self._last_successful_command = now
809
+ self._last_command_time = now # Track for throttling
810
  self._last_sent_pose = pose.copy() # Cache sent pose
811
  self._consecutive_errors = 0 # Reset error counter
812
 
reachy_mini_ha_voice/satellite.py CHANGED
@@ -323,6 +323,10 @@ class VoiceSatelliteProtocol(APIServer):
323
  wake_word_phrase = wake_word.wake_word
324
  _LOGGER.debug("Detected wake word: %s", wake_word_phrase)
325
 
 
 
 
 
326
  self.send_messages(
327
  [VoiceAssistantRequest(start=True, wake_word_phrase=wake_word_phrase)]
328
  )
@@ -340,6 +344,10 @@ class VoiceSatelliteProtocol(APIServer):
340
  else:
341
  _LOGGER.debug("TTS response stopped manually")
342
 
 
 
 
 
343
  self._tts_finished()
344
 
345
  def play_tts(self) -> None:
@@ -371,6 +379,9 @@ class VoiceSatelliteProtocol(APIServer):
371
  else:
372
  self.unduck()
373
  _LOGGER.debug("TTS response finished")
 
 
 
374
  # Reachy Mini: Return to idle
375
  self._reachy_on_idle()
376
 
 
323
  wake_word_phrase = wake_word.wake_word
324
  _LOGGER.debug("Detected wake word: %s", wake_word_phrase)
325
 
326
+ # Pause movement commands during audio activity to prevent serial port overload
327
+ if self.state.motion:
328
+ self.state.motion.set_audio_active(True)
329
+
330
  self.send_messages(
331
  [VoiceAssistantRequest(start=True, wake_word_phrase=wake_word_phrase)]
332
  )
 
344
  else:
345
  _LOGGER.debug("TTS response stopped manually")
346
 
347
+ # Resume movement commands after audio activity
348
+ if self.state.motion:
349
+ self.state.motion.set_audio_active(False)
350
+
351
  self._tts_finished()
352
 
353
  def play_tts(self) -> None:
 
379
  else:
380
  self.unduck()
381
  _LOGGER.debug("TTS response finished")
382
+ # Resume movement commands after audio activity
383
+ if self.state.motion:
384
+ self.state.motion.set_audio_active(False)
385
  # Reachy Mini: Return to idle
386
  self._reachy_on_idle()
387