RemiFabre commited on
Commit ·
4dc3463
1
Parent(s): 54a230d
status: guard double start and expose get_status snapshot
Browse files
src/reachy_mini_conversation_demo/moves.py
CHANGED
|
@@ -324,7 +324,11 @@ class MovementManager:
|
|
| 324 |
self._face_offsets_dirty = False
|
| 325 |
|
| 326 |
self._shared_state_lock = threading.Lock()
|
|
|
|
| 327 |
self._shared_is_listening = self._is_listening
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
def queue_move(self, move: Move) -> None:
|
| 330 |
"""Queue a primary move to run after the currently executing one.
|
|
@@ -361,6 +365,17 @@ class MovementManager:
|
|
| 361 |
"""
|
| 362 |
self._command_queue.put(("set_moving_state", duration))
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
def mark_user_activity(self) -> None:
|
| 365 |
"""Record external activity and postpone idle behaviours (thread-safe)."""
|
| 366 |
self._command_queue.put(("mark_activity", None))
|
|
@@ -481,6 +496,7 @@ class MovementManager:
|
|
| 481 |
def _publish_shared_state(self) -> None:
|
| 482 |
"""Expose idle-related state for external threads."""
|
| 483 |
with self._shared_state_lock:
|
|
|
|
| 484 |
self._shared_is_listening = self._is_listening
|
| 485 |
|
| 486 |
def _manage_move_queue(self, current_time: float) -> None:
|
|
@@ -686,7 +702,8 @@ class MovementManager:
|
|
| 686 |
else:
|
| 687 |
self._set_target_err_suppressed += 1
|
| 688 |
else:
|
| 689 |
-
self.
|
|
|
|
| 690 |
|
| 691 |
def _update_frequency_stats(
|
| 692 |
self, loop_start: float, prev_loop_start: float, stats: LoopFrequencyStats
|
|
@@ -711,6 +728,18 @@ class MovementManager:
|
|
| 711 |
sleep_time = max(0.0, self.target_period - computation_time)
|
| 712 |
return sleep_time, stats
|
| 713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
def _maybe_log_frequency(
|
| 715 |
self, loop_count: int, print_interval_loops: int, stats: LoopFrequencyStats
|
| 716 |
) -> None:
|
|
@@ -743,6 +772,9 @@ class MovementManager:
|
|
| 743 |
|
| 744 |
def start(self) -> None:
|
| 745 |
"""Start the worker thread that drives the 100 Hz control loop."""
|
|
|
|
|
|
|
|
|
|
| 746 |
self._stop_event.clear()
|
| 747 |
self._thread = threading.Thread(target=self.working_loop, daemon=True)
|
| 748 |
self._thread.start()
|
|
@@ -756,6 +788,41 @@ class MovementManager:
|
|
| 756 |
self._thread = None
|
| 757 |
logger.info("Move worker stopped")
|
| 758 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
def working_loop(self) -> None:
|
| 760 |
"""Control loop main movements - reproduces main_works.py control architecture.
|
| 761 |
|
|
@@ -766,7 +833,7 @@ class MovementManager:
|
|
| 766 |
loop_count = 0
|
| 767 |
prev_loop_start = self._now()
|
| 768 |
print_interval_loops = max(1, int(self.target_frequency * 2))
|
| 769 |
-
freq_stats =
|
| 770 |
|
| 771 |
while not self._stop_event.is_set():
|
| 772 |
loop_start = self._now()
|
|
@@ -797,6 +864,7 @@ class MovementManager:
|
|
| 797 |
# 7) Adaptive sleep to align to next tick, then publish shared state
|
| 798 |
sleep_time, freq_stats = self._schedule_next_tick(loop_start, freq_stats)
|
| 799 |
self._publish_shared_state()
|
|
|
|
| 800 |
|
| 801 |
# 8) Periodic telemetry on loop frequency
|
| 802 |
self._maybe_log_frequency(loop_count, print_interval_loops, freq_stats)
|
|
|
|
| 324 |
self._face_offsets_dirty = False
|
| 325 |
|
| 326 |
self._shared_state_lock = threading.Lock()
|
| 327 |
+
self._shared_last_activity_time = self.state.last_activity_time
|
| 328 |
self._shared_is_listening = self._is_listening
|
| 329 |
+
self._status_lock = threading.Lock()
|
| 330 |
+
self._freq_stats = LoopFrequencyStats()
|
| 331 |
+
self._freq_snapshot = LoopFrequencyStats()
|
| 332 |
|
| 333 |
def queue_move(self, move: Move) -> None:
|
| 334 |
"""Queue a primary move to run after the currently executing one.
|
|
|
|
| 365 |
"""
|
| 366 |
self._command_queue.put(("set_moving_state", duration))
|
| 367 |
|
| 368 |
+
def is_idle(self) -> bool:
|
| 369 |
+
"""Return True when the robot has been inactive longer than the idle delay."""
|
| 370 |
+
with self._shared_state_lock:
|
| 371 |
+
last_activity = self._shared_last_activity_time
|
| 372 |
+
listening = self._shared_is_listening
|
| 373 |
+
|
| 374 |
+
if listening:
|
| 375 |
+
return False
|
| 376 |
+
|
| 377 |
+
return self._now() - last_activity >= self.idle_inactivity_delay
|
| 378 |
+
|
| 379 |
def mark_user_activity(self) -> None:
|
| 380 |
"""Record external activity and postpone idle behaviours (thread-safe)."""
|
| 381 |
self._command_queue.put(("mark_activity", None))
|
|
|
|
| 496 |
def _publish_shared_state(self) -> None:
|
| 497 |
"""Expose idle-related state for external threads."""
|
| 498 |
with self._shared_state_lock:
|
| 499 |
+
self._shared_last_activity_time = self.state.last_activity_time
|
| 500 |
self._shared_is_listening = self._is_listening
|
| 501 |
|
| 502 |
def _manage_move_queue(self, current_time: float) -> None:
|
|
|
|
| 702 |
else:
|
| 703 |
self._set_target_err_suppressed += 1
|
| 704 |
else:
|
| 705 |
+
with self._status_lock:
|
| 706 |
+
self._last_commanded_pose = clone_full_body_pose((head, antennas, body_yaw))
|
| 707 |
|
| 708 |
def _update_frequency_stats(
|
| 709 |
self, loop_start: float, prev_loop_start: float, stats: LoopFrequencyStats
|
|
|
|
| 728 |
sleep_time = max(0.0, self.target_period - computation_time)
|
| 729 |
return sleep_time, stats
|
| 730 |
|
| 731 |
+
def _record_frequency_snapshot(self, stats: LoopFrequencyStats) -> None:
|
| 732 |
+
"""Store a thread-safe snapshot of current frequency statistics."""
|
| 733 |
+
with self._status_lock:
|
| 734 |
+
self._freq_snapshot = LoopFrequencyStats(
|
| 735 |
+
mean=stats.mean,
|
| 736 |
+
m2=stats.m2,
|
| 737 |
+
min_freq=stats.min_freq,
|
| 738 |
+
count=stats.count,
|
| 739 |
+
last_freq=stats.last_freq,
|
| 740 |
+
potential_freq=stats.potential_freq,
|
| 741 |
+
)
|
| 742 |
+
|
| 743 |
def _maybe_log_frequency(
|
| 744 |
self, loop_count: int, print_interval_loops: int, stats: LoopFrequencyStats
|
| 745 |
) -> None:
|
|
|
|
| 772 |
|
| 773 |
def start(self) -> None:
|
| 774 |
"""Start the worker thread that drives the 100 Hz control loop."""
|
| 775 |
+
if self._thread is not None and self._thread.is_alive():
|
| 776 |
+
logger.warning("Move worker already running; start() ignored")
|
| 777 |
+
return
|
| 778 |
self._stop_event.clear()
|
| 779 |
self._thread = threading.Thread(target=self.working_loop, daemon=True)
|
| 780 |
self._thread.start()
|
|
|
|
| 788 |
self._thread = None
|
| 789 |
logger.info("Move worker stopped")
|
| 790 |
|
| 791 |
+
def get_status(self) -> dict[str, Any]:
|
| 792 |
+
"""Return a lightweight status snapshot for observability."""
|
| 793 |
+
with self._status_lock:
|
| 794 |
+
pose_snapshot = clone_full_body_pose(self._last_commanded_pose)
|
| 795 |
+
freq_snapshot = LoopFrequencyStats(
|
| 796 |
+
mean=self._freq_snapshot.mean,
|
| 797 |
+
m2=self._freq_snapshot.m2,
|
| 798 |
+
min_freq=self._freq_snapshot.min_freq,
|
| 799 |
+
count=self._freq_snapshot.count,
|
| 800 |
+
last_freq=self._freq_snapshot.last_freq,
|
| 801 |
+
potential_freq=self._freq_snapshot.potential_freq,
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
head_matrix = pose_snapshot[0].tolist() if pose_snapshot else None
|
| 805 |
+
antennas = pose_snapshot[1] if pose_snapshot else None
|
| 806 |
+
body_yaw = pose_snapshot[2] if pose_snapshot else None
|
| 807 |
+
|
| 808 |
+
return {
|
| 809 |
+
"queue_size": len(self.move_queue),
|
| 810 |
+
"is_listening": self._is_listening,
|
| 811 |
+
"breathing_active": self._breathing_active,
|
| 812 |
+
"last_commanded_pose": {
|
| 813 |
+
"head": head_matrix,
|
| 814 |
+
"antennas": antennas,
|
| 815 |
+
"body_yaw": body_yaw,
|
| 816 |
+
},
|
| 817 |
+
"loop_frequency": {
|
| 818 |
+
"last": freq_snapshot.last_freq,
|
| 819 |
+
"mean": freq_snapshot.mean,
|
| 820 |
+
"min": freq_snapshot.min_freq,
|
| 821 |
+
"potential": freq_snapshot.potential_freq,
|
| 822 |
+
"samples": freq_snapshot.count,
|
| 823 |
+
},
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
def working_loop(self) -> None:
|
| 827 |
"""Control loop main movements - reproduces main_works.py control architecture.
|
| 828 |
|
|
|
|
| 833 |
loop_count = 0
|
| 834 |
prev_loop_start = self._now()
|
| 835 |
print_interval_loops = max(1, int(self.target_frequency * 2))
|
| 836 |
+
freq_stats = self._freq_stats
|
| 837 |
|
| 838 |
while not self._stop_event.is_set():
|
| 839 |
loop_start = self._now()
|
|
|
|
| 864 |
# 7) Adaptive sleep to align to next tick, then publish shared state
|
| 865 |
sleep_time, freq_stats = self._schedule_next_tick(loop_start, freq_stats)
|
| 866 |
self._publish_shared_state()
|
| 867 |
+
self._record_frequency_snapshot(freq_stats)
|
| 868 |
|
| 869 |
# 8) Periodic telemetry on loop frequency
|
| 870 |
self._maybe_log_frequency(loop_count, print_interval_loops, freq_stats)
|