Desmond-Dong commited on
Commit
1e8f606
·
1 Parent(s): c99f503

"feat-add-emotion-buttons-and-microphone-volume-control"

Browse files
reachy_mini_ha_voice/reachy_controller.py CHANGED
@@ -119,6 +119,52 @@ class ReachyController:
119
  except Exception as e:
120
  logger.error(f"Error setting speaker volume: {e}")
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  # ========== Phase 2: Motor Control ==========
123
 
124
  def get_motors_enabled(self) -> bool:
 
119
  except Exception as e:
120
  logger.error(f"Error setting speaker volume: {e}")
121
 
122
+ def get_microphone_volume(self) -> float:
123
+ """Get microphone volume (0-100) using local SDK audio interface."""
124
+ if not self.is_available:
125
+ return 50.0 # Default if not available
126
+
127
+ try:
128
+ # Use Reachy Mini's local audio interface (like get_DoA)
129
+ audio = self.reachy.media.audio
130
+ if audio is not None and audio._respeaker is not None:
131
+ # AUDIO_MGR_MIC_GAIN returns gain in dB (typically 0-15 dB)
132
+ gain_db = audio._respeaker.read("AUDIO_MGR_MIC_GAIN")
133
+ if gain_db is not None:
134
+ # Convert dB gain to 0-100 percentage
135
+ # Assuming 0 dB = 0%, 15 dB = 100%
136
+ volume = min(100.0, max(0.0, (gain_db[0] / 15.0) * 100.0))
137
+ logger.debug(f"Microphone gain: {gain_db[0]:.2f} dB -> {volume:.0f}%")
138
+ return volume
139
+ except Exception as e:
140
+ logger.debug(f"Could not get microphone volume from SDK: {e}")
141
+
142
+ return 50.0 # Default fallback
143
+
144
+ def set_microphone_volume(self, volume: float) -> None:
145
+ """
146
+ Set microphone volume (0-100) using local SDK audio interface.
147
+
148
+ Args:
149
+ volume: Volume level 0-100
150
+ """
151
+ volume = max(0.0, min(100.0, volume))
152
+
153
+ if self.is_available:
154
+ try:
155
+ # Use Reachy Mini's local audio interface (like get_DoA)
156
+ audio = self.reachy.media.audio
157
+ if audio is not None and audio._respeaker is not None:
158
+ # Convert 0-100% to dB gain (0-15 dB range)
159
+ gain_db = (volume / 100.0) * 15.0
160
+ audio._respeaker.write("AUDIO_MGR_MIC_GAIN", [gain_db])
161
+ logger.info(f"Microphone volume set to {volume}% (gain: {gain_db:.2f} dB)")
162
+ return
163
+ except Exception as e:
164
+ logger.error(f"Failed to set microphone volume: {e}")
165
+ else:
166
+ logger.warning("Cannot set microphone volume: robot not available")
167
+
168
  # ========== Phase 2: Motor Control ==========
169
 
170
  def get_motors_enabled(self) -> bool:
reachy_mini_ha_voice/satellite.py CHANGED
@@ -88,6 +88,8 @@ class VoiceSatelliteProtocol(APIServer):
88
  self._setup_phase5_entities()
89
  self._setup_phase6_entities()
90
  self._setup_phase7_entities()
 
 
91
 
92
  self._is_streaming_audio = False
93
  self._tts_url: Optional[str] = None
@@ -518,6 +520,37 @@ class VoiceSatelliteProtocol(APIServer):
518
  except Exception as e:
519
  _LOGGER.error("Reachy Mini motion error: %s", e)
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  # -------------------------------------------------------------------------
522
  # Entity Setup Methods
523
  # -------------------------------------------------------------------------
@@ -1047,3 +1080,102 @@ class VoiceSatelliteProtocol(APIServer):
1047
  self.state.entities.append(imu_temperature)
1048
 
1049
  _LOGGER.info("Phase 7 entities registered: IMU accelerometer, gyroscope, temperature")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  self._setup_phase5_entities()
89
  self._setup_phase6_entities()
90
  self._setup_phase7_entities()
91
+ self._setup_phase8_entities()
92
+ self._setup_phase9_entities()
93
 
94
  self._is_streaming_audio = False
95
  self._tts_url: Optional[str] = None
 
520
  except Exception as e:
521
  _LOGGER.error("Reachy Mini motion error: %s", e)
522
 
523
+ def _play_emotion(self, emotion_name: str) -> None:
524
+ """Play an emotion/expression from the emotions library.
525
+
526
+ Args:
527
+ emotion_name: Name of the emotion (e.g., "happy1", "sad1", etc.)
528
+ """
529
+ try:
530
+ import requests
531
+
532
+ # Get WLAN IP from daemon status
533
+ wlan_ip = "localhost"
534
+ if self.state.reachy_mini is not None:
535
+ try:
536
+ status = self.state.reachy_mini.client.get_status(wait=False)
537
+ wlan_ip = status.get('wlan_ip', 'localhost')
538
+ except Exception:
539
+ wlan_ip = "localhost"
540
+
541
+ # Call the emotion playback API
542
+ # Dataset: pollen-robotics/reachy-mini-emotions-library
543
+ url = f"http://{wlan_ip}:8000/api/move/play/recorded-move-dataset/pollen-robotics/reachy-mini-emotions-library/{emotion_name}"
544
+
545
+ response = requests.post(url, timeout=5)
546
+ if response.status_code == 200:
547
+ _LOGGER.info(f"Playing emotion: {emotion_name}")
548
+ else:
549
+ _LOGGER.warning(f"Failed to play emotion {emotion_name}: HTTP {response.status_code}")
550
+
551
+ except Exception as e:
552
+ _LOGGER.error(f"Error playing emotion {emotion_name}: {e}")
553
+
554
  # -------------------------------------------------------------------------
555
  # Entity Setup Methods
556
  # -------------------------------------------------------------------------
 
1080
  self.state.entities.append(imu_temperature)
1081
 
1082
  _LOGGER.info("Phase 7 entities registered: IMU accelerometer, gyroscope, temperature")
1083
+
1084
+ def _setup_phase8_entities(self) -> None:
1085
+ """Setup Phase 8 entities: Emotion and expression buttons."""
1086
+
1087
+ # Happy emotion
1088
+ happy_button = ButtonEntity(
1089
+ server=self,
1090
+ key=len(self.state.entities),
1091
+ name="Happy",
1092
+ object_id="emotion_happy",
1093
+ icon="mdi:emoticon-happy",
1094
+ device_class="restart",
1095
+ on_press=lambda: self._play_emotion("happy1"),
1096
+ )
1097
+ self.state.entities.append(happy_button)
1098
+
1099
+ # Sad emotion
1100
+ sad_button = ButtonEntity(
1101
+ server=self,
1102
+ key=len(self.state.entities),
1103
+ name="Sad",
1104
+ object_id="emotion_sad",
1105
+ icon="mdi:emoticon-sad",
1106
+ device_class="restart",
1107
+ on_press=lambda: self._play_emotion("sad1"),
1108
+ )
1109
+ self.state.entities.append(sad_button)
1110
+
1111
+ # Angry emotion
1112
+ angry_button = ButtonEntity(
1113
+ server=self,
1114
+ key=len(self.state.entities),
1115
+ name="Angry",
1116
+ object_id="emotion_angry",
1117
+ icon="mdi:emoticon-angry",
1118
+ device_class="restart",
1119
+ on_press=lambda: self._play_emotion("angry1"),
1120
+ )
1121
+ self.state.entities.append(angry_button)
1122
+
1123
+ # Fear emotion
1124
+ fear_button = ButtonEntity(
1125
+ server=self,
1126
+ key=len(self.state.entities),
1127
+ name="Fear",
1128
+ object_id="emotion_fear",
1129
+ icon="mdi:emoticon-frown",
1130
+ device_class="restart",
1131
+ on_press=lambda: self._play_emotion("fear1"),
1132
+ )
1133
+ self.state.entities.append(fear_button)
1134
+
1135
+ # Surprise emotion
1136
+ surprise_button = ButtonEntity(
1137
+ server=self,
1138
+ key=len(self.state.entities),
1139
+ name="Surprise",
1140
+ object_id="emotion_surprise",
1141
+ icon="mdi:emoticon-surprised",
1142
+ device_class="restart",
1143
+ on_press=lambda: self._play_emotion("surprise1"),
1144
+ )
1145
+ self.state.entities.append(surprise_button)
1146
+
1147
+ # Disgust emotion
1148
+ disgust_button = ButtonEntity(
1149
+ server=self,
1150
+ key=len(self.state.entities),
1151
+ name="Disgust",
1152
+ object_id="emotion_disgust",
1153
+ icon="mdi:emoticon-poop",
1154
+ device_class="restart",
1155
+ on_press=lambda: self._play_emotion("disgust1"),
1156
+ )
1157
+ self.state.entities.append(disgust_button)
1158
+
1159
+ _LOGGER.info("Phase 8 entities registered: emotions (happy, sad, angry, fear, surprise, disgust)")
1160
+
1161
+ def _setup_phase9_entities(self) -> None:
1162
+ """Setup Phase 9 entities: Audio controls."""
1163
+
1164
+ # Microphone volume control
1165
+ microphone_volume = NumberEntity(
1166
+ server=self,
1167
+ key=len(self.state.entities),
1168
+ name="Microphone Volume",
1169
+ object_id="microphone_volume",
1170
+ min_value=0.0,
1171
+ max_value=100.0,
1172
+ step=1.0,
1173
+ icon="mdi:microphone",
1174
+ unit_of_measurement="%",
1175
+ mode=2, # Slider mode
1176
+ value_getter=self.reachy_controller.get_microphone_volume,
1177
+ value_setter=self.reachy_controller.set_microphone_volume,
1178
+ )
1179
+ self.state.entities.append(microphone_volume)
1180
+
1181
+ _LOGGER.info("Phase 9 entities registered: microphone_volume")