Commit ·
affc41b
1
Parent(s): 1b8e11b
fix: enable HA control of robot pose and fix continuous conversation
Browse files1. Robot pose control from Home Assistant:
- Added set_target_pose() to MovementManager
- ReachyController now routes pose setters through MovementManager
- Head position, orientation, body yaw, antennas all controllable from HA
2. Fixed continuous conversation (tap-to-wake):
- Fixed _is_streaming_audio not being set correctly for continuation
- Fixed _reachy_on_idle() being called even when continuing
- Added _reachy_on_listening() call when continuing conversation
- Added logging for conversation continuation
3. Extended emotion list:
- Added 50+ emotions from robot's emotion library
- Includes basic emotions, extended emotions, actions, and special animations
reachy_mini_ha_voice/entity_registry.py
CHANGED
|
@@ -124,22 +124,67 @@ class EntityRegistry:
|
|
| 124 |
# Emotion state
|
| 125 |
self._current_emotion = "None"
|
| 126 |
# Map emotion names to available robot emotions
|
| 127 |
-
#
|
| 128 |
-
# impatient1/2, enthusiastic1/2, cheerful1, laughing1/2, irritated1/2, oops1/2, curious1,
|
| 129 |
-
# electric1, contempt1, inquiring1/2/3, attentive1/2, frustrated1, dance1/2/3, no1, sad1/2,
|
| 130 |
-
# understanding1/2, come1, calming1, exhausted1, scared1, downcast1, success1/2, disgusted1,
|
| 131 |
-
# amazed1, displeased1/2, dying1, no_excited1, thoughtful1/2, lonely1, welcoming1/2,
|
| 132 |
-
# no_sad1, reprimand1/2/3, boredom1/2, grateful1, uncertain1, furious1, anxiety1, yes_sad1,
|
| 133 |
-
# proud1/2/3, shy1, indifferent1, tired1, serenity1, helpful1/2, incomprehensible2, relief1/2,
|
| 134 |
-
# confused1, sleep1, yes1, uncomfortable1, lost1
|
| 135 |
self._emotion_map = {
|
| 136 |
"None": None,
|
|
|
|
| 137 |
"Happy": "cheerful1",
|
| 138 |
"Sad": "sad1",
|
| 139 |
"Angry": "rage1",
|
| 140 |
"Fear": "fear1",
|
| 141 |
"Surprise": "surprised1",
|
| 142 |
"Disgust": "disgusted1",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
def setup_all_entities(self, entities: List) -> None:
|
|
|
|
| 124 |
# Emotion state
|
| 125 |
self._current_emotion = "None"
|
| 126 |
# Map emotion names to available robot emotions
|
| 127 |
+
# Full list of available emotions from robot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
self._emotion_map = {
|
| 129 |
"None": None,
|
| 130 |
+
# Basic emotions
|
| 131 |
"Happy": "cheerful1",
|
| 132 |
"Sad": "sad1",
|
| 133 |
"Angry": "rage1",
|
| 134 |
"Fear": "fear1",
|
| 135 |
"Surprise": "surprised1",
|
| 136 |
"Disgust": "disgusted1",
|
| 137 |
+
# Extended emotions
|
| 138 |
+
"Laughing": "laughing1",
|
| 139 |
+
"Loving": "loving1",
|
| 140 |
+
"Proud": "proud1",
|
| 141 |
+
"Grateful": "grateful1",
|
| 142 |
+
"Enthusiastic": "enthusiastic1",
|
| 143 |
+
"Curious": "curious1",
|
| 144 |
+
"Amazed": "amazed1",
|
| 145 |
+
"Shy": "shy1",
|
| 146 |
+
"Confused": "confused1",
|
| 147 |
+
"Thoughtful": "thoughtful1",
|
| 148 |
+
"Anxious": "anxiety1",
|
| 149 |
+
"Scared": "scared1",
|
| 150 |
+
"Frustrated": "frustrated1",
|
| 151 |
+
"Irritated": "irritated1",
|
| 152 |
+
"Furious": "furious1",
|
| 153 |
+
"Contempt": "contempt1",
|
| 154 |
+
"Bored": "boredom1",
|
| 155 |
+
"Tired": "tired1",
|
| 156 |
+
"Exhausted": "exhausted1",
|
| 157 |
+
"Lonely": "lonely1",
|
| 158 |
+
"Downcast": "downcast1",
|
| 159 |
+
"Resigned": "resigned1",
|
| 160 |
+
"Uncertain": "uncertain1",
|
| 161 |
+
"Uncomfortable": "uncomfortable1",
|
| 162 |
+
"Lost": "lost1",
|
| 163 |
+
"Indifferent": "indifferent1",
|
| 164 |
+
# Positive actions
|
| 165 |
+
"Yes": "yes1",
|
| 166 |
+
"No": "no1",
|
| 167 |
+
"Welcoming": "welcoming1",
|
| 168 |
+
"Helpful": "helpful1",
|
| 169 |
+
"Attentive": "attentive1",
|
| 170 |
+
"Understanding": "understanding1",
|
| 171 |
+
"Calming": "calming1",
|
| 172 |
+
"Relief": "relief1",
|
| 173 |
+
"Success": "success1",
|
| 174 |
+
"Serenity": "serenity1",
|
| 175 |
+
# Negative actions
|
| 176 |
+
"Oops": "oops1",
|
| 177 |
+
"Displeased": "displeased1",
|
| 178 |
+
"Impatient": "impatient1",
|
| 179 |
+
"Reprimand": "reprimand1",
|
| 180 |
+
"GoAway": "go_away1",
|
| 181 |
+
# Special
|
| 182 |
+
"Come": "come1",
|
| 183 |
+
"Inquiring": "inquiring1",
|
| 184 |
+
"Sleep": "sleep1",
|
| 185 |
+
"Dance": "dance1",
|
| 186 |
+
"Electric": "electric1",
|
| 187 |
+
"Dying": "dying1",
|
| 188 |
}
|
| 189 |
|
| 190 |
def setup_all_entities(self, entities: List) -> None:
|
reachy_mini_ha_voice/movement_manager.py
CHANGED
|
@@ -479,6 +479,41 @@ class MovementManager:
|
|
| 479 |
with self._face_tracking_lock:
|
| 480 |
self._face_tracking_offsets = offsets
|
| 481 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
# =========================================================================
|
| 483 |
# Internal: Command processing (runs in control loop)
|
| 484 |
# =========================================================================
|
|
@@ -534,6 +569,28 @@ class MovementManager:
|
|
| 534 |
amplitude_deg, duration = payload
|
| 535 |
self._do_shake(amplitude_deg, duration)
|
| 536 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
def _start_action(self, action: PendingAction) -> None:
|
| 538 |
"""Start a new motion action."""
|
| 539 |
self._pending_action = action
|
|
|
|
| 479 |
with self._face_tracking_lock:
|
| 480 |
self._face_tracking_offsets = offsets
|
| 481 |
|
| 482 |
+
def set_target_pose(
|
| 483 |
+
self,
|
| 484 |
+
x: Optional[float] = None,
|
| 485 |
+
y: Optional[float] = None,
|
| 486 |
+
z: Optional[float] = None,
|
| 487 |
+
roll: Optional[float] = None,
|
| 488 |
+
pitch: Optional[float] = None,
|
| 489 |
+
yaw: Optional[float] = None,
|
| 490 |
+
body_yaw: Optional[float] = None,
|
| 491 |
+
antenna_left: Optional[float] = None,
|
| 492 |
+
antenna_right: Optional[float] = None,
|
| 493 |
+
) -> None:
|
| 494 |
+
"""Thread-safe: Set target pose components.
|
| 495 |
+
|
| 496 |
+
Only provided values will be updated. Values are in meters for position
|
| 497 |
+
and radians for angles.
|
| 498 |
+
|
| 499 |
+
Args:
|
| 500 |
+
x, y, z: Head position in meters
|
| 501 |
+
roll, pitch, yaw: Head orientation in radians
|
| 502 |
+
body_yaw: Body yaw in radians
|
| 503 |
+
antenna_left, antenna_right: Antenna angles in radians
|
| 504 |
+
"""
|
| 505 |
+
self._command_queue.put(("set_pose", {
|
| 506 |
+
"x": x,
|
| 507 |
+
"y": y,
|
| 508 |
+
"z": z,
|
| 509 |
+
"roll": roll,
|
| 510 |
+
"pitch": pitch,
|
| 511 |
+
"yaw": yaw,
|
| 512 |
+
"body_yaw": body_yaw,
|
| 513 |
+
"antenna_left": antenna_left,
|
| 514 |
+
"antenna_right": antenna_right,
|
| 515 |
+
}))
|
| 516 |
+
|
| 517 |
# =========================================================================
|
| 518 |
# Internal: Command processing (runs in control loop)
|
| 519 |
# =========================================================================
|
|
|
|
| 569 |
amplitude_deg, duration = payload
|
| 570 |
self._do_shake(amplitude_deg, duration)
|
| 571 |
|
| 572 |
+
elif cmd == "set_pose":
|
| 573 |
+
# Update target pose from external control (e.g., Home Assistant)
|
| 574 |
+
if payload.get("x") is not None:
|
| 575 |
+
self.state.target_x = payload["x"]
|
| 576 |
+
if payload.get("y") is not None:
|
| 577 |
+
self.state.target_y = payload["y"]
|
| 578 |
+
if payload.get("z") is not None:
|
| 579 |
+
self.state.target_z = payload["z"]
|
| 580 |
+
if payload.get("roll") is not None:
|
| 581 |
+
self.state.target_roll = payload["roll"]
|
| 582 |
+
if payload.get("pitch") is not None:
|
| 583 |
+
self.state.target_pitch = payload["pitch"]
|
| 584 |
+
if payload.get("yaw") is not None:
|
| 585 |
+
self.state.target_yaw = payload["yaw"]
|
| 586 |
+
if payload.get("body_yaw") is not None:
|
| 587 |
+
self.state.target_body_yaw = payload["body_yaw"]
|
| 588 |
+
if payload.get("antenna_left") is not None:
|
| 589 |
+
self.state.target_antenna_left = payload["antenna_left"]
|
| 590 |
+
if payload.get("antenna_right") is not None:
|
| 591 |
+
self.state.target_antenna_right = payload["antenna_right"]
|
| 592 |
+
logger.debug("External pose update: %s", payload)
|
| 593 |
+
|
| 594 |
def _start_action(self, action: PendingAction) -> None:
|
| 595 |
"""Start a new motion action."""
|
| 596 |
self._pending_action = action
|
reachy_mini_ha_voice/reachy_controller.py
CHANGED
|
@@ -47,6 +47,7 @@ class ReachyController:
|
|
| 47 |
"""
|
| 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
|
|
@@ -58,6 +59,15 @@ class ReachyController:
|
|
| 58 |
# Thread lock for ReSpeaker USB access to prevent conflicts with GStreamer audio pipeline
|
| 59 |
self._respeaker_lock = __import__('threading').Lock()
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
@property
|
| 62 |
def is_available(self) -> bool:
|
| 63 |
"""Check if robot is available."""
|
|
@@ -408,58 +418,74 @@ class ReachyController:
|
|
| 408 |
return 0.0
|
| 409 |
|
| 410 |
def _disabled_pose_setter(self, name: str) -> None:
|
| 411 |
-
"""Log warning
|
| 412 |
-
logger.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
-
# Head position getters
|
| 415 |
def get_head_x(self) -> float:
|
| 416 |
"""Get head X position in mm."""
|
| 417 |
return self._get_head_pose_component('x')
|
| 418 |
|
| 419 |
def set_head_x(self, x_mm: float) -> None:
|
| 420 |
-
"""
|
| 421 |
-
self.
|
|
|
|
| 422 |
|
| 423 |
def get_head_y(self) -> float:
|
| 424 |
"""Get head Y position in mm."""
|
| 425 |
return self._get_head_pose_component('y')
|
| 426 |
|
| 427 |
def set_head_y(self, y_mm: float) -> None:
|
| 428 |
-
"""
|
| 429 |
-
self.
|
|
|
|
| 430 |
|
| 431 |
def get_head_z(self) -> float:
|
| 432 |
"""Get head Z position in mm."""
|
| 433 |
return self._get_head_pose_component('z')
|
| 434 |
|
| 435 |
def set_head_z(self, z_mm: float) -> None:
|
| 436 |
-
"""
|
| 437 |
-
self.
|
|
|
|
| 438 |
|
| 439 |
-
# Head orientation getters
|
| 440 |
def get_head_roll(self) -> float:
|
| 441 |
"""Get head roll angle in degrees."""
|
| 442 |
return self._get_head_pose_component('roll')
|
| 443 |
|
| 444 |
def set_head_roll(self, roll_deg: float) -> None:
|
| 445 |
-
"""
|
| 446 |
-
self.
|
|
|
|
| 447 |
|
| 448 |
def get_head_pitch(self) -> float:
|
| 449 |
"""Get head pitch angle in degrees."""
|
| 450 |
return self._get_head_pose_component('pitch')
|
| 451 |
|
| 452 |
def set_head_pitch(self, pitch_deg: float) -> None:
|
| 453 |
-
"""
|
| 454 |
-
self.
|
|
|
|
| 455 |
|
| 456 |
def get_head_yaw(self) -> float:
|
| 457 |
"""Get head yaw angle in degrees."""
|
| 458 |
return self._get_head_pose_component('yaw')
|
| 459 |
|
| 460 |
def set_head_yaw(self, yaw_deg: float) -> None:
|
| 461 |
-
"""
|
| 462 |
-
self.
|
|
|
|
| 463 |
|
| 464 |
def get_body_yaw(self) -> float:
|
| 465 |
"""Get body yaw angle in degrees."""
|
|
@@ -474,8 +500,9 @@ class ReachyController:
|
|
| 474 |
return 0.0
|
| 475 |
|
| 476 |
def set_body_yaw(self, yaw_deg: float) -> None:
|
| 477 |
-
"""
|
| 478 |
-
self.
|
|
|
|
| 479 |
|
| 480 |
def get_antenna_left(self) -> float:
|
| 481 |
"""Get left antenna angle in degrees."""
|
|
@@ -490,8 +517,9 @@ class ReachyController:
|
|
| 490 |
return 0.0
|
| 491 |
|
| 492 |
def set_antenna_left(self, angle_deg: float) -> None:
|
| 493 |
-
"""
|
| 494 |
-
self.
|
|
|
|
| 495 |
|
| 496 |
def get_antenna_right(self) -> float:
|
| 497 |
"""Get right antenna angle in degrees."""
|
|
@@ -506,8 +534,9 @@ class ReachyController:
|
|
| 506 |
return 0.0
|
| 507 |
|
| 508 |
def set_antenna_right(self, angle_deg: float) -> None:
|
| 509 |
-
"""
|
| 510 |
-
self.
|
|
|
|
| 511 |
|
| 512 |
# ========== Phase 4: Look At Control ==========
|
| 513 |
|
|
|
|
| 47 |
"""
|
| 48 |
self.reachy = reachy_mini
|
| 49 |
self._speaker_volume = 100 # Default volume
|
| 50 |
+
self._movement_manager = None # Set later via set_movement_manager()
|
| 51 |
|
| 52 |
# Status caching - only for get_status() which may trigger I/O
|
| 53 |
# Note: get_current_head_pose() and get_current_joint_positions() are
|
|
|
|
| 59 |
# Thread lock for ReSpeaker USB access to prevent conflicts with GStreamer audio pipeline
|
| 60 |
self._respeaker_lock = __import__('threading').Lock()
|
| 61 |
|
| 62 |
+
def set_movement_manager(self, movement_manager) -> None:
|
| 63 |
+
"""Set the MovementManager instance for pose control.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
movement_manager: MovementManager instance
|
| 67 |
+
"""
|
| 68 |
+
self._movement_manager = movement_manager
|
| 69 |
+
logger.info("MovementManager set for ReachyController")
|
| 70 |
+
|
| 71 |
@property
|
| 72 |
def is_available(self) -> bool:
|
| 73 |
"""Check if robot is available."""
|
|
|
|
| 418 |
return 0.0
|
| 419 |
|
| 420 |
def _disabled_pose_setter(self, name: str) -> None:
|
| 421 |
+
"""Log warning when MovementManager is not available."""
|
| 422 |
+
logger.warning(f"set_{name} failed - MovementManager not set")
|
| 423 |
+
|
| 424 |
+
def _set_pose_via_manager(self, **kwargs) -> bool:
|
| 425 |
+
"""Set pose via MovementManager if available.
|
| 426 |
+
|
| 427 |
+
Returns True if successful, False if MovementManager not available.
|
| 428 |
+
"""
|
| 429 |
+
if self._movement_manager is None:
|
| 430 |
+
return False
|
| 431 |
+
self._movement_manager.set_target_pose(**kwargs)
|
| 432 |
+
return True
|
| 433 |
|
| 434 |
+
# Head position getters and setters
|
| 435 |
def get_head_x(self) -> float:
|
| 436 |
"""Get head X position in mm."""
|
| 437 |
return self._get_head_pose_component('x')
|
| 438 |
|
| 439 |
def set_head_x(self, x_mm: float) -> None:
|
| 440 |
+
"""Set head X position in mm via MovementManager."""
|
| 441 |
+
if not self._set_pose_via_manager(x=x_mm / 1000.0): # mm to m
|
| 442 |
+
self._disabled_pose_setter('head_x')
|
| 443 |
|
| 444 |
def get_head_y(self) -> float:
|
| 445 |
"""Get head Y position in mm."""
|
| 446 |
return self._get_head_pose_component('y')
|
| 447 |
|
| 448 |
def set_head_y(self, y_mm: float) -> None:
|
| 449 |
+
"""Set head Y position in mm via MovementManager."""
|
| 450 |
+
if not self._set_pose_via_manager(y=y_mm / 1000.0): # mm to m
|
| 451 |
+
self._disabled_pose_setter('head_y')
|
| 452 |
|
| 453 |
def get_head_z(self) -> float:
|
| 454 |
"""Get head Z position in mm."""
|
| 455 |
return self._get_head_pose_component('z')
|
| 456 |
|
| 457 |
def set_head_z(self, z_mm: float) -> None:
|
| 458 |
+
"""Set head Z position in mm via MovementManager."""
|
| 459 |
+
if not self._set_pose_via_manager(z=z_mm / 1000.0): # mm to m
|
| 460 |
+
self._disabled_pose_setter('head_z')
|
| 461 |
|
| 462 |
+
# Head orientation getters and setters
|
| 463 |
def get_head_roll(self) -> float:
|
| 464 |
"""Get head roll angle in degrees."""
|
| 465 |
return self._get_head_pose_component('roll')
|
| 466 |
|
| 467 |
def set_head_roll(self, roll_deg: float) -> None:
|
| 468 |
+
"""Set head roll angle in degrees via MovementManager."""
|
| 469 |
+
if not self._set_pose_via_manager(roll=math.radians(roll_deg)):
|
| 470 |
+
self._disabled_pose_setter('head_roll')
|
| 471 |
|
| 472 |
def get_head_pitch(self) -> float:
|
| 473 |
"""Get head pitch angle in degrees."""
|
| 474 |
return self._get_head_pose_component('pitch')
|
| 475 |
|
| 476 |
def set_head_pitch(self, pitch_deg: float) -> None:
|
| 477 |
+
"""Set head pitch angle in degrees via MovementManager."""
|
| 478 |
+
if not self._set_pose_via_manager(pitch=math.radians(pitch_deg)):
|
| 479 |
+
self._disabled_pose_setter('head_pitch')
|
| 480 |
|
| 481 |
def get_head_yaw(self) -> float:
|
| 482 |
"""Get head yaw angle in degrees."""
|
| 483 |
return self._get_head_pose_component('yaw')
|
| 484 |
|
| 485 |
def set_head_yaw(self, yaw_deg: float) -> None:
|
| 486 |
+
"""Set head yaw angle in degrees via MovementManager."""
|
| 487 |
+
if not self._set_pose_via_manager(yaw=math.radians(yaw_deg)):
|
| 488 |
+
self._disabled_pose_setter('head_yaw')
|
| 489 |
|
| 490 |
def get_body_yaw(self) -> float:
|
| 491 |
"""Get body yaw angle in degrees."""
|
|
|
|
| 500 |
return 0.0
|
| 501 |
|
| 502 |
def set_body_yaw(self, yaw_deg: float) -> None:
|
| 503 |
+
"""Set body yaw angle in degrees via MovementManager."""
|
| 504 |
+
if not self._set_pose_via_manager(body_yaw=math.radians(yaw_deg)):
|
| 505 |
+
self._disabled_pose_setter('body_yaw')
|
| 506 |
|
| 507 |
def get_antenna_left(self) -> float:
|
| 508 |
"""Get left antenna angle in degrees."""
|
|
|
|
| 517 |
return 0.0
|
| 518 |
|
| 519 |
def set_antenna_left(self, angle_deg: float) -> None:
|
| 520 |
+
"""Set left antenna angle in degrees via MovementManager."""
|
| 521 |
+
if not self._set_pose_via_manager(antenna_left=math.radians(angle_deg)):
|
| 522 |
+
self._disabled_pose_setter('antenna_left')
|
| 523 |
|
| 524 |
def get_antenna_right(self) -> float:
|
| 525 |
"""Get right antenna angle in degrees."""
|
|
|
|
| 534 |
return 0.0
|
| 535 |
|
| 536 |
def set_antenna_right(self, angle_deg: float) -> None:
|
| 537 |
+
"""Set right antenna angle in degrees via MovementManager."""
|
| 538 |
+
if not self._set_pose_via_manager(antenna_right=math.radians(angle_deg)):
|
| 539 |
+
self._disabled_pose_setter('antenna_right')
|
| 540 |
|
| 541 |
# ========== Phase 4: Look At Control ==========
|
| 542 |
|
reachy_mini_ha_voice/satellite.py
CHANGED
|
@@ -81,6 +81,10 @@ class VoiceSatelliteProtocol(APIServer):
|
|
| 81 |
|
| 82 |
# Initialize Reachy controller
|
| 83 |
self.reachy_controller = ReachyController(state.reachy_mini)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
# Initialize entity registry (tap_detector from state if available)
|
| 86 |
tap_detector = getattr(state, 'tap_detector', None)
|
|
@@ -156,12 +160,12 @@ class VoiceSatelliteProtocol(APIServer):
|
|
| 156 |
self.play_tts()
|
| 157 |
|
| 158 |
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
|
| 159 |
-
|
|
|
|
| 160 |
if not self._tts_played:
|
| 161 |
self._tts_finished()
|
| 162 |
self._tts_played = False
|
| 163 |
-
#
|
| 164 |
-
self._reachy_on_idle()
|
| 165 |
|
| 166 |
def handle_timer_event(
|
| 167 |
self,
|
|
@@ -420,16 +424,19 @@ class VoiceSatelliteProtocol(APIServer):
|
|
| 420 |
should_continue = self._continue_conversation or self._tap_conversation_mode
|
| 421 |
|
| 422 |
if should_continue:
|
|
|
|
|
|
|
| 423 |
self.send_messages([VoiceAssistantRequest(start=True)])
|
| 424 |
self._is_streaming_audio = True
|
| 425 |
|
| 426 |
if self._tap_conversation_mode:
|
| 427 |
-
_LOGGER.debug("Continuing tap conversation mode")
|
| 428 |
# Provide feedback for continuous conversation mode
|
| 429 |
self._tap_continue_feedback()
|
| 430 |
-
|
| 431 |
-
|
|
|
|
| 432 |
else:
|
|
|
|
| 433 |
self.unduck()
|
| 434 |
_LOGGER.debug("TTS response finished")
|
| 435 |
# Reachy Mini: Return to idle
|
|
|
|
| 81 |
|
| 82 |
# Initialize Reachy controller
|
| 83 |
self.reachy_controller = ReachyController(state.reachy_mini)
|
| 84 |
+
|
| 85 |
+
# Connect MovementManager to ReachyController for pose control from HA
|
| 86 |
+
if state.motion is not None and state.motion.movement_manager is not None:
|
| 87 |
+
self.reachy_controller.set_movement_manager(state.motion.movement_manager)
|
| 88 |
|
| 89 |
# Initialize entity registry (tap_detector from state if available)
|
| 90 |
tap_detector = getattr(state, 'tap_detector', None)
|
|
|
|
| 160 |
self.play_tts()
|
| 161 |
|
| 162 |
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
|
| 163 |
+
# Note: Don't set _is_streaming_audio = False here
|
| 164 |
+
# _tts_finished() will handle it based on whether we continue conversation
|
| 165 |
if not self._tts_played:
|
| 166 |
self._tts_finished()
|
| 167 |
self._tts_played = False
|
| 168 |
+
# Note: _reachy_on_idle() is called inside _tts_finished() if not continuing
|
|
|
|
| 169 |
|
| 170 |
def handle_timer_event(
|
| 171 |
self,
|
|
|
|
| 424 |
should_continue = self._continue_conversation or self._tap_conversation_mode
|
| 425 |
|
| 426 |
if should_continue:
|
| 427 |
+
_LOGGER.info("Continuing conversation (tap_mode=%s, ha_continue=%s)",
|
| 428 |
+
self._tap_conversation_mode, self._continue_conversation)
|
| 429 |
self.send_messages([VoiceAssistantRequest(start=True)])
|
| 430 |
self._is_streaming_audio = True
|
| 431 |
|
| 432 |
if self._tap_conversation_mode:
|
|
|
|
| 433 |
# Provide feedback for continuous conversation mode
|
| 434 |
self._tap_continue_feedback()
|
| 435 |
+
|
| 436 |
+
# Stay in listening mode, don't go to idle
|
| 437 |
+
self._reachy_on_listening()
|
| 438 |
else:
|
| 439 |
+
self._is_streaming_audio = False
|
| 440 |
self.unduck()
|
| 441 |
_LOGGER.debug("TTS response finished")
|
| 442 |
# Reachy Mini: Return to idle
|