Commit ·
0c6d129
1
Parent(s): 312c52f
v0.2.7: Add DOA caching to prevent ReSpeaker query overload
Browse files- Add _get_cached_doa() method with 500ms TTL
- Use thread-safe ReSpeaker access with _respeaker_lock
- Prevents daemon crash from excessive DOA queries when HA subscribes to sensors
- pyproject.toml +1 -1
- reachy_mini_ha_voice/__init__.py +1 -1
- reachy_mini_ha_voice/reachy_controller.py +32 -22
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.
|
| 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.7"
|
| 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.
|
| 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.7"
|
| 15 |
__author__ = "Desmond Dong"
|
| 16 |
|
| 17 |
# Don't import main module here to avoid runpy warning
|
reachy_mini_ha_voice/reachy_controller.py
CHANGED
|
@@ -64,6 +64,13 @@ class ReachyController:
|
|
| 64 |
|
| 65 |
# Thread lock for ReSpeaker USB access to prevent conflicts with GStreamer audio pipeline
|
| 66 |
self._respeaker_lock = __import__('threading').Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
@property
|
| 69 |
def is_available(self) -> bool:
|
|
@@ -713,34 +720,37 @@ class ReachyController:
|
|
| 713 |
|
| 714 |
# ========== Phase 5: Audio Sensors ==========
|
| 715 |
|
| 716 |
-
def
|
| 717 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
if not self.is_available:
|
| 719 |
-
return
|
|
|
|
| 720 |
try:
|
| 721 |
-
# Access DOA through media_manager
|
| 722 |
-
|
|
|
|
|
|
|
| 723 |
if doa_result is not None:
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
except Exception as e:
|
| 728 |
-
logger.
|
| 729 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
|
| 731 |
def get_speech_detected(self) -> bool:
|
| 732 |
-
"""Check if speech is detected."""
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
try:
|
| 736 |
-
# Access speech detection through media_manager
|
| 737 |
-
doa_result = self.reachy.media.get_DoA()
|
| 738 |
-
if doa_result is not None:
|
| 739 |
-
return doa_result[1]
|
| 740 |
-
return False
|
| 741 |
-
except Exception as e:
|
| 742 |
-
logger.error(f"Error getting speech detection: {e}")
|
| 743 |
-
return False
|
| 744 |
|
| 745 |
# ========== Phase 6: Diagnostic Information ==========
|
| 746 |
|
|
|
|
| 64 |
|
| 65 |
# Thread lock for ReSpeaker USB access to prevent conflicts with GStreamer audio pipeline
|
| 66 |
self._respeaker_lock = __import__('threading').Lock()
|
| 67 |
+
|
| 68 |
+
# DOA caching to prevent excessive ReSpeaker queries
|
| 69 |
+
# DOA is only meaningful during wake word detection, not for continuous polling
|
| 70 |
+
self._last_doa_query = 0.0
|
| 71 |
+
self._doa_cache_ttl = 0.5 # 500ms cache TTL for DOA
|
| 72 |
+
self._cached_doa_angle = 0.0
|
| 73 |
+
self._cached_speech_detected = False
|
| 74 |
|
| 75 |
@property
|
| 76 |
def is_available(self) -> bool:
|
|
|
|
| 720 |
|
| 721 |
# ========== Phase 5: Audio Sensors ==========
|
| 722 |
|
| 723 |
+
def _get_cached_doa(self) -> None:
|
| 724 |
+
"""Update cached DOA values if cache expired."""
|
| 725 |
+
now = time.time()
|
| 726 |
+
if now - self._last_doa_query < self._doa_cache_ttl:
|
| 727 |
+
return # Use cached values
|
| 728 |
+
|
| 729 |
if not self.is_available:
|
| 730 |
+
return
|
| 731 |
+
|
| 732 |
try:
|
| 733 |
+
# Access DOA through media_manager with thread safety
|
| 734 |
+
with self._respeaker_lock:
|
| 735 |
+
doa_result = self.reachy.media.get_DoA()
|
| 736 |
+
|
| 737 |
if doa_result is not None:
|
| 738 |
+
self._cached_doa_angle = math.degrees(doa_result[0])
|
| 739 |
+
self._cached_speech_detected = doa_result[1]
|
| 740 |
+
self._last_doa_query = now
|
| 741 |
except Exception as e:
|
| 742 |
+
logger.debug(f"Error getting DOA: {e}")
|
| 743 |
+
# Keep using cached values on error
|
| 744 |
+
|
| 745 |
+
def get_doa_angle(self) -> float:
|
| 746 |
+
"""Get direction of arrival angle in degrees (cached)."""
|
| 747 |
+
self._get_cached_doa()
|
| 748 |
+
return self._cached_doa_angle
|
| 749 |
|
| 750 |
def get_speech_detected(self) -> bool:
|
| 751 |
+
"""Check if speech is detected (cached)."""
|
| 752 |
+
self._get_cached_doa()
|
| 753 |
+
return self._cached_speech_detected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
|
| 755 |
# ========== Phase 6: Diagnostic Information ==========
|
| 756 |
|