Desmond-Dong commited on
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 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, -90 to 90)
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:544-574` - `_play_emotion()` method
 
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.6"
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
- _LOGGER.info(f"Playing emotion: {emotion_name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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")