drenayaz commited on
Commit
5945942
·
1 Parent(s): c3b8d8e

add debug page for each component

Browse files
wake_me_up/main.py CHANGED
@@ -31,6 +31,25 @@ class WakeMeUp(ReachyMiniApp):
31
  STATE_POLL_INTERVAL = 500 # ms - frontend polling interval
32
  AUDIO_POLL_INTERVAL = 100 # ms - audio level polling interval
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  def __init__(self):
35
  super().__init__()
36
  self._state_lock = threading.Lock()
@@ -57,7 +76,17 @@ class WakeMeUp(ReachyMiniApp):
57
  self._touch_start_time = None
58
  self._touch_accumulated_time = 0.0 # Total time above threshold
59
  self._last_audio_check = None
60
- self._auto_play_movement = False # Flag to auto-play movement in step 2
 
 
 
 
 
 
 
 
 
 
61
 
62
  self._register_routes()
63
 
@@ -106,16 +135,18 @@ class WakeMeUp(ReachyMiniApp):
106
  # Main loop
107
  while not stop_event.is_set():
108
  # Check and execute auto-play actions (one per iteration)
109
- check_auto_play('_auto_play_movement', 2, 0.5, self._play_test_movement, (reachy_mini,), "test movement") or \
110
- check_auto_play('_auto_play_emotion', 3, 0.5, self._play_emotion, (reachy_mini, "success2"), "happy emotion") or \
111
- check_auto_play('_auto_play_curious', 4, 1.0, self._play_emotion, (reachy_mini, "curious1"), "curious emotion") or \
112
- check_auto_play('_auto_play_enthusiastic', 5, 0.5, self._play_emotion, (reachy_mini, "enthusiastic1"), "enthusiastic emotion")
113
-
114
- # Capture camera frames only during step 4
 
 
115
  with self._state_lock:
116
  current = self._current_step
117
 
118
- if current == 4:
119
  frame = reachy_mini.media.get_frame()
120
  if frame is not None:
121
  with self._state_lock:
@@ -144,8 +175,8 @@ class WakeMeUp(ReachyMiniApp):
144
  with self._state_lock:
145
  self._audio_level = audio_level
146
 
147
- # Touch detection logic (only during step 1)
148
- if self._current_step == 1 and not self._touch_detected:
149
  current_time = time.time()
150
 
151
  if audio_level > self.TOUCH_THRESHOLD:
@@ -158,9 +189,9 @@ class WakeMeUp(ReachyMiniApp):
158
  # Check if accumulated time reaches required duration
159
  if self._touch_accumulated_time >= self.TOUCH_DURATION_REQUIRED:
160
  self._touch_detected = True
161
- print("Touch detected! Auto-advancing to step 2")
162
- # Auto-advance to step 2
163
- self._current_step = 2
164
  self._auto_play_movement = True
165
 
166
  if self._touch_start_time is None:
@@ -199,6 +230,59 @@ class WakeMeUp(ReachyMiniApp):
199
  except Exception as e:
200
  print(f"Error stopping audio: {e}")
201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  def _get_current_volume(self) -> float:
203
  """Read current system volume (same method as dashboard)"""
204
  try:
@@ -478,6 +562,211 @@ class WakeMeUp(ReachyMiniApp):
478
  with self._state_lock:
479
  self._emotion_playing = False
480
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  # ========== Camera Streaming ==========
482
  def _frame_generator(self):
483
  """MJPEG streaming generator"""
@@ -529,34 +818,84 @@ class WakeMeUp(ReachyMiniApp):
529
  @self.settings_app.post("/api/validate_step")
530
  def validate_step(payload: dict):
531
  """User validates current step with works/doesn't work"""
532
- step = payload.get("step") # 1, 2, 3, or 4
533
  works = payload.get("works") # True or False
534
 
535
  with self._state_lock:
536
  if step == 1:
 
537
  self._current_step = 2
538
- # Reset touch detection for next time
 
 
 
 
 
539
  self._touch_detected = False
540
  self._touch_start_time = None
541
  self._touch_accumulated_time = 0.0
542
  self._last_audio_check = None
543
- elif step == 2:
544
- self._current_step = 3
545
- # Auto-play emotion when entering step 3
546
- self._auto_play_emotion = True
547
  elif step == 3:
 
548
  self._current_step = 4
549
- # Auto-play curious emotion after 1s on step 4
550
- self._auto_play_curious = True
 
 
 
551
  elif step == 4:
552
- self._current_step = 5 # Done
553
- # Auto-play enthusiastic emotion on step 5
 
 
 
 
 
 
 
 
 
 
 
554
  self._auto_play_enthusiastic = True
555
 
556
  next_step = self._current_step
557
 
558
  return {"status": "ok", "next_step": next_step}
559
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  @self.settings_app.post("/api/play_movement")
561
  def play_movement():
562
  """Trigger motor test movement"""
@@ -598,6 +937,20 @@ class WakeMeUp(ReachyMiniApp):
598
  ).start()
599
  return {"status": "playing"}
600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  @self.settings_app.post("/api/set_volume")
602
  def set_volume(payload: dict):
603
  volume = payload.get("volume", 0.5)
@@ -639,6 +992,140 @@ class WakeMeUp(ReachyMiniApp):
639
  media_type="multipart/x-mixed-replace; boundary=frame"
640
  )
641
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  def _serialize_state(self):
643
  """Serialize current state for API"""
644
  with self._state_lock:
 
31
  STATE_POLL_INTERVAL = 500 # ms - frontend polling interval
32
  AUDIO_POLL_INTERVAL = 100 # ms - audio level polling interval
33
 
34
+ # Expected sleep position (degrees) - from testbench
35
+ SLEEP_POSITION_DEGREES = {
36
+ "body_rotation": 5.0,
37
+ "stewart_1": -23.0,
38
+ "stewart_2": 58.5,
39
+ "stewart_3": -11.0,
40
+ "stewart_4": 9.5,
41
+ "stewart_5": -58.0,
42
+ "stewart_6": 23.0,
43
+ "right_antenna": -175.0,
44
+ "left_antenna": 175.0,
45
+ }
46
+
47
+ # Thresholds for position validation (degrees)
48
+ POSITION_ERROR_THRESHOLD = 15.0 # Error if diff > 15° (for stewart motors)
49
+ POSITION_ERROR_THRESHOLD_BASE = 20.0 # Error for body_rotation
50
+ POSITION_ERROR_THRESHOLD_ANTENNAS = 40.0 # Error for antennas
51
+ SWAP_DETECTION_THRESHOLD = 15.0 # Threshold for detecting motor swaps
52
+
53
  def __init__(self):
54
  super().__init__()
55
  self._state_lock = threading.Lock()
 
76
  self._touch_start_time = None
77
  self._touch_accumulated_time = 0.0 # Total time above threshold
78
  self._last_audio_check = None
79
+ self._auto_play_movement = False # Flag to auto-play movement in step 4
80
+ self._auto_go_to_sleep = False # Flag to auto-sleep when going back to step 3
81
+ self._auto_disable_motors = False # Flag to disable motors on step 2
82
+ self._sleep_position_check = None # Stores sleep position validation results
83
+
84
+ # Audio recording for debug page
85
+ self._recorded_audio = None # Stores recorded audio data
86
+ self._is_recording = False
87
+
88
+ # Track if we're in debug mode for each step
89
+ self._in_debug_mode = False
90
 
91
  self._register_routes()
92
 
 
135
  # Main loop
136
  while not stop_event.is_set():
137
  # Check and execute auto-play actions (one per iteration)
138
+ check_auto_play('_auto_play_movement', 4, 0.5, self._play_test_movement, (reachy_mini,), "test movement") or \
139
+ check_auto_play('_auto_play_emotion', 5, 0.5, self._play_emotion, (reachy_mini, "success2"), "happy emotion") or \
140
+ check_auto_play('_auto_play_curious', 6, 1.0, self._play_emotion, (reachy_mini, "curious1"), "curious emotion") or \
141
+ check_auto_play('_auto_play_enthusiastic', 7, 0.5, self._play_emotion, (reachy_mini, "enthusiastic1"), "enthusiastic emotion") or \
142
+ check_auto_play('_auto_go_to_sleep', 3, 0.3, self._go_to_sleep_wrapper, (reachy_mini,), "goto sleep") or \
143
+ check_auto_play('_auto_disable_motors', 2, 0.3, self._disable_motors_wrapper, (reachy_mini,), "disable motors")
144
+
145
+ # Capture camera frames only during step 6
146
  with self._state_lock:
147
  current = self._current_step
148
 
149
+ if current == 6:
150
  frame = reachy_mini.media.get_frame()
151
  if frame is not None:
152
  with self._state_lock:
 
175
  with self._state_lock:
176
  self._audio_level = audio_level
177
 
178
+ # Touch detection logic (only during step 3 and not in debug mode)
179
+ if self._current_step == 3 and not self._touch_detected and not self._in_debug_mode:
180
  current_time = time.time()
181
 
182
  if audio_level > self.TOUCH_THRESHOLD:
 
189
  # Check if accumulated time reaches required duration
190
  if self._touch_accumulated_time >= self.TOUCH_DURATION_REQUIRED:
191
  self._touch_detected = True
192
+ print("Touch detected! Auto-advancing to step 4")
193
+ # Auto-advance to step 4
194
+ self._current_step = 4
195
  self._auto_play_movement = True
196
 
197
  if self._touch_start_time is None:
 
230
  except Exception as e:
231
  print(f"Error stopping audio: {e}")
232
 
233
+ def _record_audio(self, duration=5.0):
234
+ """Record audio for debug testing"""
235
+ try:
236
+ import sounddevice as sd
237
+
238
+ with self._state_lock:
239
+ self._is_recording = True
240
+
241
+ print(f"Recording audio for {duration} seconds...")
242
+ samplerate = 44100
243
+ recorded = sd.rec(
244
+ int(duration * samplerate),
245
+ samplerate=samplerate,
246
+ channels=1,
247
+ dtype='float32'
248
+ )
249
+ sd.wait() # Wait until recording is finished
250
+
251
+ with self._state_lock:
252
+ self._recorded_audio = recorded
253
+ self._is_recording = False
254
+
255
+ print("Recording complete")
256
+ return True
257
+
258
+ except Exception as e:
259
+ print(f"Failed to record audio: {e}")
260
+ with self._state_lock:
261
+ self._is_recording = False
262
+ return False
263
+
264
+ def _play_recorded_audio(self):
265
+ """Play back recorded audio"""
266
+ try:
267
+ import sounddevice as sd
268
+
269
+ with self._state_lock:
270
+ audio_data = self._recorded_audio
271
+
272
+ if audio_data is None:
273
+ print("No recorded audio to play")
274
+ return False
275
+
276
+ print("Playing recorded audio...")
277
+ sd.play(audio_data, samplerate=44100)
278
+ sd.wait() # Wait until playback is finished
279
+ print("Playback complete")
280
+ return True
281
+
282
+ except Exception as e:
283
+ print(f"Failed to play recorded audio: {e}")
284
+ return False
285
+
286
  def _get_current_volume(self) -> float:
287
  """Read current system volume (same method as dashboard)"""
288
  try:
 
562
  with self._state_lock:
563
  self._emotion_playing = False
564
 
565
+ def _go_to_sleep_wrapper(self, reachy_mini):
566
+ """Put robot to sleep (for going back from step 4 to step 3)"""
567
+ try:
568
+ print("Going to sleep (back navigation)...")
569
+ reachy_mini.goto_sleep()
570
+ with self._state_lock:
571
+ self._is_sleeping = True
572
+ print("Robot is now sleeping")
573
+ except Exception as e:
574
+ print(f"Failed to go to sleep: {e}")
575
+
576
+ def _disable_motors_wrapper(self, reachy_mini):
577
+ """Disable motors for manual positioning (step 2)"""
578
+ try:
579
+ print("Disabling motors for manual positioning...")
580
+ reachy_mini.disable_motors()
581
+ with self._state_lock:
582
+ self._is_sleeping = False
583
+ print("Motors disabled")
584
+ except Exception as e:
585
+ print(f"Failed to disable motors: {e}")
586
+
587
+ def _test_antenna_movement(self, reachy_mini, side):
588
+ """Move one antenna to test if it's correctly connected"""
589
+ try:
590
+ print(f"Testing {side} antenna...")
591
+ reachy_mini.enable_motors()
592
+
593
+ # First raise head to neutral (0 degrees pitch)
594
+ reachy_mini.goto_target(
595
+ head=create_head_pose(pitch=0.0, yaw=0.0, roll=0.0),
596
+ antennas=[0, 0],
597
+ duration=0.8
598
+ )
599
+ time.sleep(0.8)
600
+
601
+ # Move the specified antenna with a small shake (4 repetitions)
602
+ # Note: antenna indices are swapped - right is index 0, left is index 1
603
+ if side == "left":
604
+ # Left antenna (index 1) - fast shake 4 times
605
+ head=create_head_pose(pitch=0.0, yaw=0.0, roll=0.0),
606
+ for _ in range(4):
607
+ reachy_mini.goto_target(antennas=np.radians([0, 10]), duration=0.2)
608
+ reachy_mini.goto_target(antennas=np.radians([0, -10]), duration=0.2)
609
+ # Return to neutral
610
+ reachy_mini.goto_target(antennas=np.radians([0, 0]), duration=0.2)
611
+
612
+ elif side == "right":
613
+ # Right antenna (index 0) - fast shake 4 times
614
+ for _ in range(4):
615
+ reachy_mini.goto_target(antennas=np.radians([10, 0]), duration=0.2)
616
+ reachy_mini.goto_target(antennas=np.radians([-10, 0]), duration=0.2)
617
+ # Return to neutral
618
+ reachy_mini.goto_target(antennas=np.radians([0, 0]), duration=0.2)
619
+
620
+ print(f"{side} antenna test complete")
621
+
622
+ except Exception as e:
623
+ print(f"Failed to test {side} antenna: {e}")
624
+
625
+ def _detect_motor_swaps(self, motors_data):
626
+ """Detect if motors have been swapped during assembly
627
+
628
+ Args:
629
+ motors_data: dict of {motor_name: actual_position_deg}
630
+
631
+ Returns:
632
+ list of detected swaps: [{"motor_a": name, "motor_b": name, "confidence": str}, ...]
633
+ """
634
+ detected_swaps = []
635
+ checked_pairs = set() # To avoid checking the same pair twice
636
+
637
+ motor_names = list(motors_data.keys())
638
+
639
+ for i, motor_a in enumerate(motor_names):
640
+ if motor_a not in self.SLEEP_POSITION_DEGREES:
641
+ continue
642
+
643
+ actual_a = motors_data[motor_a]
644
+ expected_a = self.SLEEP_POSITION_DEGREES[motor_a]
645
+
646
+ # Skip if motor A is already in correct position
647
+ if abs(actual_a - expected_a) < self.SWAP_DETECTION_THRESHOLD:
648
+ continue
649
+
650
+ for motor_b in motor_names[i+1:]:
651
+ if motor_b not in self.SLEEP_POSITION_DEGREES:
652
+ continue
653
+
654
+ # Skip if already checked this pair
655
+ pair_key = tuple(sorted([motor_a, motor_b]))
656
+ if pair_key in checked_pairs:
657
+ continue
658
+
659
+ actual_b = motors_data[motor_b]
660
+ expected_b = self.SLEEP_POSITION_DEGREES[motor_b]
661
+
662
+ # Skip if motor B is already in correct position
663
+ if abs(actual_b - expected_b) < self.SWAP_DETECTION_THRESHOLD:
664
+ continue
665
+
666
+ # Check if positions are swapped:
667
+ # - Motor A's actual position is close to Motor B's expected position
668
+ # - Motor B's actual position is close to Motor A's expected position
669
+ diff_a_to_b = abs(actual_a - expected_b)
670
+ diff_b_to_a = abs(actual_b - expected_a)
671
+
672
+ if diff_a_to_b < self.SWAP_DETECTION_THRESHOLD and diff_b_to_a < self.SWAP_DETECTION_THRESHOLD:
673
+ # Calculate confidence based on how close the match is
674
+ avg_diff = (diff_a_to_b + diff_b_to_a) / 2
675
+ if avg_diff < 5.0:
676
+ confidence = "high"
677
+ elif avg_diff < 10.0:
678
+ confidence = "medium"
679
+ else:
680
+ confidence = "low"
681
+
682
+ detected_swaps.append({
683
+ "motor_a": motor_a,
684
+ "motor_b": motor_b,
685
+ "confidence": confidence,
686
+ "diff_avg": round(avg_diff, 1)
687
+ })
688
+
689
+ checked_pairs.add(pair_key)
690
+
691
+ return detected_swaps
692
+
693
+ def _check_sleep_position(self, reachy_mini):
694
+ """Check if robot is in correct sleep position"""
695
+ try:
696
+ print("Checking sleep position...")
697
+ head_joints, antenna_joints = reachy_mini.get_current_joint_positions()
698
+
699
+ # Convert to degrees
700
+ motors_data = {
701
+ "body_rotation": np.rad2deg(head_joints[0]),
702
+ "stewart_1": np.rad2deg(head_joints[1]),
703
+ "stewart_2": np.rad2deg(head_joints[2]),
704
+ "stewart_3": np.rad2deg(head_joints[3]),
705
+ "stewart_4": np.rad2deg(head_joints[4]),
706
+ "stewart_5": np.rad2deg(head_joints[5]),
707
+ "stewart_6": np.rad2deg(head_joints[6]),
708
+ "right_antenna": np.rad2deg(antenna_joints[0]),
709
+ "left_antenna": np.rad2deg(antenna_joints[1]),
710
+ }
711
+
712
+ # Validate each motor
713
+ results = []
714
+ all_ok = True
715
+
716
+ for motor_name, actual_deg in motors_data.items():
717
+ if motor_name in self.SLEEP_POSITION_DEGREES:
718
+ expected = self.SLEEP_POSITION_DEGREES[motor_name]
719
+ diff = abs(actual_deg - expected)
720
+
721
+ # Use different thresholds based on motor type
722
+ if motor_name == "body_rotation":
723
+ threshold = self.POSITION_ERROR_THRESHOLD_BASE
724
+ elif motor_name in ["right_antenna", "left_antenna"]:
725
+ threshold = self.POSITION_ERROR_THRESHOLD_ANTENNAS
726
+ else:
727
+ threshold = self.POSITION_ERROR_THRESHOLD
728
+
729
+ if diff > threshold:
730
+ status = "error"
731
+ all_ok = False
732
+ else:
733
+ status = "ok"
734
+
735
+ results.append({
736
+ "name": motor_name,
737
+ "actual": round(actual_deg, 1),
738
+ "expected": expected,
739
+ "diff": round(diff, 1),
740
+ "status": status,
741
+ "threshold": threshold
742
+ })
743
+
744
+ # Detect potential motor swaps
745
+ detected_swaps = []
746
+ if not all_ok:
747
+ detected_swaps = self._detect_motor_swaps(motors_data)
748
+
749
+ validation_result = {
750
+ "motors": results,
751
+ "all_ok": all_ok,
752
+ "has_errors": not all_ok,
753
+ "detected_swaps": detected_swaps,
754
+ "has_swaps": len(detected_swaps) > 0,
755
+ "timestamp": time.time()
756
+ }
757
+
758
+ with self._state_lock:
759
+ self._sleep_position_check = validation_result
760
+
761
+ if detected_swaps:
762
+ print(f"Sleep position check: {len(detected_swaps)} potential swap(s) detected")
763
+ print(f"Sleep position check complete: all_ok={validation_result['all_ok']}")
764
+ return validation_result
765
+
766
+ except Exception as e:
767
+ print(f"Failed to check sleep position: {e}")
768
+ return None
769
+
770
  # ========== Camera Streaming ==========
771
  def _frame_generator(self):
772
  """MJPEG streaming generator"""
 
818
  @self.settings_app.post("/api/validate_step")
819
  def validate_step(payload: dict):
820
  """User validates current step with works/doesn't work"""
821
+ step = payload.get("step") # 1-6
822
  works = payload.get("works") # True or False
823
 
824
  with self._state_lock:
825
  if step == 1:
826
+ # Step 1: Intro page -> Step 2: Sleep position
827
  self._current_step = 2
828
+ # Trigger motor disable on entering step 2
829
+ self._auto_disable_motors = True
830
+ elif step == 2:
831
+ # Step 2: Sleep position -> Step 3: Audio test
832
+ self._current_step = 3
833
+ # Reset touch detection
834
  self._touch_detected = False
835
  self._touch_start_time = None
836
  self._touch_accumulated_time = 0.0
837
  self._last_audio_check = None
 
 
 
 
838
  elif step == 3:
839
+ # Step 3: Audio test -> Step 4: Motor test
840
  self._current_step = 4
841
+ # Reset touch detection for next time
842
+ self._touch_detected = False
843
+ self._touch_start_time = None
844
+ self._touch_accumulated_time = 0.0
845
+ self._last_audio_check = None
846
  elif step == 4:
847
+ # Step 4: Motor test -> Step 5: Audio output test
848
+ self._current_step = 5
849
+ # Auto-play emotion when entering step 5
850
+ self._auto_play_emotion = True
851
+ elif step == 5:
852
+ # Step 5: Audio output test -> Step 6: Camera test
853
+ self._current_step = 6
854
+ # Auto-play curious emotion after 1s on step 6
855
+ self._auto_play_curious = True
856
+ elif step == 6:
857
+ # Step 6: Camera test -> Step 7: Success
858
+ self._current_step = 7
859
+ # Auto-play enthusiastic emotion on step 7
860
  self._auto_play_enthusiastic = True
861
 
862
  next_step = self._current_step
863
 
864
  return {"status": "ok", "next_step": next_step}
865
 
866
+ @self.settings_app.post("/api/go_to_step")
867
+ def go_to_step(payload: dict):
868
+ """Go directly to a specific step (for back navigation)"""
869
+ target_step = payload.get("step")
870
+
871
+ if target_step not in [1, 2, 3, 4, 5, 6]:
872
+ return {"status": "error", "message": "Invalid step"}
873
+
874
+ with self._state_lock:
875
+ # Only allow going back if robot is not busy
876
+ if self._movement_busy or self._emotion_playing:
877
+ return {"status": "error", "message": "Robot is busy"}
878
+
879
+ current_step = self._current_step
880
+ self._current_step = target_step
881
+
882
+ # Reset touch detection state when going back to step 3
883
+ if target_step == 3:
884
+ self._touch_detected = False
885
+ self._touch_start_time = None
886
+ self._touch_accumulated_time = 0.0
887
+ self._last_audio_check = None
888
+
889
+ # If going from step 4 to step 3, trigger auto-sleep
890
+ if current_step == 4 and target_step == 3:
891
+ self._auto_go_to_sleep = True
892
+
893
+ # If entering step 2, trigger motor disable
894
+ if target_step == 2:
895
+ self._auto_disable_motors = True
896
+
897
+ return {"status": "ok", "current_step": target_step}
898
+
899
  @self.settings_app.post("/api/play_movement")
900
  def play_movement():
901
  """Trigger motor test movement"""
 
937
  ).start()
938
  return {"status": "playing"}
939
 
940
+ @self.settings_app.post("/api/play_test_sound")
941
+ def play_test_sound():
942
+ """Play a short test sound without movement (for volume testing)"""
943
+ if self._reachy_mini is None:
944
+ return {"status": "error", "message": "Robot not initialized"}
945
+
946
+ try:
947
+ # Play just the sound without any movement
948
+ self._reachy_mini.media.play_sound("wake_up.wav")
949
+ return {"status": "playing"}
950
+ except Exception as e:
951
+ print(f"Failed to play test sound: {e}")
952
+ return {"status": "error", "message": str(e)}
953
+
954
  @self.settings_app.post("/api/set_volume")
955
  def set_volume(payload: dict):
956
  volume = payload.get("volume", 0.5)
 
992
  media_type="multipart/x-mixed-replace; boundary=frame"
993
  )
994
 
995
+ @self.settings_app.post("/api/check_sleep_position")
996
+ def check_sleep_position():
997
+ """Check if robot motors are in correct sleep position"""
998
+ if self._reachy_mini is None:
999
+ return {"status": "error", "message": "Robot not initialized"}
1000
+
1001
+ result = self._check_sleep_position(self._reachy_mini)
1002
+ if result is None:
1003
+ return {"status": "error", "message": "Failed to check position"}
1004
+
1005
+ return {"status": "ok", "validation": result}
1006
+
1007
+ @self.settings_app.get("/api/sleep_position_status")
1008
+ def get_sleep_position_status():
1009
+ """Get last sleep position check results"""
1010
+ with self._state_lock:
1011
+ if self._sleep_position_check is None:
1012
+ return {"status": "not_checked"}
1013
+ return {"status": "ok", "validation": self._sleep_position_check}
1014
+
1015
+ @self.settings_app.get("/api/robot_variant")
1016
+ def get_robot_variant():
1017
+ """Get robot variant (wireless or lite)"""
1018
+ if self._reachy_mini is None:
1019
+ return {"status": "error", "message": "Robot not initialized"}
1020
+
1021
+ try:
1022
+ daemon_status = self._reachy_mini.client.get_status()
1023
+ is_wireless = daemon_status.get("wireless_version", False)
1024
+ return {
1025
+ "status": "ok",
1026
+ "variant": "wireless" if is_wireless else "lite"
1027
+ }
1028
+ except Exception as e:
1029
+ return {"status": "error", "message": str(e)}
1030
+
1031
+ @self.settings_app.post("/api/start_recording")
1032
+ def start_recording(payload: dict):
1033
+ """Start recording audio for debug testing"""
1034
+ duration = payload.get("duration", 5.0)
1035
+
1036
+ with self._state_lock:
1037
+ if self._is_recording:
1038
+ return {"status": "error", "message": "Already recording"}
1039
+
1040
+ threading.Thread(
1041
+ target=self._record_audio,
1042
+ args=(duration,),
1043
+ daemon=True
1044
+ ).start()
1045
+
1046
+ return {"status": "recording", "duration": duration}
1047
+
1048
+ @self.settings_app.post("/api/play_recording")
1049
+ def play_recording():
1050
+ """Play back recorded audio"""
1051
+ with self._state_lock:
1052
+ if self._recorded_audio is None:
1053
+ return {"status": "error", "message": "No recording available"}
1054
+
1055
+ threading.Thread(
1056
+ target=self._play_recorded_audio,
1057
+ daemon=True
1058
+ ).start()
1059
+
1060
+ return {"status": "playing"}
1061
+
1062
+ @self.settings_app.get("/api/recording_status")
1063
+ def get_recording_status():
1064
+ """Get current recording status"""
1065
+ with self._state_lock:
1066
+ return {
1067
+ "is_recording": self._is_recording,
1068
+ "has_recording": self._recorded_audio is not None
1069
+ }
1070
+
1071
+ @self.settings_app.post("/api/test_antenna")
1072
+ def test_antenna(payload: dict):
1073
+ """Test individual antenna movement for debug"""
1074
+ if self._reachy_mini is None:
1075
+ return {"status": "error", "message": "Robot not initialized"}
1076
+
1077
+ antenna_side = payload.get("side") # "left" or "right"
1078
+ if antenna_side not in ["left", "right"]:
1079
+ return {"status": "error", "message": "Invalid antenna side"}
1080
+
1081
+ threading.Thread(
1082
+ target=self._test_antenna_movement,
1083
+ args=(self._reachy_mini, antenna_side),
1084
+ daemon=True
1085
+ ).start()
1086
+
1087
+ return {"status": "testing", "side": antenna_side}
1088
+
1089
+ @self.settings_app.get("/api/capture_photo")
1090
+ def capture_photo():
1091
+ """Capture a single photo from the camera for debug"""
1092
+ if self._reachy_mini is None:
1093
+ return {"status": "error", "message": "Robot not initialized"}
1094
+
1095
+ try:
1096
+ # Capture a frame
1097
+ frame = self._reachy_mini.media.get_frame()
1098
+ if frame is None:
1099
+ return {"status": "error", "message": "No frame available"}
1100
+
1101
+ # Resize and encode as JPEG
1102
+ resized = cv2.resize(frame, (640, 480))
1103
+ ret, jpeg = cv2.imencode('.jpg', resized, [cv2.IMWRITE_JPEG_QUALITY, 90])
1104
+
1105
+ if not ret:
1106
+ return {"status": "error", "message": "Failed to encode image"}
1107
+
1108
+ # Convert to base64 for embedding in JSON
1109
+ import base64
1110
+ img_base64 = base64.b64encode(jpeg.tobytes()).decode('utf-8')
1111
+
1112
+ return {
1113
+ "status": "ok",
1114
+ "image": f"data:image/jpeg;base64,{img_base64}"
1115
+ }
1116
+
1117
+ except Exception as e:
1118
+ print(f"Failed to capture photo: {e}")
1119
+ return {"status": "error", "message": str(e)}
1120
+
1121
+ @self.settings_app.post("/api/set_debug_mode")
1122
+ def set_debug_mode(payload: dict):
1123
+ """Set debug mode flag to disable auto-advance"""
1124
+ debug_mode = payload.get("debug_mode", False)
1125
+ with self._state_lock:
1126
+ self._in_debug_mode = debug_mode
1127
+ return {"status": "ok", "debug_mode": debug_mode}
1128
+
1129
  def _serialize_state(self):
1130
  """Serialize current state for API"""
1131
  with self._state_lock:
wake_me_up/static/assets/reachy_mini_side.jpg DELETED

Git LFS Details

  • SHA256: 6be9599b12ce311e4f9de66f568cbb73a630965ec5506d90dd10cebf41a45950
  • Pointer size: 131 Bytes
  • Size of remote file: 106 kB
wake_me_up/static/index.html CHANGED
@@ -8,7 +8,6 @@
8
  </head>
9
  <body>
10
  <header class="header">
11
- <h1>First Steps with Reachy Mini</h1>
12
  </header>
13
 
14
  <main class="container">
@@ -24,116 +23,452 @@
24
  </div>
25
  </section>
26
 
27
- <!-- Step 1: Audio Input Test -->
28
  <section id="step-1" class="step-screen">
29
  <div class="card">
30
  <div class="title-with-icon">
31
- <img src="/static/assets/sleeping-reachy.svg" alt="Sleeping Reachy" class="sleeping-reachy-icon">
32
- <h2>Time to Wake Up!</h2>
33
  </div>
34
- <video class="demo-video" autoplay loop muted playsinline>
35
- <source src="/static/assets/touch_demo.mp4" type="video/mp4">
36
- <p style="color: #999;">Demo video not available</p>
37
- </video>
38
-
39
- <p class="instruction">Wake up Reachy Mini</p>
40
 
41
- <div class="touch-progress-container">
42
- <div class="touch-progress-bar" id="touch-progress-bar"></div>
 
 
 
43
  </div>
44
 
45
- <div class="waveform-container" id="waveform"></div>
46
-
47
  <div class="button-group">
48
- <a href="https://huggingface.co/docs/reachy_mini/troubleshooting" target="_blank" class="btn-fail">Sound doesn't work </a>
49
  </div>
50
-
51
- <div class="step-indicator" id="step-indicator">Step <span id="current-step">1</span> of 4</div>
52
  </div>
53
  </section>
54
 
55
- <!-- Step 2: Motor Test -->
56
  <section id="step-2" class="step-screen">
57
  <div class="card">
58
- <div class="title-with-icon">
59
- <img src="/static/assets/reachy-mini.png" alt="Reachy Mini" class="reachy-mini-icon">
60
- <h2>Stretch Time!</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
- <video class="demo-video" muted playsinline preload="auto">
63
- <source src="/static/assets/movement_demo.mp4" type="video/mp4">
64
- <p style="color: #999;">Demo video not available</p>
65
- </video>
66
- <p class="instruction">Watch the robot perform the movement</p>
67
 
68
- <button class="btn-text" id="btn-play-movement" onclick="playMovement()">
69
- Replay Movement
70
- </button>
 
 
 
 
 
 
 
71
 
72
- <div class="button-group" id="motor-validate" style="display: none;">
73
- <a href="https://huggingface.co/docs/reachy_mini/troubleshooting" target="_blank" class="btn-fail">Robot didn't move correctly </a>
74
- <button class="btn-success" onclick="validateStep(2, true)">Robot did the same </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  </div>
76
 
77
- <div class="step-indicator">Step 2 of 4</div>
 
78
  </div>
79
  </section>
80
 
81
- <!-- Step 3: Audio Output Test -->
82
  <section id="step-3" class="step-screen">
83
  <div class="card">
84
- <div class="title-with-icon">
85
- <img src="/static/assets/reachy-mini-sav.png" alt="Reachy Mini" class="reachy-mini-icon">
86
- <h2>Can You Hear Me?</h2>
87
- </div>
88
- <p class="instruction">Listen to the robot's happy sound</p>
 
 
 
 
 
89
 
90
- <div class="volume-control">
91
- <label for="volume-slider">Volume</label>
92
- <input type="range" id="volume-slider" min="0" max="100" value="50">
93
- <span id="volume-value">50%</span>
 
 
 
 
 
 
 
94
  </div>
95
 
96
- <button class="btn-text" id="btn-play-sound" onclick="playHappy()">Play Sound</button>
 
 
 
 
 
97
 
98
- <div class="button-group">
99
- <a href="https://huggingface.co/docs/reachy_mini/troubleshooting" target="_blank" class="btn-fail">No sound </a>
100
- <button class="btn-success" onclick="validateStep(3, true)">I hear the sound </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  </div>
102
 
103
- <div class="step-indicator">Step 3 of 4</div>
 
 
 
 
 
 
 
 
 
104
  </div>
105
  </section>
106
 
107
- <!-- Step 4: Camera Test -->
108
  <section id="step-4" class="step-screen">
109
  <div class="card">
110
- <div class="title-with-icon">
111
- <img src="/static/assets/reachy-mini-explorer.png" alt="Reachy Mini" class="reachy-mini-icon explorer-icon">
112
- <h2>Let's Look Around!</h2>
113
- </div>
114
- <p class="instruction">Check if you can see the camera feed</p>
 
 
 
 
 
 
 
 
 
 
115
 
116
- <div class="camera-container">
117
- <img id="camera-feed" src="/video_feed" alt="Camera feed">
 
 
118
  </div>
119
 
120
- <div class="button-group">
121
- <a href="https://huggingface.co/docs/reachy_mini/troubleshooting" target="_blank" class="btn-fail">Camera doesn't work </a>
122
- <button class="btn-success" onclick="validateStep(4, true)">Camera works </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  </div>
124
 
125
- <div class="step-indicator">Step 4 of 4</div>
 
 
 
 
 
 
 
 
 
126
  </div>
127
  </section>
128
 
129
- <!-- Step 5: Summary -->
130
  <section id="step-5" class="step-screen">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  <div class="card">
132
  <div class="title-with-icon">
133
  <img src="/static/assets/reachy-mini-party.png" alt="Reachy Mini" class="reachy-mini-icon">
134
  <h2>We're Ready to Play!</h2>
135
  </div>
136
  <p class="instruction" style="font-size: 18px; margin: 30px 0;">Everything is working correctly</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  </div>
138
  </section>
139
  </main>
 
8
  </head>
9
  <body>
10
  <header class="header">
 
11
  </header>
12
 
13
  <main class="container">
 
23
  </div>
24
  </section>
25
 
26
+ <!-- Step 1: Introduction -->
27
  <section id="step-1" class="step-screen">
28
  <div class="card">
29
  <div class="title-with-icon">
30
+ <img src="/static/assets/reachy-mini.png" alt="Reachy Mini" class="reachy-mini-icon">
31
+ <h2>Welcome to Wake Me Up App</h2>
32
  </div>
33
+ <p class="instruction">This app will help you test all of Reachy Mini's components: microphone, motors, speaker, and camera.</p>
 
 
 
 
 
34
 
35
+ <div style="display: flex; align-items: center; gap: 20px; margin: 30px auto; max-width: 550px;">
36
+ <img src="/static/assets/reachy-mini-stop.svg" alt="Warning" style="width: 110px; height: auto; flex-shrink: 0;">
37
+ <p style="margin: 0; font-size: 15px; color: #0f172a; line-height: 1.6; text-align: left;">
38
+ <strong>Important:</strong> During the tests, Reachy Mini will move. Please pay attention to the antenna movements.
39
+ </p>
40
  </div>
41
 
 
 
42
  <div class="button-group">
43
+ <button class="btn-success" onclick="validateStep(1, true)">Let's start! </button>
44
  </div>
 
 
45
  </div>
46
  </section>
47
 
48
+ <!-- Step 2: Sleep Position Setup -->
49
  <section id="step-2" class="step-screen">
50
  <div class="card">
51
+ <!-- Normal view -->
52
+ <div id="step-2-normal">
53
+ <div class="title-with-icon">
54
+ <img src="/static/assets/sleeping-reachy.svg" alt="Sleeping Reachy" class="sleeping-reachy-icon">
55
+ <h2>Make sure I'm sleeping well</h2>
56
+ </div>
57
+ <p class="instruction">Please position Reachy Mini in the sleeping position as shown below:</p>
58
+
59
+ <div style="display: flex; gap: 0; justify-content: center; margin: 20px auto; max-width: 500px; flex-wrap: wrap; overflow: hidden; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
60
+ <div style="width: 250px; height: 400px; overflow: hidden;">
61
+ <img src="/static/assets/reachy-mini-front.jpg" alt="Reachy Mini Front View" style="width: 100%; height: 100%; object-fit: cover; object-position: center;">
62
+ </div>
63
+ <div style="width: 250px; height: 400px; overflow: hidden;">
64
+ <img src="/static/assets/reachy-mini-side.jpg" alt="Reachy Mini Side View" style="width: 100%; height: 100%; object-fit: cover; object-position: center;">
65
+ </div>
66
+ </div>
67
+
68
+ <button class="btn-primary" onclick="checkSleepPosition()" id="btn-check-position">Check Position</button>
69
+
70
+ <div id="position-results" style="display: none; margin: 20px auto; max-width: 500px;"></div>
71
+
72
+ <!-- Reachy speaking bubble for normal view -->
73
+ <div class="reachy-bubble-container">
74
+ <div class="speech-bubble">
75
+ This step helps verify that no motor has been inverted during assembly
76
+ </div>
77
+ <img src="/static/assets/reachy-mini-builder.png" alt="Reachy Mini Builder" class="bubble-reachy-icon">
78
+ </div>
79
  </div>
 
 
 
 
 
80
 
81
+ <!-- Debug view -->
82
+ <div id="step-2-debug" style="display: none;">
83
+ <div class="title-with-icon">
84
+ <img src="/static/assets/reachy-mini-doctor.png" alt="Reachy Mini Doctor" style="width: 180px; height: auto;">
85
+ <h2>Motors problem</h2>
86
+ </div>
87
+ <p class="instruction" style="font-size: 16px; color: #475569; margin-bottom: 25px;">
88
+ It seems the motors are not in the correct position.<br>
89
+ Please make sure the robot is in sleep position and check again.
90
+ </p>
91
 
92
+ <div id="debug-motor-summary" style="max-width: 500px; margin: 20px auto;"></div>
93
+
94
+ <div class="button-group" style="gap: 15px;">
95
+ <button class="btn-success" onclick="recheckPosition()">Recheck Position</button>
96
+ <button class="btn-secondary" onclick="toggleAdvancedDebug()" id="btn-advanced-debug">Advanced Motor Debug</button>
97
+ </div>
98
+
99
+ <div id="debug-motor-details" style="display: none; max-width: 600px; margin: 30px auto; padding: 20px; background: #f8fafc; border-radius: 12px; border: 1px solid #cbd5e1;"></div>
100
+
101
+ <!-- Reachy speaking bubble for debug view -->
102
+ <div class="reachy-bubble-container">
103
+ <div class="speech-bubble">
104
+ The "stewart" motors control the neck mechanism
105
+ </div>
106
+ <img src="/static/assets/reachy-mini-builder.png" alt="Reachy Mini Builder" class="bubble-reachy-icon">
107
+ </div>
108
  </div>
109
 
110
+ <div class="step-indicator">Step 2 of 6</div>
111
+ <button class="btn-back" id="back-btn-2" onclick="goToPreviousStep()">← Previous step</button>
112
  </div>
113
  </section>
114
 
115
+ <!-- Step 3: Audio Input Test -->
116
  <section id="step-3" class="step-screen">
117
  <div class="card">
118
+ <!-- Normal view -->
119
+ <div id="step-3-normal">
120
+ <div class="title-with-icon">
121
+ <img src="/static/assets/sleeping-reachy.svg" alt="Sleeping Reachy" class="sleeping-reachy-icon">
122
+ <h2>Time to Wake Up!</h2>
123
+ </div>
124
+ <video class="demo-video" autoplay loop muted playsinline>
125
+ <source src="/static/assets/touch_demo.mp4" type="video/mp4">
126
+ <p style="color: #999;">Demo video not available</p>
127
+ </video>
128
 
129
+ <p class="instruction">Wake up Reachy Mini</p>
130
+
131
+ <div class="touch-progress-container">
132
+ <div class="touch-progress-bar" id="touch-progress-bar"></div>
133
+ </div>
134
+
135
+ <div class="waveform-container" id="waveform"></div>
136
+
137
+ <div class="button-group">
138
+ <button class="btn-fail" onclick="showStep3Debug()">Sound doesn't work</button>
139
+ </div>
140
  </div>
141
 
142
+ <!-- Debug view -->
143
+ <div id="step-3-debug" style="display: none;">
144
+ <div class="title-with-icon">
145
+ <img src="/static/assets/reachy-mini-doctor.png" alt="Reachy Mini Doctor" style="width: 180px; height: auto;">
146
+ <h2>Microphone problem</h2>
147
+ </div>
148
 
149
+ <!-- Waveform for testing -->
150
+ <div style="margin: 20px 0;">
151
+ <p style="font-weight: 600; margin-bottom: 10px;">Check if microphone detects sound:</p>
152
+ <div class="waveform-container" id="waveform-debug"></div>
153
+ </div>
154
+
155
+ <!-- Troubleshooting tips -->
156
+ <div style="background: #f8fafc; border-radius: 10px; padding: 20px; margin: 20px 0;">
157
+ <p style="font-weight: 600; margin-bottom: 10px;">Troubleshooting tips:</p>
158
+ <ul style="text-align: left; margin: 0; padding-left: 20px; line-height: 1.8; font-size: 14px;" id="step3-tips-list">
159
+ <li id="lite-audio-tip" style="display: none;">For Reachy Mini Lite: Check the PC audio input settings and select "Reachy Mini Audio"</li>
160
+ <li id="linux-audio-tip" style="display: none;">If running on Linux, you may need to configure audio permissions: <a href="https://huggingface.co/docs/reachy_mini/SDK/installation#-linux-users" target="_blank" style="color: #0f172a; text-decoration: underline;">see Linux setup guide</a></li>
161
+ <li>Update and reboot the robot</li>
162
+ <li>If the issue persists, check the <a href="https://huggingface.co/docs/reachy_mini/troubleshooting#audio-issues" target="_blank" style="color: #0f172a; text-decoration: underline;">FAQ</a></li>
163
+ <li>Still having issues? Write a message in the <a href="https://discord.com/invite/2bAhWfXme9" target="_blank" style="color: #0f172a; text-decoration: underline;">support channel on Discord</a></li>
164
+ </ul>
165
+ </div>
166
+
167
+ <!-- Back button -->
168
+ <div style="text-align: center;">
169
+ <button class="btn-primary" onclick="backToStep3Normal()">← Back to test</button>
170
+ </div>
171
  </div>
172
 
173
+ <div class="step-indicator" id="step-indicator">Step <span id="current-step">3</span> of 6</div>
174
+ <button class="btn-back" id="back-btn-3" onclick="goToPreviousStep()">← Previous step</button>
175
+
176
+ <!-- Reachy speaking bubble -->
177
+ <div class="reachy-bubble-container">
178
+ <div class="speech-bubble">
179
+ Let's see if my microphone is working correctly
180
+ </div>
181
+ <img src="/static/assets/reachy-mini-builder.png" alt="Reachy Mini Builder" class="bubble-reachy-icon">
182
+ </div>
183
  </div>
184
  </section>
185
 
186
+ <!-- Step 4: Motor Test -->
187
  <section id="step-4" class="step-screen">
188
  <div class="card">
189
+ <!-- Normal view -->
190
+ <div id="step-4-normal">
191
+ <div class="title-with-icon">
192
+ <img src="/static/assets/reachy-mini.png" alt="Reachy Mini" class="reachy-mini-icon">
193
+ <h2>Stretch Time!</h2>
194
+ </div>
195
+ <video class="demo-video" muted playsinline preload="auto">
196
+ <source src="/static/assets/movement_demo.mp4" type="video/mp4">
197
+ <p style="color: #999;">Demo video not available</p>
198
+ </video>
199
+ <p class="instruction">Watch the robot perform the movement</p>
200
+
201
+ <button class="btn-text" id="btn-play-movement" onclick="playMovement()">
202
+ Replay Movement
203
+ </button>
204
 
205
+ <div class="button-group" id="motor-validate" style="display: none;">
206
+ <button class="btn-fail" onclick="showStep4Debug()">Robot didn't move correctly</button>
207
+ <button class="btn-success" onclick="validateStep(4, true)">Robot did the same </button>
208
+ </div>
209
  </div>
210
 
211
+ <!-- Debug view -->
212
+ <div id="step-4-debug" style="display: none;">
213
+ <div class="title-with-icon">
214
+ <img src="/static/assets/reachy-mini-doctor.png" alt="Reachy Mini Doctor" style="width: 180px; height: auto;">
215
+ <h2>Motors problem</h2>
216
+ </div>
217
+
218
+ <!-- Antenna test section -->
219
+ <div style="margin: 20px 0;">
220
+ <p style="font-weight: 600; margin-bottom: 15px;">Test if the antennas are inverted:</p>
221
+
222
+ <div style="display: flex; gap: 20px; justify-content: center; align-items: center; margin: 30px 0;">
223
+ <!-- Right antenna button -->
224
+ <div style="text-align: center;">
225
+ <button class="btn-primary" onclick="testAntenna('right')" id="btn-test-right" style="min-width: 120px;">
226
+ Right Antenna
227
+ </button>
228
+ <p style="font-size: 12px; color: #64748b; margin-top: 8px;">Your left when facing the robot</p>
229
+ </div>
230
+
231
+
232
+ <!-- Visual diagram -->
233
+ <div style="text-align: center;">
234
+ <img src="/static/assets/reachy-mini.png" alt="Reachy Mini" style="width: 80px; height: auto;">
235
+ <p style="font-size: 11px; color: #94a3b8; margin-top: 4px;">Front view</p>
236
+ </div>
237
+ <!-- Left antenna button -->
238
+ <div style="text-align: center;">
239
+ <button class="btn-primary" onclick="testAntenna('left')" id="btn-test-left" style="min-width: 120px;">
240
+ Left Antenna
241
+ </button>
242
+ <p style="font-size: 12px; color: #64748b; margin-top: 8px;">Your right when facing the robot</p>
243
+ </div>
244
+
245
+
246
+ </div>
247
+
248
+ <div id="antenna-feedback" style="display: none; margin: 20px 0;">
249
+ <p style="color: #0f172a; font-size: 14px; text-align: center;" id="antenna-feedback-text"></p>
250
+ </div>
251
+ </div>
252
+
253
+ <!-- Troubleshooting tips -->
254
+ <div style="background: #f8fafc; border-radius: 10px; padding: 20px; margin: 20px 0;">
255
+ <p style="font-weight: 600; margin-bottom: 10px;">Troubleshooting tips:</p>
256
+ <ul style="text-align: left; margin: 0; padding-left: 20px; line-height: 1.8; font-size: 14px;">
257
+ <li>Test each antenna above to check if they are inverted</li>
258
+ <li>Update and reboot the robot</li>
259
+ <li>If the issue persists, check the <a href="https://huggingface.co/docs/reachy_mini/troubleshooting#motor-issues" target="_blank" style="color: #0f172a; text-decoration: underline;">FAQ</a></li>
260
+ <li>Still having issues? Write a message in the <a href="https://discord.com/invite/2bAhWfXme9" target="_blank" style="color: #0f172a; text-decoration: underline;">support channel on Discord</a></li>
261
+ </ul>
262
+ </div>
263
+
264
+ <!-- Back button -->
265
+ <div style="text-align: center;">
266
+ <button class="btn-primary" onclick="backToStep4Normal()">← Back to test</button>
267
+ </div>
268
  </div>
269
 
270
+ <div class="step-indicator">Step 4 of 6</div>
271
+ <button class="btn-back" id="back-btn-4" onclick="goToPreviousStep()">← Previous step</button>
272
+
273
+ <!-- Reachy speaking bubble -->
274
+ <div class="reachy-bubble-container">
275
+ <div class="speech-bubble">
276
+ This test checks if the motors work correctly and are in the right position
277
+ </div>
278
+ <img src="/static/assets/reachy-mini-builder.png" alt="Reachy Mini Builder" class="bubble-reachy-icon">
279
+ </div>
280
  </div>
281
  </section>
282
 
283
+ <!-- Step 5: Audio Output Test -->
284
  <section id="step-5" class="step-screen">
285
+ <div class="card">
286
+ <!-- Normal view -->
287
+ <div id="step-5-normal">
288
+ <div class="title-with-icon">
289
+ <img src="/static/assets/reachy-mini-sav.png" alt="Reachy Mini" class="reachy-mini-icon">
290
+ <h2>Can You Hear Me?</h2>
291
+ </div>
292
+ <p class="instruction">Listen to the robot's happy sound</p>
293
+
294
+ <div class="volume-control">
295
+ <label for="volume-slider">Volume</label>
296
+ <input type="range" id="volume-slider" min="0" max="100" value="50">
297
+ <span id="volume-value">50%</span>
298
+ </div>
299
+
300
+ <button class="btn-text" id="btn-play-sound" onclick="playHappy()">Replay Sound</button>
301
+
302
+ <div class="button-group">
303
+ <button class="btn-fail" onclick="showStep5Debug()">No sound</button>
304
+ <button class="btn-success" onclick="validateStep(5, true)">I hear the sound </button>
305
+ </div>
306
+ </div>
307
+
308
+ <!-- Debug view -->
309
+ <div id="step-5-debug" style="display: none;">
310
+ <div class="title-with-icon">
311
+ <img src="/static/assets/reachy-mini-doctor.png" alt="Reachy Mini Doctor" style="width: 180px; height: auto;">
312
+ <h2>Speaker problem</h2>
313
+ </div>
314
+
315
+ <!-- Two column layout -->
316
+ <div style="display: flex; gap: 30px; margin: 20px 0; align-items: flex-start;">
317
+ <!-- Left: Volume control -->
318
+ <div style="flex: 1;">
319
+ <p style="font-weight: 600; margin-bottom: 10px;">Adjust volume:</p>
320
+ <div class="volume-control">
321
+ <label for="volume-slider-debug">Volume</label>
322
+ <input type="range" id="volume-slider-debug" min="0" max="100" value="50">
323
+ <span id="volume-value-debug">50%</span>
324
+ </div>
325
+ </div>
326
+
327
+ <!-- Right: Record and replay -->
328
+ <div style="flex: 1;">
329
+ <p style="font-weight: 600; margin-bottom: 10px;">Test with recording:</p>
330
+ <div style="display: flex; flex-direction: column; gap: 10px;">
331
+ <button class="btn-primary" onclick="startRecordingSpeaker()" id="btn-record-speaker">Record (5s)</button>
332
+ <button class="btn-secondary" onclick="playRecordingSpeaker()" id="btn-play-speaker" disabled>Play Recording</button>
333
+ </div>
334
+ </div>
335
+ </div>
336
+
337
+ <!-- Troubleshooting tips -->
338
+ <div style="background: #f8fafc; border-radius: 10px; padding: 20px; margin: 20px 0;">
339
+ <p style="font-weight: 600; margin-bottom: 10px;">Troubleshooting tips:</p>
340
+ <ul style="text-align: left; margin: 0; padding-left: 20px; line-height: 1.8; font-size: 14px;">
341
+ <li>Check if the volume is high enough (under 50% the volume is barely audible, try with higher volume)</li>
342
+ <li>Test by recording and replaying a sound using the buttons above</li>
343
+ <li id="lite-speaker-tip" style="display: none;">For Reachy Mini Lite: Check the PC audio output settings and select "Reachy Mini Audio"</li>
344
+ <li>Update and reboot the robot</li>
345
+ <li>If the issue persists, check the <a href="https://huggingface.co/docs/reachy_mini/troubleshooting#audio-issues" target="_blank" style="color: #0f172a; text-decoration: underline;">FAQ</a></li>
346
+ <li>Still having issues? Write a message in the <a href="https://discord.com/invite/2bAhWfXme9" target="_blank" style="color: #0f172a; text-decoration: underline;">support channel on Discord</a></li>
347
+ </ul>
348
+ </div>
349
+
350
+ <!-- Back button -->
351
+ <div style="text-align: center;">
352
+ <button class="btn-primary" onclick="backToStep5Normal()">← Back to test</button>
353
+ </div>
354
+ </div>
355
+
356
+ <div class="step-indicator">Step 5 of 6</div>
357
+ <button class="btn-back" id="back-btn-5" onclick="goToPreviousStep()">← Previous step</button>
358
+
359
+ <!-- Reachy speaking bubble -->
360
+ <div class="reachy-bubble-container">
361
+ <div class="speech-bubble">
362
+ This test verifies the speaker output and volume control
363
+ </div>
364
+ <img src="/static/assets/reachy-mini-builder.png" alt="Reachy Mini Builder" class="bubble-reachy-icon">
365
+ </div>
366
+ </div>
367
+ </section>
368
+
369
+ <!-- Step 6: Camera Test -->
370
+ <section id="step-6" class="step-screen">
371
+ <div class="card">
372
+ <!-- Normal view -->
373
+ <div id="step-6-normal">
374
+ <div class="title-with-icon">
375
+ <img src="/static/assets/reachy-mini-explorer.png" alt="Reachy Mini" class="reachy-mini-icon explorer-icon">
376
+ <h2>Let's Look Around!</h2>
377
+ </div>
378
+ <p class="instruction">Check if you can see the camera feed</p>
379
+
380
+ <div class="camera-container">
381
+ <img id="camera-feed" src="/video_feed" alt="Camera feed">
382
+ </div>
383
+
384
+ <div class="button-group">
385
+ <button class="btn-fail" onclick="showStep6Debug()">Camera doesn't work</button>
386
+ <button class="btn-success" onclick="validateStep(6, true)">Camera works </button>
387
+ </div>
388
+ </div>
389
+
390
+ <!-- Debug view -->
391
+ <div id="step-6-debug" style="display: none;">
392
+ <div class="title-with-icon">
393
+ <img src="/static/assets/reachy-mini-doctor.png" alt="Reachy Mini Doctor" style="width: 180px; height: auto;">
394
+ <h2>Camera problem</h2>
395
+ </div>
396
+
397
+ <!-- Camera snapshot test -->
398
+ <div style="margin: 20px 0;">
399
+ <p style="font-weight: 600; margin-bottom: 10px;">Test camera capture:</p>
400
+ <button class="btn-primary" onclick="capturePhoto()" id="btn-capture">📷 Capture Photo</button>
401
+
402
+ <div id="captured-photo-container" style="display: none; margin-top: 20px;">
403
+ <p style="font-weight: 600; margin-bottom: 10px;">Captured image:</p>
404
+ <div class="camera-container">
405
+ <img id="captured-photo" alt="Captured photo" style="max-width: 100%; border-radius: 8px;">
406
+ </div>
407
+ </div>
408
+ </div>
409
+
410
+ <!-- Troubleshooting tips -->
411
+ <div style="background: #f8fafc; border-radius: 10px; padding: 20px; margin: 20px 0;">
412
+ <p style="font-weight: 600; margin-bottom: 10px;">Troubleshooting tips:</p>
413
+ <ul style="text-align: left; margin: 0; padding-left: 20px; line-height: 1.8; font-size: 14px;">
414
+ <li>Try capturing a photo using the button above</li>
415
+ <li>Update and reboot the robot</li>
416
+ <li>If the issue persists, check the <a href="https://huggingface.co/docs/reachy_mini/troubleshooting#camera-issues" target="_blank" style="color: #0f172a; text-decoration: underline;">FAQ</a></li>
417
+ <li>Still having issues? Write a message in the <a href="https://discord.com/invite/2bAhWfXme9" target="_blank" style="color: #0f172a; text-decoration: underline;">support channel on Discord</a></li>
418
+ </ul>
419
+ </div>
420
+
421
+ <!-- Back button -->
422
+ <div style="text-align: center;">
423
+ <button class="btn-primary" onclick="backToStep6Normal()">← Back to test</button>
424
+ </div>
425
+ </div>
426
+
427
+ <div class="step-indicator">Step 6 of 6</div>
428
+ <button class="btn-back" id="back-btn-6" onclick="goToPreviousStep()">← Previous step</button>
429
+
430
+ <!-- Reachy speaking bubble -->
431
+ <div class="reachy-bubble-container">
432
+ <div class="speech-bubble">
433
+ This test verifies that the camera is working
434
+ </div>
435
+ <img src="/static/assets/reachy-mini-builder.png" alt="Reachy Mini Builder" class="bubble-reachy-icon">
436
+ </div>
437
+ </div>
438
+ </section>
439
+
440
+ <!-- Step 7: Summary -->
441
+ <section id="step-7" class="step-screen">
442
  <div class="card">
443
  <div class="title-with-icon">
444
  <img src="/static/assets/reachy-mini-party.png" alt="Reachy Mini" class="reachy-mini-icon">
445
  <h2>We're Ready to Play!</h2>
446
  </div>
447
  <p class="instruction" style="font-size: 18px; margin: 30px 0;">Everything is working correctly</p>
448
+
449
+ <!-- Next steps -->
450
+ <div style="margin: 20px 0 40px 0;">
451
+ <p class="instruction" style="font-size: 18px; margin-bottom: 20px;">You can now explore the apps or create your own:</p>
452
+
453
+ <div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
454
+ <a href="https://huggingface.co/spaces/pollen-robotics/Reachy_Mini" target="_blank" class="btn-primary" style="display: inline-block; text-decoration: none;">
455
+ Explore Apps
456
+ </a>
457
+ <a href="https://huggingface.co/blog/pollen-robotics/make-and-publish-your-reachy-mini-apps" target="_blank" class="btn-primary" style="display: inline-block; text-decoration: none;">
458
+ Create Your Own App
459
+ </a>
460
+ </div>
461
+ </div>
462
+
463
+ <button class="btn-back" id="back-btn-7" onclick="goToPreviousStep()">← Previous step</button>
464
+
465
+ <!-- Reachy speaking bubble -->
466
+ <div class="reachy-bubble-container">
467
+ <div class="speech-bubble">
468
+ Diagnostic complete! All components are functioning correctly
469
+ </div>
470
+ <img src="/static/assets/reachy-mini-builder.png" alt="Reachy Mini Builder" class="bubble-reachy-icon">
471
+ </div>
472
  </div>
473
  </section>
474
  </main>
wake_me_up/static/main.js CHANGED
@@ -2,6 +2,7 @@ let waveformBars = [];
2
  let audioPollingInterval = null;
3
  let wasMovementPlaying = false;
4
  let lastStep = null;
 
5
 
6
  // Initialize
7
  document.addEventListener('DOMContentLoaded', () => {
@@ -47,6 +48,7 @@ async function fetchAudioLevel() {
47
  const resp = await fetch('/api/audio_level', { cache: 'no-store' });
48
  const data = await resp.json();
49
  updateWaveform(data.audio_level);
 
50
  updateTouchDetection(data.touch_progress, data.touch_detected);
51
  } catch (err) {
52
  console.error('Audio fetch failed:', err);
@@ -90,27 +92,35 @@ function setButtonsDisabled(selector, disabled) {
90
 
91
  // UI Updates
92
  function updateUI(state) {
 
 
 
93
  // Update current step number in step 1 indicator
94
  const currentStepSpan = document.getElementById('current-step');
95
  if (currentStepSpan) {
96
  currentStepSpan.textContent = state.current_step;
97
  }
98
 
 
 
 
 
 
99
  document.querySelectorAll('.step-screen').forEach(s => s.classList.remove('active'));
100
  document.getElementById('step-' + state.current_step).classList.add('active');
101
 
102
  // Start/stop fast audio polling based on step
103
- if (state.current_step === 1) {
104
  startAudioPolling();
105
  } else {
106
  stopAudioPolling();
107
  }
108
 
109
- if (state.current_step === 2) {
110
  const btn = document.getElementById('btn-play-movement');
111
  const validateDiv = document.getElementById('motor-validate');
112
  const validateButtons = document.querySelectorAll('#motor-validate button');
113
- const video = document.querySelector('#step-2 .demo-video');
114
 
115
  // Detect when movement starts playing (transition from false to true)
116
  if (state.movement_playing && !wasMovementPlaying) {
@@ -154,7 +164,7 @@ function updateUI(state) {
154
  wasMovementPlaying = state.movement_playing;
155
  }
156
 
157
- if (state.current_step === 3) {
158
  // Update button text based on whether emotion has been played
159
  const btn = document.getElementById('btn-play-sound');
160
  if (btn && !state.is_first_emotion) {
@@ -171,10 +181,10 @@ function updateUI(state) {
171
  }
172
 
173
  // Disable validation buttons while emotion is playing
174
- setButtonsDisabled('#step-3 .button-group button, #step-3 .button-group a', state.emotion_playing);
175
  }
176
 
177
- if (state.current_step === 4 && lastStep !== 4) {
178
  // Reload camera feed only once when entering step 4 to prevent frozen image
179
  const cameraFeed = document.getElementById('camera-feed');
180
  if (cameraFeed) {
@@ -197,9 +207,12 @@ function updateUI(state) {
197
  }
198
  }
199
 
200
- if (state.current_step === 4) {
 
 
 
201
  // Disable validation buttons while curious emotion is playing
202
- setButtonsDisabled('#step-4 .button-group button, #step-4 .button-group a', state.emotion_playing);
203
  }
204
 
205
  lastStep = state.current_step;
@@ -305,24 +318,759 @@ function updateWaveform(audioLevel) {
305
  });
306
  }
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  function setupVolumeSlider() {
 
 
309
  const slider = document.getElementById('volume-slider');
310
- if (!slider) return;
311
-
312
- slider.addEventListener('input', async (e) => {
313
- const value = parseInt(e.target.value);
314
- document.getElementById('volume-value').textContent = value + '%';
315
-
316
- try {
317
- await fetch('/api/set_volume', {
318
- method: 'POST',
319
- headers: { 'Content-Type': 'application/json' },
320
- body: JSON.stringify({ volume: value / 100 })
321
- });
322
- } catch (err) {
323
- console.error('Set volume failed:', err);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  }
325
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  }
327
 
328
 
 
2
  let audioPollingInterval = null;
3
  let wasMovementPlaying = false;
4
  let lastStep = null;
5
+ let currentStep = 1; // Track current step globally
6
 
7
  // Initialize
8
  document.addEventListener('DOMContentLoaded', () => {
 
48
  const resp = await fetch('/api/audio_level', { cache: 'no-store' });
49
  const data = await resp.json();
50
  updateWaveform(data.audio_level);
51
+ updateDebugWaveform(data.audio_level); // Also update debug waveform
52
  updateTouchDetection(data.touch_progress, data.touch_detected);
53
  } catch (err) {
54
  console.error('Audio fetch failed:', err);
 
92
 
93
  // UI Updates
94
  function updateUI(state) {
95
+ // Update global current step variable
96
+ currentStep = state.current_step;
97
+
98
  // Update current step number in step 1 indicator
99
  const currentStepSpan = document.getElementById('current-step');
100
  if (currentStepSpan) {
101
  currentStepSpan.textContent = state.current_step;
102
  }
103
 
104
+ // Reset step 2 view when navigating to it
105
+ if (state.current_step === 2 && lastStep !== 2) {
106
+ resetStep2View();
107
+ }
108
+
109
  document.querySelectorAll('.step-screen').forEach(s => s.classList.remove('active'));
110
  document.getElementById('step-' + state.current_step).classList.add('active');
111
 
112
  // Start/stop fast audio polling based on step
113
+ if (state.current_step === 3) {
114
  startAudioPolling();
115
  } else {
116
  stopAudioPolling();
117
  }
118
 
119
+ if (state.current_step === 4) {
120
  const btn = document.getElementById('btn-play-movement');
121
  const validateDiv = document.getElementById('motor-validate');
122
  const validateButtons = document.querySelectorAll('#motor-validate button');
123
+ const video = document.querySelector('#step-4 .demo-video');
124
 
125
  // Detect when movement starts playing (transition from false to true)
126
  if (state.movement_playing && !wasMovementPlaying) {
 
164
  wasMovementPlaying = state.movement_playing;
165
  }
166
 
167
+ if (state.current_step === 5) {
168
  // Update button text based on whether emotion has been played
169
  const btn = document.getElementById('btn-play-sound');
170
  if (btn && !state.is_first_emotion) {
 
181
  }
182
 
183
  // Disable validation buttons while emotion is playing
184
+ setButtonsDisabled('#step-5 .button-group button, #step-5 .button-group a', state.emotion_playing);
185
  }
186
 
187
+ if (state.current_step === 6 && lastStep !== 6) {
188
  // Reload camera feed only once when entering step 4 to prevent frozen image
189
  const cameraFeed = document.getElementById('camera-feed');
190
  if (cameraFeed) {
 
207
  }
208
  }
209
 
210
+ // Update back button state
211
+ updateBackButtonState(state);
212
+
213
+ if (state.current_step === 6) {
214
  // Disable validation buttons while curious emotion is playing
215
+ setButtonsDisabled('#step-6 .button-group button, #step-6 .button-group a', state.emotion_playing);
216
  }
217
 
218
  lastStep = state.current_step;
 
318
  });
319
  }
320
 
321
+ // Update debug waveform visualization (same logic but for debug container)
322
+ function updateDebugWaveform(audioLevel) {
323
+ const container = document.getElementById('waveform-debug');
324
+ if (!container) return;
325
+
326
+ const bars = container.querySelectorAll('.waveform-bar');
327
+ if (bars.length === 0) return;
328
+
329
+ const maxHeight = 120;
330
+ const minHeight = 4;
331
+ const baseLevel = Math.min(audioLevel / 100, 1);
332
+
333
+ bars.forEach((bar, index) => {
334
+ const position = index / bars.length;
335
+ const centerDistance = Math.abs(position - 0.5) * 2;
336
+ const envelope = 1 - Math.pow(centerDistance, 2);
337
+
338
+ const randomness = Math.random() * 0.4 + 0.8;
339
+ const height = minHeight + (maxHeight - minHeight) * baseLevel * envelope * randomness;
340
+
341
+ bar.style.height = height + 'px';
342
+ });
343
+ }
344
+
345
  function setupVolumeSlider() {
346
+ let volumeTimeout = null;
347
+
348
  const slider = document.getElementById('volume-slider');
349
+ if (slider) {
350
+ slider.addEventListener('input', async (e) => {
351
+ const value = parseInt(e.target.value);
352
+ const valueSpan = document.getElementById('volume-value');
353
+ if (valueSpan) {
354
+ valueSpan.textContent = value + '%';
355
+ }
356
+
357
+ try {
358
+ await fetch('/api/set_volume', {
359
+ method: 'POST',
360
+ headers: { 'Content-Type': 'application/json' },
361
+ body: JSON.stringify({ volume: value / 100 })
362
+ });
363
+
364
+ // Play a short test sound when slider stops moving (debounced)
365
+ clearTimeout(volumeTimeout);
366
+ volumeTimeout = setTimeout(() => {
367
+ fetch('/api/play_test_sound', { method: 'POST' }).catch(err =>
368
+ console.error('Volume test sound failed:', err)
369
+ );
370
+ }, 500); // Wait 500ms after user stops moving slider
371
+ } catch (err) {
372
+ console.error('Set volume failed:', err);
373
+ }
374
+ });
375
+ }
376
+
377
+ // Setup debug volume slider if it exists
378
+ const debugSlider = document.getElementById('volume-slider-debug');
379
+ if (debugSlider) {
380
+ debugSlider.addEventListener('input', async (e) => {
381
+ const value = parseInt(e.target.value);
382
+ const valueSpan = document.getElementById('volume-value-debug');
383
+ if (valueSpan) {
384
+ valueSpan.textContent = value + '%';
385
+ }
386
+
387
+ try {
388
+ await fetch('/api/set_volume', {
389
+ method: 'POST',
390
+ headers: { 'Content-Type': 'application/json' },
391
+ body: JSON.stringify({ volume: value / 100 })
392
+ });
393
+
394
+ // Play a short test sound when slider stops moving (debounced)
395
+ clearTimeout(volumeTimeout);
396
+ volumeTimeout = setTimeout(() => {
397
+ fetch('/api/play_test_sound', { method: 'POST' }).catch(err =>
398
+ console.error('Volume test sound failed:', err)
399
+ );
400
+ }, 500); // Wait 500ms after user stops moving slider
401
+ } catch (err) {
402
+ console.error('Set volume failed:', err);
403
+ }
404
+ });
405
+ }
406
+ }
407
+
408
+ // Back button functionality
409
+ function updateBackButtonState(state) {
410
+ // Check if robot is busy (moving or playing emotion)
411
+ const isRobotBusy = state.movement_busy || state.emotion_playing;
412
+
413
+ // Update each back button based on current step
414
+ for (let step = 1; step <= 7; step++) {
415
+ const backBtn = document.getElementById(`back-btn-${step}`);
416
+ if (!backBtn) continue;
417
+
418
+ // Show button only on the current step (and not on step 0 or step 1)
419
+ if (state.current_step === step && step > 1) {
420
+ backBtn.style.display = 'block';
421
+ // Disable if robot is busy
422
+ backBtn.disabled = isRobotBusy;
423
+ } else {
424
+ backBtn.style.display = 'none';
425
  }
426
+ }
427
+ }
428
+
429
+ async function checkSleepPosition() {
430
+ try {
431
+ const btn = document.getElementById('btn-check-position');
432
+ btn.disabled = true;
433
+ btn.textContent = 'Checking...';
434
+
435
+ const response = await fetch('/api/check_sleep_position', {
436
+ method: 'POST'
437
+ });
438
+
439
+ const result = await response.json();
440
+
441
+ if (result.status === "ok") {
442
+ if (result.validation.all_ok) {
443
+ // All OK - show brief success message and advance to next step
444
+ displayPositionResults(result.validation);
445
+ setTimeout(() => {
446
+ validateStep(2, true);
447
+ }, 1000);
448
+ } else {
449
+ // Has errors - switch to debug view
450
+ showDebugView(result.validation);
451
+ }
452
+ } else {
453
+ alert('Failed to check position: ' + (result.message || 'Unknown error'));
454
+ btn.disabled = false;
455
+ btn.textContent = 'Check Position';
456
+ }
457
+ } catch (err) {
458
+ console.error('Check position failed:', err);
459
+ alert('Failed to check position');
460
+ const btn = document.getElementById('btn-check-position');
461
+ btn.disabled = false;
462
+ btn.textContent = 'Check Position';
463
+ }
464
+ }
465
+
466
+ function displayPositionResults(validation) {
467
+ const container = document.getElementById('position-results');
468
+
469
+ // Only show success message if all OK (motors list not shown)
470
+ let html = '<div style="text-align: center;">';
471
+ html += '<p style="color: #0f172a; font-weight: 600; font-size: 18px; margin: 20px 0;">✓ All motors in correct position!</p>';
472
+ html += '<p style="color: #64748b; font-size: 14px;">Advancing to next step...</p>';
473
+ html += '</div>';
474
+
475
+ container.innerHTML = html;
476
+ container.style.display = 'block';
477
+ }
478
+
479
+ let currentValidation = null;
480
+
481
+ function showDebugView(validation) {
482
+ // Store validation for advanced debug
483
+ currentValidation = validation;
484
+
485
+ // Hide normal view
486
+ document.getElementById('step-2-normal').style.display = 'none';
487
+
488
+ // Show debug view
489
+ const debugView = document.getElementById('step-2-debug');
490
+ debugView.style.display = 'block';
491
+
492
+ // Create summary
493
+ const motorSummary = document.getElementById('debug-motor-summary');
494
+ let html = '<div style="text-align: center;">';
495
+
496
+ // Show detected swaps prominently if any
497
+ if (validation.has_swaps && validation.detected_swaps.length > 0) {
498
+ html += '<div style="background: #f8fafc; border-radius: 10px; padding: 20px; margin-bottom: 20px;">';
499
+ html += '<p style="font-weight: 600; font-size: 14px; color: #0f172a; margin-bottom: 15px;">Possible motor inversions detected:</p>';
500
+
501
+ validation.detected_swaps.forEach(swap => {
502
+ const nameA = swap.motor_a.replace(/_/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
503
+ const nameB = swap.motor_b.replace(/_/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
504
+
505
+ html += `<div style="padding: 12px; background: white; border-radius: 8px; margin: 10px 0; border: 1px solid #e2e8f0; font-size: 14px; color: #475569;">`;
506
+ html += `${nameA} <span style="font-weight: 600; color: #0f172a;">↔</span> ${nameB}`;
507
+ html += `</div>`;
508
+ });
509
+
510
+ html += '</div>';
511
+ } else {
512
+ // No swaps detected, show motors with errors
513
+ const errorMotors = validation.motors.filter(m => m.status === 'error');
514
+ html += '<p style="font-weight: 600; margin-bottom: 15px; color: #0f172a; font-size: 14px;">Motors that need adjustment:</p>';
515
+ html += '<div style="background: #f8fafc; border-radius: 10px; padding: 15px;">';
516
+
517
+ errorMotors.forEach(motor => {
518
+ const displayName = motor.name.replace(/_/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
519
+ html += `<div style="padding: 10px; margin: 8px 0; background: white; border-radius: 6px; color: #475569; font-size: 14px; border: 1px solid #e2e8f0;">`;
520
+ html += `${displayName}`;
521
+ html += `</div>`;
522
+ });
523
+
524
+ html += '</div>';
525
+ }
526
+
527
+ html += '</div>';
528
+ motorSummary.innerHTML = html;
529
+
530
+ // Reset advanced debug view
531
+ document.getElementById('debug-motor-details').style.display = 'none';
532
+ document.getElementById('btn-advanced-debug').textContent = 'Advanced Motor Debug';
533
+ }
534
+
535
+ function recheckPosition() {
536
+ // Reset to normal view and trigger check again
537
+ backToPositionSetup();
538
+ setTimeout(() => {
539
+ checkSleepPosition();
540
+ }, 300);
541
+ }
542
+
543
+ function toggleAdvancedDebug() {
544
+ const detailsDiv = document.getElementById('debug-motor-details');
545
+ const btn = document.getElementById('btn-advanced-debug');
546
+
547
+ if (detailsDiv.style.display === 'none') {
548
+ // Show advanced debug
549
+ let html = '<h3 style="margin-top: 0; color: #0f172a; font-size: 16px;">Detailed Motor Positions</h3>';
550
+ html += '<div style="font-size: 13px;">';
551
+
552
+ currentValidation.motors.forEach(motor => {
553
+ const statusColor = motor.status === 'ok' ? '#065f46' : '#991b1b';
554
+ const statusIcon = motor.status === 'ok' ? '✓' : '✗';
555
+ const bgColor = motor.status === 'ok' ? '#d1fae5' : '#fee2e2';
556
+
557
+ html += `<div style="padding: 12px; margin: 8px 0; border-radius: 8px; background: ${bgColor};">`;
558
+ html += `<div style="font-weight: 600; color: ${statusColor}; margin-bottom: 8px;">`;
559
+ html += `${statusIcon} ${motor.name.replace(/_/g, ' ').toUpperCase()}`;
560
+ html += `</div>`;
561
+ html += `<div style="color: #64748b; font-size: 12px; line-height: 1.6;">`;
562
+ html += `Expected: ${motor.expected}° | `;
563
+ html += `Current: ${motor.actual}° | `;
564
+ html += `Diff: ${motor.diff}° (max: ${motor.threshold}°)`;
565
+ html += `</div>`;
566
+ html += `</div>`;
567
+ });
568
+
569
+ html += '</div>';
570
+ detailsDiv.innerHTML = html;
571
+ detailsDiv.style.display = 'block';
572
+ btn.textContent = 'Hide Advanced Debug';
573
+ } else {
574
+ // Hide advanced debug
575
+ detailsDiv.style.display = 'none';
576
+ btn.textContent = 'Advanced Motor Debug';
577
+ }
578
+ }
579
+
580
+ // Step 3 Debug functions
581
+ let recordedAudio = null;
582
+
583
+ async function showStep3Debug() {
584
+ // Hide normal view
585
+ document.getElementById('step-3-normal').style.display = 'none';
586
+
587
+ // Show debug view
588
+ document.getElementById('step-3-debug').style.display = 'block';
589
+
590
+ // Hide bubble
591
+ const bubble = document.querySelector('#step-3 .reachy-bubble-container');
592
+ if (bubble) bubble.style.visibility = 'hidden';
593
+
594
+ // Set debug mode flag to disable auto-advance
595
+ try {
596
+ await fetch('/api/set_debug_mode', {
597
+ method: 'POST',
598
+ headers: { 'Content-Type': 'application/json' },
599
+ body: JSON.stringify({ debug_mode: true })
600
+ });
601
+ } catch (err) {
602
+ console.error('Failed to set debug mode:', err);
603
+ }
604
+
605
+ // Check if Lite and show Lite-specific tips
606
+ try {
607
+ const response = await fetch('/api/robot_variant');
608
+ const result = await response.json();
609
+ if (result.status === 'ok' && result.variant === 'lite') {
610
+ const liteAudioTip = document.getElementById('lite-audio-tip');
611
+ if (liteAudioTip) liteAudioTip.style.display = 'list-item';
612
+ }
613
+ } catch (err) {
614
+ console.error('Failed to get robot variant:', err);
615
+ }
616
+
617
+ // Check if Linux and show Linux-specific tip
618
+ const isLinux = navigator.userAgent.toLowerCase().includes('linux');
619
+ if (isLinux) {
620
+ const linuxTip = document.getElementById('linux-audio-tip');
621
+ if (linuxTip) linuxTip.style.display = 'list-item';
622
+ }
623
+
624
+ // Initialize debug waveform
625
+ initDebugWaveform();
626
+ startAudioPolling();
627
+ }
628
+
629
+ async function backToStep3Normal() {
630
+ document.getElementById('step-3-normal').style.display = 'block';
631
+ document.getElementById('step-3-debug').style.display = 'none';
632
+ recordedAudio = null;
633
+
634
+ // Show bubble again
635
+ const bubble = document.querySelector('#step-3 .reachy-bubble-container');
636
+ if (bubble) bubble.style.visibility = 'visible';
637
+
638
+ // Disable debug mode flag to re-enable auto-advance
639
+ try {
640
+ await fetch('/api/set_debug_mode', {
641
+ method: 'POST',
642
+ headers: { 'Content-Type': 'application/json' },
643
+ body: JSON.stringify({ debug_mode: false })
644
+ });
645
+ } catch (err) {
646
+ console.error('Failed to clear debug mode:', err);
647
+ }
648
+
649
+ // Hide conditional tips
650
+ const liteAudioTip = document.getElementById('lite-audio-tip');
651
+ const linuxTip = document.getElementById('linux-audio-tip');
652
+ if (liteAudioTip) liteAudioTip.style.display = 'none';
653
+ if (linuxTip) linuxTip.style.display = 'none';
654
+ }
655
+
656
+ function initDebugWaveform() {
657
+ const container = document.getElementById('waveform-debug');
658
+ if (!container || container.children.length > 0) return;
659
+
660
+ const barCount = 50;
661
+ for (let i = 0; i < barCount; i++) {
662
+ const bar = document.createElement('div');
663
+ bar.className = 'waveform-bar';
664
+ bar.style.height = '4px';
665
+ container.appendChild(bar);
666
+ }
667
+ }
668
+
669
+ async function testSpeaker() {
670
+ try {
671
+ const btn = event.target;
672
+ btn.disabled = true;
673
+ btn.textContent = 'Playing...';
674
+
675
+ await fetch('/api/play_happy', { method: 'POST' });
676
+
677
+ setTimeout(() => {
678
+ btn.disabled = false;
679
+ btn.textContent = 'Test Speaker';
680
+ }, 3000);
681
+ } catch (err) {
682
+ console.error('Test speaker failed:', err);
683
+ alert('Failed to test speaker');
684
+ }
685
+ }
686
+
687
+ async function startRecording() {
688
+ try {
689
+ const recordBtn = document.getElementById('btn-record');
690
+ const playBtn = document.getElementById('btn-play');
691
+
692
+ // Disable buttons during recording
693
+ recordBtn.disabled = true;
694
+ playBtn.disabled = true;
695
+ recordBtn.textContent = 'Recording...';
696
+
697
+ // Start recording (5 seconds)
698
+ const response = await fetch('/api/start_recording', {
699
+ method: 'POST',
700
+ headers: { 'Content-Type': 'application/json' },
701
+ body: JSON.stringify({ duration: 5.0 })
702
+ });
703
+
704
+ const result = await response.json();
705
+ if (result.status === 'recording') {
706
+ // Poll for recording completion
707
+ let countdown = 5;
708
+ const countdownInterval = setInterval(() => {
709
+ countdown--;
710
+ recordBtn.textContent = `Recording (${countdown}s)`;
711
+ if (countdown <= 0) {
712
+ clearInterval(countdownInterval);
713
+ }
714
+ }, 1000);
715
+
716
+ // Wait for recording to complete (5s + buffer)
717
+ setTimeout(async () => {
718
+ // Check if recording is complete
719
+ const statusResp = await fetch('/api/recording_status');
720
+ const statusResult = await statusResp.json();
721
+
722
+ recordBtn.disabled = false;
723
+ recordBtn.textContent = 'Record (5s)';
724
+
725
+ if (statusResult.has_recording) {
726
+ playBtn.disabled = false;
727
+ recordedAudio = true; // Mark that we have a recording
728
+ }
729
+ }, 5500);
730
+ }
731
+ } catch (err) {
732
+ console.error('Recording failed:', err);
733
+ alert('Failed to start recording');
734
+ const recordBtn = document.getElementById('btn-record');
735
+ recordBtn.disabled = false;
736
+ recordBtn.textContent = 'Record (5s)';
737
+ }
738
+ }
739
+
740
+ async function playRecording() {
741
+ try {
742
+ const playBtn = document.getElementById('btn-play');
743
+ playBtn.disabled = true;
744
+ playBtn.textContent = 'Playing...';
745
+
746
+ await fetch('/api/play_recording', { method: 'POST' });
747
+
748
+ // Re-enable after playback (estimate 5s)
749
+ setTimeout(() => {
750
+ playBtn.disabled = false;
751
+ playBtn.textContent = 'Play Recording';
752
+ }, 5500);
753
+ } catch (err) {
754
+ console.error('Playback failed:', err);
755
+ alert('Failed to play recording');
756
+ const playBtn = document.getElementById('btn-play');
757
+ playBtn.disabled = false;
758
+ playBtn.textContent = 'Play Recording';
759
+ }
760
+ }
761
+
762
+ function resetStep2View() {
763
+ // Show normal view
764
+ const normalView = document.getElementById('step-2-normal');
765
+ if (normalView) normalView.style.display = 'block';
766
+
767
+ // Hide debug view
768
+ const debugView = document.getElementById('step-2-debug');
769
+ if (debugView) debugView.style.display = 'none';
770
+
771
+ // Reset button
772
+ const btn = document.getElementById('btn-check-position');
773
+ if (btn) {
774
+ btn.disabled = false;
775
+ btn.textContent = 'Check Position';
776
+ }
777
+
778
+ // Hide results
779
+ const results = document.getElementById('position-results');
780
+ if (results) results.style.display = 'none';
781
+
782
+ // Hide advanced debug
783
+ const details = document.getElementById('debug-motor-details');
784
+ if (details) details.style.display = 'none';
785
+ }
786
+
787
+ function backToPositionSetup() {
788
+ resetStep2View();
789
+ }
790
+
791
+ // Step 4 Debug functions
792
+ async function showStep4Debug() {
793
+ // Hide normal view
794
+ document.getElementById('step-4-normal').style.display = 'none';
795
+
796
+ // Show debug view
797
+ document.getElementById('step-4-debug').style.display = 'block';
798
+
799
+ // Hide bubble
800
+ const bubble = document.querySelector('#step-4 .reachy-bubble-container');
801
+ if (bubble) bubble.style.visibility = 'hidden';
802
+ }
803
+
804
+ function backToStep4Normal() {
805
+ document.getElementById('step-4-normal').style.display = 'block';
806
+ document.getElementById('step-4-debug').style.display = 'none';
807
+
808
+ // Show bubble again
809
+ const bubble = document.querySelector('#step-4 .reachy-bubble-container');
810
+ if (bubble) bubble.style.visibility = 'visible';
811
+
812
+ // Hide feedback
813
+ const feedback = document.getElementById('antenna-feedback');
814
+ if (feedback) feedback.style.display = 'none';
815
+ }
816
+
817
+ async function testAntenna(side) {
818
+ try {
819
+ const leftBtn = document.getElementById('btn-test-left');
820
+ const rightBtn = document.getElementById('btn-test-right');
821
+ const feedback = document.getElementById('antenna-feedback');
822
+ const feedbackText = document.getElementById('antenna-feedback-text');
823
+
824
+ // Disable both buttons during test
825
+ leftBtn.disabled = true;
826
+ rightBtn.disabled = true;
827
+
828
+ // Update button text
829
+ if (side === 'left') {
830
+ leftBtn.textContent = 'Testing...';
831
+ } else {
832
+ rightBtn.textContent = 'Testing...';
833
+ }
834
+
835
+ // Show feedback
836
+ feedback.style.display = 'block';
837
+ const sideName = side === 'left' ? 'LEFT' : 'RIGHT';
838
+ feedbackText.textContent = `The ${sideName} antenna should shake now...`;
839
+
840
+ // Call API to test antenna
841
+ await fetch('/api/test_antenna', {
842
+ method: 'POST',
843
+ headers: { 'Content-Type': 'application/json' },
844
+ body: JSON.stringify({ side: side })
845
+ });
846
+
847
+ // Wait for movement to complete (estimated 2.8 seconds)
848
+ setTimeout(() => {
849
+ leftBtn.disabled = false;
850
+ rightBtn.disabled = false;
851
+ leftBtn.textContent = 'Left Antenna';
852
+ rightBtn.textContent = 'Right Antenna';
853
+
854
+ feedbackText.textContent = `If the wrong antenna moved, they may have been swapped during assembly.`;
855
+ }, 2800);
856
+
857
+ } catch (err) {
858
+ console.error('Antenna test failed:', err);
859
+ alert('Failed to test antenna');
860
+
861
+ // Re-enable buttons
862
+ document.getElementById('btn-test-left').disabled = false;
863
+ document.getElementById('btn-test-right').disabled = false;
864
+ document.getElementById('btn-test-left').textContent = 'Left Antenna';
865
+ document.getElementById('btn-test-right').textContent = 'Right Antenna';
866
+ }
867
+ }
868
+
869
+ // Step 5 Debug functions
870
+ async function showStep5Debug() {
871
+ // Hide normal view
872
+ document.getElementById('step-5-normal').style.display = 'none';
873
+
874
+ // Show debug view
875
+ document.getElementById('step-5-debug').style.display = 'block';
876
+
877
+ // Hide bubble
878
+ const bubble = document.querySelector('#step-5 .reachy-bubble-container');
879
+ if (bubble) bubble.style.visibility = 'hidden';
880
+
881
+ // Check if Lite and show tip
882
+ try {
883
+ const response = await fetch('/api/robot_variant');
884
+ const result = await response.json();
885
+ if (result.status === 'ok' && result.variant === 'lite') {
886
+ const liteTip = document.getElementById('lite-speaker-tip');
887
+ if (liteTip) liteTip.style.display = 'list-item';
888
+ }
889
+ } catch (err) {
890
+ console.error('Failed to get robot variant:', err);
891
+ }
892
+
893
+ // Sync debug slider with current volume
894
+ const normalSlider = document.getElementById('volume-slider');
895
+ const debugSlider = document.getElementById('volume-slider-debug');
896
+ const debugValue = document.getElementById('volume-value-debug');
897
+ if (normalSlider && debugSlider) {
898
+ debugSlider.value = normalSlider.value;
899
+ debugValue.textContent = normalSlider.value + '%';
900
+ }
901
+ }
902
+
903
+ function backToStep5Normal() {
904
+ document.getElementById('step-5-normal').style.display = 'block';
905
+ document.getElementById('step-5-debug').style.display = 'none';
906
+
907
+ // Show bubble again
908
+ const bubble = document.querySelector('#step-5 .reachy-bubble-container');
909
+ if (bubble) bubble.style.visibility = 'visible';
910
+
911
+ // Hide Lite tip
912
+ const liteTip = document.getElementById('lite-speaker-tip');
913
+ if (liteTip) liteTip.style.display = 'none';
914
+ }
915
+
916
+ async function startRecordingSpeaker() {
917
+ try {
918
+ const recordBtn = document.getElementById('btn-record-speaker');
919
+ const playBtn = document.getElementById('btn-play-speaker');
920
+
921
+ // Disable buttons during recording
922
+ recordBtn.disabled = true;
923
+ playBtn.disabled = true;
924
+ recordBtn.textContent = 'Recording...';
925
+
926
+ // Start recording (5 seconds)
927
+ const response = await fetch('/api/start_recording', {
928
+ method: 'POST',
929
+ headers: { 'Content-Type': 'application/json' },
930
+ body: JSON.stringify({ duration: 5.0 })
931
+ });
932
+
933
+ const result = await response.json();
934
+ if (result.status === 'recording') {
935
+ // Poll for recording completion
936
+ let countdown = 5;
937
+ const countdownInterval = setInterval(() => {
938
+ countdown--;
939
+ recordBtn.textContent = `Recording (${countdown}s)`;
940
+ if (countdown <= 0) {
941
+ clearInterval(countdownInterval);
942
+ }
943
+ }, 1000);
944
+
945
+ // Wait for recording to complete (5s + buffer)
946
+ setTimeout(async () => {
947
+ // Check if recording is complete
948
+ const statusResp = await fetch('/api/recording_status');
949
+ const statusResult = await statusResp.json();
950
+
951
+ recordBtn.disabled = false;
952
+ recordBtn.textContent = 'Record (5s)';
953
+
954
+ if (statusResult.has_recording) {
955
+ playBtn.disabled = false;
956
+ }
957
+ }, 5500);
958
+ }
959
+ } catch (err) {
960
+ console.error('Recording failed:', err);
961
+ alert('Failed to start recording');
962
+ const recordBtn = document.getElementById('btn-record-speaker');
963
+ recordBtn.disabled = false;
964
+ recordBtn.textContent = 'Record (5s)';
965
+ }
966
+ }
967
+
968
+ async function playRecordingSpeaker() {
969
+ try {
970
+ const playBtn = document.getElementById('btn-play-speaker');
971
+ playBtn.disabled = true;
972
+ playBtn.textContent = 'Playing...';
973
+
974
+ await fetch('/api/play_recording', { method: 'POST' });
975
+
976
+ // Re-enable after playback (estimate 5s)
977
+ setTimeout(() => {
978
+ playBtn.disabled = false;
979
+ playBtn.textContent = 'Play Recording';
980
+ }, 5500);
981
+ } catch (err) {
982
+ console.error('Playback failed:', err);
983
+ alert('Failed to play recording');
984
+ const playBtn = document.getElementById('btn-play-speaker');
985
+ playBtn.disabled = false;
986
+ playBtn.textContent = 'Play Recording';
987
+ }
988
+ }
989
+
990
+ // Step 6 Debug functions
991
+ async function showStep6Debug() {
992
+ // Hide normal view
993
+ document.getElementById('step-6-normal').style.display = 'none';
994
+
995
+ // Show debug view
996
+ document.getElementById('step-6-debug').style.display = 'block';
997
+
998
+ // Hide bubble
999
+ const bubble = document.querySelector('#step-6 .reachy-bubble-container');
1000
+ if (bubble) bubble.style.visibility = 'hidden';
1001
+ }
1002
+
1003
+ function backToStep6Normal() {
1004
+ document.getElementById('step-6-normal').style.display = 'block';
1005
+ document.getElementById('step-6-debug').style.display = 'none';
1006
+
1007
+ // Show bubble again
1008
+ const bubble = document.querySelector('#step-6 .reachy-bubble-container');
1009
+ if (bubble) bubble.style.visibility = 'visible';
1010
+
1011
+ // Hide captured photo
1012
+ const photoContainer = document.getElementById('captured-photo-container');
1013
+ if (photoContainer) photoContainer.style.display = 'none';
1014
+ }
1015
+
1016
+ async function capturePhoto() {
1017
+ try {
1018
+ const btn = document.getElementById('btn-capture');
1019
+ const photoContainer = document.getElementById('captured-photo-container');
1020
+ const photoImg = document.getElementById('captured-photo');
1021
+
1022
+ btn.disabled = true;
1023
+ btn.textContent = 'Capturing...';
1024
+
1025
+ const response = await fetch('/api/capture_photo');
1026
+ const result = await response.json();
1027
+
1028
+ if (result.status === 'ok') {
1029
+ // Display the captured photo
1030
+ photoImg.src = result.image;
1031
+ photoContainer.style.display = 'block';
1032
+ } else {
1033
+ alert('Failed to capture photo: ' + (result.message || 'Unknown error'));
1034
+ }
1035
+
1036
+ btn.disabled = false;
1037
+ btn.textContent = 'Capture Photo';
1038
+
1039
+ } catch (err) {
1040
+ console.error('Photo capture failed:', err);
1041
+ alert('Failed to capture photo');
1042
+
1043
+ const btn = document.getElementById('btn-capture');
1044
+ btn.disabled = false;
1045
+ btn.textContent = 'Capture Photo';
1046
+ }
1047
+ }
1048
+
1049
+ async function goToPreviousStep() {
1050
+ try {
1051
+ // Use global currentStep variable
1052
+ if (currentStep <= 1) return; // Can't go back from step 1
1053
+
1054
+ const previousStep = currentStep - 1;
1055
+
1056
+ // Use the new go_to_step endpoint for backward navigation
1057
+ const response = await fetch('/api/go_to_step', {
1058
+ method: 'POST',
1059
+ headers: { 'Content-Type': 'application/json' },
1060
+ body: JSON.stringify({ step: previousStep })
1061
+ });
1062
+
1063
+ const result = await response.json();
1064
+ if (result.status === "error") {
1065
+ console.error('Go to previous step failed:', result.message);
1066
+ } else {
1067
+ // Refresh state immediately
1068
+ await fetchState();
1069
+ }
1070
+
1071
+ } catch (err) {
1072
+ console.error('Go to previous step failed:', err);
1073
+ }
1074
  }
1075
 
1076
 
wake_me_up/static/style.css CHANGED
@@ -29,7 +29,7 @@ body {
29
  .step-indicator {
30
  position: absolute;
31
  bottom: 20px;
32
- left: 20px;
33
  font-size: 13px;
34
  font-weight: 500;
35
  color: #64748b;
@@ -249,7 +249,8 @@ body {
249
 
250
  .btn-primary,
251
  .btn-success,
252
- .btn-fail {
 
253
  padding: 12px 24px;
254
  border-radius: 8px;
255
  font-size: 14px;
@@ -287,6 +288,20 @@ body {
287
  color: #FF9900;
288
  }
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  /* Text link style for secondary actions */
291
  .btn-text {
292
  background: transparent;
@@ -307,6 +322,7 @@ body {
307
 
308
  .btn-primary:disabled,
309
  .btn-success:disabled,
 
310
  .btn-text:disabled {
311
  background: #f3f4f6;
312
  color: #9ca3af;
@@ -314,6 +330,33 @@ body {
314
  opacity: 0.6;
315
  }
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  /* Summary */
318
  .summary-list {
319
  list-style: none;
@@ -367,6 +410,66 @@ body {
367
  }
368
  }
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  /* Camera eyes blinking animation */
371
  /* Camera eye-opening animation */
372
  @keyframes eye-open {
 
29
  .step-indicator {
30
  position: absolute;
31
  bottom: 20px;
32
+ right: 20px;
33
  font-size: 13px;
34
  font-weight: 500;
35
  color: #64748b;
 
249
 
250
  .btn-primary,
251
  .btn-success,
252
+ .btn-fail,
253
+ .btn-secondary {
254
  padding: 12px 24px;
255
  border-radius: 8px;
256
  font-size: 14px;
 
288
  color: #FF9900;
289
  }
290
 
291
+ /* Secondary button (neutral gray) */
292
+ .btn-secondary {
293
+ background: #f1f5f9;
294
+ color: #475569;
295
+ border: 2px solid #cbd5e1;
296
+ }
297
+
298
+ .btn-secondary:hover:not(:disabled) {
299
+ background: #e2e8f0;
300
+ border-color: #94a3b8;
301
+ transform: translateY(-1px);
302
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
303
+ }
304
+
305
  /* Text link style for secondary actions */
306
  .btn-text {
307
  background: transparent;
 
322
 
323
  .btn-primary:disabled,
324
  .btn-success:disabled,
325
+ .btn-secondary:disabled,
326
  .btn-text:disabled {
327
  background: #f3f4f6;
328
  color: #9ca3af;
 
330
  opacity: 0.6;
331
  }
332
 
333
+ /* Back button - text link style */
334
+ .btn-back {
335
+ position: absolute;
336
+ bottom: 20px;
337
+ left: 20px;
338
+ background: transparent;
339
+ color: #6b7280;
340
+ border: none;
341
+ padding: 0;
342
+ font-size: 13px;
343
+ font-weight: 500;
344
+ cursor: pointer;
345
+ text-decoration: none;
346
+ transition: all 0.2s ease;
347
+ }
348
+
349
+ .btn-back:hover:not(:disabled) {
350
+ color: #FF9900;
351
+ text-decoration: underline;
352
+ }
353
+
354
+ .btn-back:disabled {
355
+ color: #4b5563;
356
+ cursor: not-allowed;
357
+ opacity: 0.6;
358
+ }
359
+
360
  /* Summary */
361
  .summary-list {
362
  list-style: none;
 
410
  }
411
  }
412
 
413
+ /* Reachy speaking bubble */
414
+ .reachy-bubble-container {
415
+ position: absolute;
416
+ bottom: 120px;
417
+ right: -270px;
418
+ display: flex;
419
+ flex-direction: row-reverse;
420
+ align-items: center;
421
+ gap: 12px;
422
+ z-index: 10;
423
+ }
424
+
425
+ .speech-bubble {
426
+ background: #FFFFFF;
427
+ border: 2px solid #FF9900;
428
+ border-radius: 12px;
429
+ padding: 12px 16px;
430
+ font-size: 14px;
431
+ font-weight: 500;
432
+ color: #0f172a;
433
+ box-shadow: 0 4px 12px rgba(255, 153, 0, 0.2);
434
+ position: relative;
435
+ max-width: 200px;
436
+ line-height: 1.4;
437
+ }
438
+
439
+ .speech-bubble::after {
440
+ content: '';
441
+ position: absolute;
442
+ left: -10px;
443
+ top: 50%;
444
+ transform: translateY(-50%);
445
+ width: 0;
446
+ height: 0;
447
+ border-top: 10px solid transparent;
448
+ border-bottom: 10px solid transparent;
449
+ border-right: 10px solid #FF9900;
450
+ }
451
+
452
+ .speech-bubble::before {
453
+ content: '';
454
+ position: absolute;
455
+ left: -7px;
456
+ top: 50%;
457
+ transform: translateY(-50%);
458
+ width: 0;
459
+ height: 0;
460
+ border-top: 8px solid transparent;
461
+ border-bottom: 8px solid transparent;
462
+ border-right: 8px solid #FFFFFF;
463
+ z-index: 1;
464
+ }
465
+
466
+ .bubble-reachy-icon {
467
+ width: 75px;
468
+ height: auto;
469
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
470
+ flex-shrink: 0;
471
+ }
472
+
473
  /* Camera eyes blinking animation */
474
  /* Camera eye-opening animation */
475
  @keyframes eye-open {