Commit Β·
f7f01c8
1
Parent(s): 17348fa
v0.5.8: Fix tap detection during emotion playback - poll daemon API for move completion
Browse files- PROJECT_PLAN.md +6 -2
- index.html +19 -0
- pyproject.toml +1 -1
- reachy_mini_ha_voice/satellite.py +61 -11
PROJECT_PLAN.md
CHANGED
|
@@ -336,9 +336,10 @@ Based on deep analysis of Reachy Mini SDK, the following entities are exposed to
|
|
| 336 |
- [x] `look_at_x/y/z` - Gaze point coordinate control
|
| 337 |
|
| 338 |
5. **Phase 5 - DOA (Direction of Arrival)** β
**Re-added for wakeup turn-to-sound**
|
| 339 |
-
- [x] `doa_angle` - Sound source direction (degrees, -
|
| 340 |
- [x] `speech_detected` - Speech detection status
|
| 341 |
- [x] Turn-to-sound at wakeup (robot turns toward speaker when wake word detected)
|
|
|
|
| 342 |
- Note: DOA only read once at wakeup to avoid daemon pressure; face tracking takes over after
|
| 343 |
|
| 344 |
6. **Phase 6 - Diagnostic Information** (Low Priority) β
**Completed**
|
|
@@ -415,11 +416,13 @@ Based on deep analysis of Reachy Mini SDK, the following entities are exposed to
|
|
| 415 |
- β
Emotion mapping: Happy/Sad/Angry/Fear/Surprise/Disgust
|
| 416 |
- β
Integration with HuggingFace action library (`pollen-robotics/reachy-mini-emotions-library`)
|
| 417 |
- β
SpeechSway system for natural head micro-movements during conversation (non-blocking)
|
|
|
|
| 418 |
|
| 419 |
**Design Decisions**:
|
| 420 |
- π― No auto-play of full emotion actions during conversation to avoid blocking
|
| 421 |
- π― Use voice-driven head sway (SpeechSway) for natural motion feedback
|
| 422 |
- π― Emotion actions retained as manual trigger feature via ESPHome entity
|
|
|
|
| 423 |
|
| 424 |
**Not Implemented**:
|
| 425 |
- β Auto-trigger emotion actions based on voice assistant response (decided not to implement to avoid blocking)
|
|
@@ -429,7 +432,8 @@ Based on deep analysis of Reachy Mini SDK, the following entities are exposed to
|
|
| 429 |
|
| 430 |
**Code Locations**:
|
| 431 |
- `entity_registry.py:633-658` - Emotion Selector entity
|
| 432 |
-
- `satellite.py:
|
|
|
|
| 433 |
- `motion.py:132-156` - Conversation start motion control (uses SpeechSway)
|
| 434 |
- `movement_manager.py:541-595` - Move queue management (allows SpeechSway overlay)
|
| 435 |
|
|
|
|
| 336 |
- [x] `look_at_x/y/z` - Gaze point coordinate control
|
| 337 |
|
| 338 |
5. **Phase 5 - DOA (Direction of Arrival)** β
**Re-added for wakeup turn-to-sound**
|
| 339 |
+
- [x] `doa_angle` - Sound source direction (degrees, 0-180Β°, where 0Β°=left, 90Β°=front, 180Β°=right)
|
| 340 |
- [x] `speech_detected` - Speech detection status
|
| 341 |
- [x] Turn-to-sound at wakeup (robot turns toward speaker when wake word detected)
|
| 342 |
+
- [x] Direction correction: `yaw = Ο/2 - doa` (fixed left/right inversion)
|
| 343 |
- Note: DOA only read once at wakeup to avoid daemon pressure; face tracking takes over after
|
| 344 |
|
| 345 |
6. **Phase 6 - Diagnostic Information** (Low Priority) β
**Completed**
|
|
|
|
| 416 |
- β
Emotion mapping: Happy/Sad/Angry/Fear/Surprise/Disgust
|
| 417 |
- β
Integration with HuggingFace action library (`pollen-robotics/reachy-mini-emotions-library`)
|
| 418 |
- β
SpeechSway system for natural head micro-movements during conversation (non-blocking)
|
| 419 |
+
- β
Tap detection disabled during emotion playback (polls daemon API for completion)
|
| 420 |
|
| 421 |
**Design Decisions**:
|
| 422 |
- π― No auto-play of full emotion actions during conversation to avoid blocking
|
| 423 |
- π― Use voice-driven head sway (SpeechSway) for natural motion feedback
|
| 424 |
- π― Emotion actions retained as manual trigger feature via ESPHome entity
|
| 425 |
+
- π― Tap detection waits for actual move completion via `/api/move/running` polling
|
| 426 |
|
| 427 |
**Not Implemented**:
|
| 428 |
- β Auto-trigger emotion actions based on voice assistant response (decided not to implement to avoid blocking)
|
|
|
|
| 432 |
|
| 433 |
**Code Locations**:
|
| 434 |
- `entity_registry.py:633-658` - Emotion Selector entity
|
| 435 |
+
- `satellite.py:_play_emotion()` - Emotion playback with move UUID tracking
|
| 436 |
+
- `satellite.py:_wait_for_move_completion()` - Polls daemon API for move completion
|
| 437 |
- `motion.py:132-156` - Conversation start motion control (uses SpeechSway)
|
| 438 |
- `movement_manager.py:541-595` - Move queue management (allows SpeechSway overlay)
|
| 439 |
|
index.html
CHANGED
|
@@ -97,6 +97,25 @@
|
|
| 97 |
<h2>Changelog</h2>
|
| 98 |
<div class="how-to-use changelog-container">
|
| 99 |
<div class="changelog-scroll">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
<div class="changelog-entry">
|
| 101 |
<span class="version">v0.5.6</span>
|
| 102 |
<span class="date">2026-01-09</span>
|
|
|
|
| 97 |
<h2>Changelog</h2>
|
| 98 |
<div class="how-to-use changelog-container">
|
| 99 |
<div class="changelog-scroll">
|
| 100 |
+
<div class="changelog-entry">
|
| 101 |
+
<span class="version">v0.5.8</span>
|
| 102 |
+
<span class="date">2026-01-09</span>
|
| 103 |
+
<ul>
|
| 104 |
+
<li>Fix: Tap detection now waits for emotion playback to complete (polls daemon API)</li>
|
| 105 |
+
<li>Fix: Prevents false tap triggers during long emotion animations</li>
|
| 106 |
+
<li>New: DOA turn-to-sound at wakeup (robot turns toward speaker)</li>
|
| 107 |
+
<li>New: DOA angle and speech_detected entities in Home Assistant</li>
|
| 108 |
+
</ul>
|
| 109 |
+
</div>
|
| 110 |
+
<div class="changelog-entry">
|
| 111 |
+
<span class="version">v0.5.7</span>
|
| 112 |
+
<span class="date">2026-01-09</span>
|
| 113 |
+
<ul>
|
| 114 |
+
<li>Fix: DOA direction inversion corrected (left/right now correct)</li>
|
| 115 |
+
<li>Fix: Home Assistant shows raw DOA angle (0-180Β°)</li>
|
| 116 |
+
<li>Fix: EntityRegistry 'state' attribute error in tap_sensitivity</li>
|
| 117 |
+
</ul>
|
| 118 |
+
</div>
|
| 119 |
<div class="changelog-entry">
|
| 120 |
<span class="version">v0.5.6</span>
|
| 121 |
<span class="date">2026-01-09</span>
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "reachy_mini_ha_voice"
|
| 7 |
-
version = "0.5.
|
| 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.5.8"
|
| 8 |
description = "Home Assistant Voice Assistant for Reachy Mini"
|
| 9 |
readme = "README.md"
|
| 10 |
requires-python = ">=3.10"
|
reachy_mini_ha_voice/satellite.py
CHANGED
|
@@ -824,16 +824,6 @@ class VoiceSatelliteProtocol(APIServer):
|
|
| 824 |
if tap_detector:
|
| 825 |
tap_detector.set_enabled(False)
|
| 826 |
_LOGGER.debug("Tap detection disabled during emotion playback")
|
| 827 |
-
|
| 828 |
-
# Re-enable after emotion completes (estimated 3 seconds)
|
| 829 |
-
def reenable_tap():
|
| 830 |
-
import time
|
| 831 |
-
time.sleep(3.0)
|
| 832 |
-
if tap_detector:
|
| 833 |
-
tap_detector.set_enabled(True)
|
| 834 |
-
_LOGGER.debug("Tap detection re-enabled after emotion")
|
| 835 |
-
|
| 836 |
-
threading.Thread(target=reenable_tap, daemon=True).start()
|
| 837 |
|
| 838 |
# Get WLAN IP from daemon status
|
| 839 |
wlan_ip = "localhost"
|
|
@@ -850,9 +840,28 @@ class VoiceSatelliteProtocol(APIServer):
|
|
| 850 |
|
| 851 |
response = requests.post(url, timeout=5)
|
| 852 |
if response.status_code == 200:
|
| 853 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 854 |
else:
|
| 855 |
_LOGGER.warning(f"Failed to play emotion {emotion_name}: HTTP {response.status_code}")
|
|
|
|
|
|
|
|
|
|
| 856 |
|
| 857 |
except Exception as e:
|
| 858 |
_LOGGER.error(f"Error playing emotion {emotion_name}: {e}")
|
|
@@ -860,3 +869,44 @@ class VoiceSatelliteProtocol(APIServer):
|
|
| 860 |
tap_detector = getattr(self.state, 'tap_detector', None)
|
| 861 |
if tap_detector:
|
| 862 |
tap_detector.set_enabled(True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 824 |
if tap_detector:
|
| 825 |
tap_detector.set_enabled(False)
|
| 826 |
_LOGGER.debug("Tap detection disabled during emotion playback")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
|
| 828 |
# Get WLAN IP from daemon status
|
| 829 |
wlan_ip = "localhost"
|
|
|
|
| 840 |
|
| 841 |
response = requests.post(url, timeout=5)
|
| 842 |
if response.status_code == 200:
|
| 843 |
+
result = response.json()
|
| 844 |
+
move_uuid = result.get('uuid')
|
| 845 |
+
_LOGGER.info(f"Playing emotion: {emotion_name} (uuid={move_uuid})")
|
| 846 |
+
|
| 847 |
+
# Wait for move completion in background thread
|
| 848 |
+
if tap_detector and move_uuid:
|
| 849 |
+
def wait_for_completion():
|
| 850 |
+
self._wait_for_move_completion(wlan_ip, move_uuid, tap_detector)
|
| 851 |
+
threading.Thread(target=wait_for_completion, daemon=True).start()
|
| 852 |
+
elif tap_detector:
|
| 853 |
+
# Fallback: re-enable after 5 seconds if no UUID
|
| 854 |
+
def reenable_tap_fallback():
|
| 855 |
+
time.sleep(5.0)
|
| 856 |
+
if tap_detector:
|
| 857 |
+
tap_detector.set_enabled(True)
|
| 858 |
+
_LOGGER.debug("Tap detection re-enabled (fallback timeout)")
|
| 859 |
+
threading.Thread(target=reenable_tap_fallback, daemon=True).start()
|
| 860 |
else:
|
| 861 |
_LOGGER.warning(f"Failed to play emotion {emotion_name}: HTTP {response.status_code}")
|
| 862 |
+
# Re-enable tap detection on failure
|
| 863 |
+
if tap_detector:
|
| 864 |
+
tap_detector.set_enabled(True)
|
| 865 |
|
| 866 |
except Exception as e:
|
| 867 |
_LOGGER.error(f"Error playing emotion {emotion_name}: {e}")
|
|
|
|
| 869 |
tap_detector = getattr(self.state, 'tap_detector', None)
|
| 870 |
if tap_detector:
|
| 871 |
tap_detector.set_enabled(True)
|
| 872 |
+
|
| 873 |
+
def _wait_for_move_completion(self, wlan_ip: str, move_uuid: str, tap_detector) -> None:
|
| 874 |
+
"""Wait for a move to complete by polling the daemon API.
|
| 875 |
+
|
| 876 |
+
Args:
|
| 877 |
+
wlan_ip: IP address of the daemon
|
| 878 |
+
move_uuid: UUID of the move to wait for
|
| 879 |
+
tap_detector: TapDetector instance to re-enable after completion
|
| 880 |
+
"""
|
| 881 |
+
import requests
|
| 882 |
+
|
| 883 |
+
MAX_WAIT_SECONDS = 30.0 # Maximum wait time to prevent infinite loops
|
| 884 |
+
POLL_INTERVAL = 0.3 # Poll every 300ms
|
| 885 |
+
|
| 886 |
+
start_time = time.time()
|
| 887 |
+
running_url = f"http://{wlan_ip}:8000/api/move/running"
|
| 888 |
+
|
| 889 |
+
try:
|
| 890 |
+
while time.time() - start_time < MAX_WAIT_SECONDS:
|
| 891 |
+
try:
|
| 892 |
+
response = requests.get(running_url, timeout=2)
|
| 893 |
+
if response.status_code == 200:
|
| 894 |
+
running_moves = response.json()
|
| 895 |
+
# Check if our move is still in the running list
|
| 896 |
+
running_uuids = [m.get('uuid') for m in running_moves]
|
| 897 |
+
if move_uuid not in running_uuids:
|
| 898 |
+
_LOGGER.debug(f"Move {move_uuid} completed")
|
| 899 |
+
break
|
| 900 |
+
else:
|
| 901 |
+
_LOGGER.debug(f"Failed to get running moves: HTTP {response.status_code}")
|
| 902 |
+
except requests.RequestException as e:
|
| 903 |
+
_LOGGER.debug(f"Error polling move status: {e}")
|
| 904 |
+
|
| 905 |
+
time.sleep(POLL_INTERVAL)
|
| 906 |
+
else:
|
| 907 |
+
_LOGGER.warning(f"Move {move_uuid} did not complete within {MAX_WAIT_SECONDS}s")
|
| 908 |
+
finally:
|
| 909 |
+
# Always re-enable tap detection
|
| 910 |
+
if tap_detector:
|
| 911 |
+
tap_detector.set_enabled(True)
|
| 912 |
+
_LOGGER.debug("Tap detection re-enabled after move completion")
|