RemiFabre commited on
Commit
c51a63a
·
1 Parent(s): 27e1781

Better load/stop experience

Browse files
emotions/main.py CHANGED
@@ -10,6 +10,7 @@ import os
10
  from pathlib import Path
11
  from typing import Any, Literal
12
 
 
13
  from fastapi import HTTPException
14
  from pydantic import BaseModel, Field
15
  from ruamel.yaml import YAML
@@ -75,6 +76,9 @@ WHEEL_NODE_LAYOUT = {
75
  },
76
  }
77
 
 
 
 
78
 
79
  @dataclass
80
  class WheelMove:
@@ -634,6 +638,34 @@ class Emotions(ReachyMiniApp):
634
  "precision": updated_move.precision if updated_move else None,
635
  }
636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  def _register_routes(self) -> None:
638
  @self.settings_app.get("/api/emotions")
639
  def list_emotions() -> dict[str, Any]:
@@ -712,22 +744,28 @@ class Emotions(ReachyMiniApp):
712
  return self._save_review(move_id, payload)
713
 
714
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event) -> None:
715
- while not stop_event.is_set():
716
- move_to_play: str | None = None
717
- with self._lock:
718
- if self._pending_move:
719
- move_to_play = self._pending_move
720
- self._pending_move = None
721
- self._current_move = move_to_play
722
-
723
- if move_to_play:
724
- move = self._recorded_emotions.get(move_to_play)
725
- reachy_mini.play_move(move, initial_goto_duration=0.7)
726
  with self._lock:
727
- self._current_move = None
728
- self._last_completed = time.time()
729
- else:
730
- stop_event.wait(0.01)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
 
732
 
733
  if __name__ == "__main__":
 
10
  from pathlib import Path
11
  from typing import Any, Literal
12
 
13
+ import numpy as np
14
  from fastapi import HTTPException
15
  from pydantic import BaseModel, Field
16
  from ruamel.yaml import YAML
 
76
  },
77
  }
78
 
79
+ SHUTDOWN_MOVE_ID = "grateful1"
80
+ MOVE_INITIAL_GOTO_DURATION = 0.7
81
+
82
 
83
  @dataclass
84
  class WheelMove:
 
638
  "precision": updated_move.precision if updated_move else None,
639
  }
640
 
641
+ def _play_shutdown_sequence(self, reachy_mini: ReachyMini) -> None:
642
+ """Play a graceful shutdown move and return to the base pose."""
643
+ if not SHUTDOWN_MOVE_ID:
644
+ return
645
+ try:
646
+ move = self._recorded_emotions.get(SHUTDOWN_MOVE_ID)
647
+ except ValueError:
648
+ self.logger.warning("Shutdown move %s missing from recorded dataset.", SHUTDOWN_MOVE_ID)
649
+ return
650
+
651
+ with self._lock:
652
+ self._pending_move = None
653
+ self._current_move = SHUTDOWN_MOVE_ID
654
+
655
+ try:
656
+ reachy_mini.play_move(move, initial_goto_duration=MOVE_INITIAL_GOTO_DURATION)
657
+ except Exception:
658
+ self.logger.exception("Failed to play shutdown move %s.", SHUTDOWN_MOVE_ID)
659
+ finally:
660
+ with self._lock:
661
+ self._current_move = None
662
+ self._last_completed = time.time()
663
+
664
+ try:
665
+ reachy_mini.goto_target(np.eye(4), antennas=[0.0, 0.0], duration=1.2)
666
+ except Exception:
667
+ self.logger.exception("Unable to interpolate Reachy Mini back to the base pose after shutdown.")
668
+
669
  def _register_routes(self) -> None:
670
  @self.settings_app.get("/api/emotions")
671
  def list_emotions() -> dict[str, Any]:
 
744
  return self._save_review(move_id, payload)
745
 
746
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event) -> None:
747
+ try:
748
+ while not stop_event.is_set():
749
+ move_to_play: str | None = None
 
 
 
 
 
 
 
 
750
  with self._lock:
751
+ if self._pending_move:
752
+ move_to_play = self._pending_move
753
+ self._pending_move = None
754
+ self._current_move = move_to_play
755
+
756
+ if move_to_play:
757
+ move = self._recorded_emotions.get(move_to_play)
758
+ reachy_mini.play_move(
759
+ move, initial_goto_duration=MOVE_INITIAL_GOTO_DURATION
760
+ )
761
+ with self._lock:
762
+ self._current_move = None
763
+ self._last_completed = time.time()
764
+ else:
765
+ stop_event.wait(0.01)
766
+ finally:
767
+ if stop_event.is_set():
768
+ self._play_shutdown_sequence(reachy_mini)
769
 
770
 
771
  if __name__ == "__main__":
emotions/static/index.html CHANGED
@@ -9,6 +9,15 @@
9
  </head>
10
 
11
  <body>
 
 
 
 
 
 
 
 
 
12
  <div class="background-halo"></div>
13
  <main class="app-shell">
14
  <header class="hero">
 
9
  </head>
10
 
11
  <body>
12
+ <div id="appLoader" class="loading-screen" aria-live="polite">
13
+ <div class="loading-card">
14
+ <div class="loading-spinner" role="presentation"></div>
15
+ <p class="loading-title">Application loading…</p>
16
+ <p id="loaderMessage" class="loading-message">
17
+ Reachy Mini is waking up. No refresh needed; if this screen stays here for a while press F5 once.
18
+ </p>
19
+ </div>
20
+ </div>
21
  <div class="background-halo"></div>
22
  <main class="app-shell">
23
  <header class="hero">
emotions/static/main.js CHANGED
@@ -20,6 +20,8 @@ const reviewNextBtn = document.getElementById("reviewNextBtn");
20
  const reviewClearBtn = document.getElementById("reviewClearBtn");
21
  const reviewQualityContainer = document.getElementById("reviewQualityOptions");
22
  const reviewPrecisionContainer = document.getElementById("reviewPrecisionOptions");
 
 
23
 
24
  const WHEEL_PADDING = 60;
25
  const MAX_LANES = 9;
@@ -59,6 +61,8 @@ const appState = {
59
  const manualState = {
60
  drag: null,
61
  };
 
 
62
 
63
  const currentUrl = new URL(window.location.href);
64
  if (!currentUrl.pathname.endsWith("/")) {
@@ -169,6 +173,19 @@ function showToast(message) {
169
  setTimeout(() => toast.classList.remove("visible"), 2600);
170
  }
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  function updateStatusChip(label, playing) {
173
  statusChip.textContent = label;
174
  statusChip.classList.toggle("playing", playing);
@@ -1184,6 +1201,17 @@ async function triggerPlay(moveId) {
1184
  }
1185
  }
1186
 
 
 
 
 
 
 
 
 
 
 
 
1187
  async function pollState() {
1188
  try {
1189
  const data = await fetchJson("/api/state");
@@ -1201,11 +1229,18 @@ async function init() {
1201
  await initReviewConfig();
1202
  await loadAndRender();
1203
  setBusyState(false, null);
 
 
1204
  pollState();
1205
  } catch (error) {
1206
  console.error(error);
1207
  updateStatusChip("Failed to load data", false);
1208
  showToast("Unable to fetch the emotions list.");
 
 
 
 
 
1209
  }
1210
  }
1211
 
 
20
  const reviewClearBtn = document.getElementById("reviewClearBtn");
21
  const reviewQualityContainer = document.getElementById("reviewQualityOptions");
22
  const reviewPrecisionContainer = document.getElementById("reviewPrecisionOptions");
23
+ const loadingScreen = document.getElementById("appLoader");
24
+ const loaderMessage = document.getElementById("loaderMessage");
25
 
26
  const WHEEL_PADDING = 60;
27
  const MAX_LANES = 9;
 
61
  const manualState = {
62
  drag: null,
63
  };
64
+ const INITIAL_WAKE_MOVE = "loving1";
65
+ let initialWakeTriggered = false;
66
 
67
  const currentUrl = new URL(window.location.href);
68
  if (!currentUrl.pathname.endsWith("/")) {
 
173
  setTimeout(() => toast.classList.remove("visible"), 2600);
174
  }
175
 
176
+ function updateLoadingScreen({ visible, message, isError } = {}) {
177
+ if (!loadingScreen) {
178
+ return;
179
+ }
180
+ if (typeof visible === "boolean") {
181
+ loadingScreen.classList.toggle("hidden", !visible);
182
+ }
183
+ loadingScreen.classList.toggle("error", Boolean(isError));
184
+ if (message && loaderMessage) {
185
+ loaderMessage.textContent = message;
186
+ }
187
+ }
188
+
189
  function updateStatusChip(label, playing) {
190
  statusChip.textContent = label;
191
  statusChip.classList.toggle("playing", playing);
 
1201
  }
1202
  }
1203
 
1204
+ function triggerInitialWakeMove() {
1205
+ if (initialWakeTriggered) {
1206
+ return;
1207
+ }
1208
+ if (!appState.moveIndex.has(INITIAL_WAKE_MOVE)) {
1209
+ return;
1210
+ }
1211
+ initialWakeTriggered = true;
1212
+ triggerPlay(INITIAL_WAKE_MOVE);
1213
+ }
1214
+
1215
  async function pollState() {
1216
  try {
1217
  const data = await fetchJson("/api/state");
 
1229
  await initReviewConfig();
1230
  await loadAndRender();
1231
  setBusyState(false, null);
1232
+ updateLoadingScreen({ visible: false });
1233
+ triggerInitialWakeMove();
1234
  pollState();
1235
  } catch (error) {
1236
  console.error(error);
1237
  updateStatusChip("Failed to load data", false);
1238
  showToast("Unable to fetch the emotions list.");
1239
+ updateLoadingScreen({
1240
+ visible: true,
1241
+ isError: true,
1242
+ message: "Something went wrong while loading. Check the robot, then press F5 once to retry.",
1243
+ });
1244
  }
1245
  }
1246
 
emotions/static/style.css CHANGED
@@ -33,6 +33,74 @@ body {
33
  color: var(--text-strong);
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  .background-halo {
37
  position: fixed;
38
  inset: 0;
 
33
  color: var(--text-strong);
34
  }
35
 
36
+ .loading-screen {
37
+ position: fixed;
38
+ inset: 0;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ background: rgba(7, 11, 22, 0.92);
43
+ z-index: 10;
44
+ transition: opacity 0.4s ease, visibility 0.4s ease;
45
+ }
46
+
47
+ .loading-screen.hidden {
48
+ opacity: 0;
49
+ visibility: hidden;
50
+ pointer-events: none;
51
+ }
52
+
53
+ .loading-card {
54
+ padding: 48px 40px;
55
+ border-radius: 28px;
56
+ border: 1px solid rgba(255, 255, 255, 0.08);
57
+ background: rgba(15, 18, 30, 0.95);
58
+ box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
59
+ max-width: 420px;
60
+ text-align: center;
61
+ }
62
+
63
+ .loading-spinner {
64
+ width: 64px;
65
+ height: 64px;
66
+ margin: 0 auto 24px;
67
+ border-radius: 50%;
68
+ border: 3px solid rgba(255, 255, 255, 0.2);
69
+ border-top-color: rgba(255, 255, 255, 0.8);
70
+ animation: spin 1s linear infinite;
71
+ }
72
+
73
+ .loading-title {
74
+ margin: 0 0 8px;
75
+ font-size: 1.4rem;
76
+ letter-spacing: 0.2em;
77
+ text-transform: uppercase;
78
+ }
79
+
80
+ .loading-message {
81
+ margin: 0;
82
+ color: var(--text-soft);
83
+ line-height: 1.6;
84
+ }
85
+
86
+ .loading-screen.error .loading-card {
87
+ border-color: rgba(255, 120, 120, 0.4);
88
+ }
89
+
90
+ .loading-screen.error .loading-message {
91
+ color: #ffb0b0;
92
+ }
93
+
94
+ @keyframes spin {
95
+ from {
96
+ transform: rotate(0deg);
97
+ }
98
+
99
+ to {
100
+ transform: rotate(360deg);
101
+ }
102
+ }
103
+
104
  .background-halo {
105
  position: fixed;
106
  inset: 0;