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._last_commanded_pose = clone_full_body_pose((head, antennas, body_yaw))
 
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 = LoopFrequencyStats()
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)