8bitkick commited on
Commit
e7efca5
·
1 Parent(s): 483872c
README.md CHANGED
@@ -10,3 +10,9 @@ tags:
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
+
14
+ ```
15
+ uv venv --python 3.12
16
+ source .venv/bin/activate
17
+ ```
18
+
pyproject.toml CHANGED
@@ -12,6 +12,9 @@ requires-python = ">=3.8"
12
  # dependencies = ["reachy-mini"]
13
  dependencies = [
14
  "reachy-mini@git+https://github.com/pollen-robotics/reachy_mini",
 
 
 
15
  ] # TODO open
16
 
17
  [project.entry-points."reachy_mini_apps"]
 
12
  # dependencies = ["reachy-mini"]
13
  dependencies = [
14
  "reachy-mini@git+https://github.com/pollen-robotics/reachy_mini",
15
+ "fastapi>=0.115.0",
16
+ "uvicorn[standard]>=0.32.0",
17
+ "websockets>=13.0",
18
  ] # TODO open
19
 
20
  [project.entry-points."reachy_mini_apps"]
reachy_mini_app_example/main.py CHANGED
@@ -1,28 +1,178 @@
1
  import threading
2
  import time
 
 
 
 
3
 
4
  import numpy as np
 
 
 
 
5
  from reachy_mini import ReachyMiniApp
6
  from reachy_mini.reachy_mini import ReachyMini
7
  from scipy.spatial.transform import Rotation as R
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  class ExampleApp(ReachyMiniApp):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
 
 
 
 
 
 
 
 
 
 
 
 
12
  t0 = time.time()
13
- while not stop_event.is_set():
14
- pose = np.eye(4)
15
- pose[:3, 3][2] = 0.005 * np.sin(2 * np.pi * 0.3 * time.time() + np.pi)
16
- euler_rot = [
17
- 0,
18
- 0,
19
- 0.5 * np.sin(2 * np.pi * 0.3 * time.time() + np.pi),
20
- ]
21
- rot_mat = R.from_euler("xyz", euler_rot, degrees=False).as_matrix()
22
- pose[:3, :3] = rot_mat
23
- pose[:3, 3][2] += 0.01 * np.sin(2 * np.pi * 0.5 * time.time())
24
- antennas = np.array([1, 1]) * np.sin(2 * np.pi * 0.5 * time.time())
25
-
26
- reachy_mini.set_target(head=pose, antennas=antennas)
27
-
28
- time.sleep(0.02)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import threading
2
  import time
3
+ import asyncio
4
+ import json
5
+ from typing import List
6
+ from queue import Queue
7
 
8
  import numpy as np
9
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
10
+ from fastapi.staticfiles import StaticFiles
11
+ from fastapi.responses import FileResponse
12
+ import uvicorn
13
  from reachy_mini import ReachyMiniApp
14
  from reachy_mini.reachy_mini import ReachyMini
15
  from scipy.spatial.transform import Rotation as R
16
 
17
 
18
+ class ConnectionManager:
19
+ def __init__(self):
20
+ self.active_connections: List[WebSocket] = []
21
+ self.state_queue: Queue = Queue()
22
+
23
+ async def connect(self, websocket: WebSocket):
24
+ await websocket.accept()
25
+ self.active_connections.append(websocket)
26
+
27
+ def disconnect(self, websocket: WebSocket):
28
+ self.active_connections.remove(websocket)
29
+
30
+ async def broadcast(self, message: dict):
31
+ disconnected = []
32
+ for connection in self.active_connections:
33
+ try:
34
+ await connection.send_json(message)
35
+ except Exception:
36
+ disconnected.append(connection)
37
+
38
+ # Clean up disconnected clients
39
+ for conn in disconnected:
40
+ if conn in self.active_connections:
41
+ self.active_connections.remove(conn)
42
+
43
+ def queue_state(self, state: dict):
44
+ """Queue state from sync context"""
45
+ self.state_queue.put(state)
46
+
47
+
48
  class ExampleApp(ReachyMiniApp):
49
+ def __init__(self):
50
+ super().__init__()
51
+ self.app = FastAPI()
52
+ self.manager = ConnectionManager()
53
+ self.current_state = {}
54
+ self.setup_routes()
55
+
56
+ def setup_routes(self):
57
+ @self.app.get("/")
58
+ async def read_root():
59
+ return FileResponse("index.html")
60
+
61
+ @self.app.websocket("/api/state/ws/full")
62
+ async def websocket_endpoint(websocket: WebSocket):
63
+ await self.manager.connect(websocket)
64
+ try:
65
+ while True:
66
+ # Check for new state data and broadcast
67
+ if not self.manager.state_queue.empty():
68
+ state = self.manager.state_queue.get()
69
+ await self.manager.broadcast(state)
70
+ await asyncio.sleep(0.01)
71
+ except WebSocketDisconnect:
72
+ self.manager.disconnect(websocket)
73
+
74
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
75
+ # Start FastAPI server in a separate thread
76
+ def start_server():
77
+ uvicorn.run(self.app, host="127.0.0.1", port=8000, log_level="info")
78
+
79
+ server_thread = threading.Thread(target=start_server, daemon=True)
80
+ server_thread.start()
81
+
82
+ print("🚀 Web server started at http://127.0.0.1:8000")
83
+ print("🔌 WebSocket available at ws://127.0.0.1:8000/api/state/ws/full")
84
+ print("📄 Open http://127.0.0.1:8000 in your browser")
85
+ print("Press Ctrl+C to stop\n")
86
+
87
  t0 = time.time()
88
+ try:
89
+ while not stop_event.is_set():
90
+ pose = np.eye(4)
91
+ pose[:3, 3][2] = 0.005 * np.sin(2 * np.pi * 0.3 * time.time() + np.pi)
92
+ euler_rot = [
93
+ 0,
94
+ 0,
95
+ 0.5 * np.sin(2 * np.pi * 0.3 * time.time() + np.pi),
96
+ ]
97
+ rot_mat = R.from_euler("xyz", euler_rot, degrees=False).as_matrix()
98
+ pose[:3, :3] = rot_mat
99
+ pose[:3, 3][2] += 0.01 * np.sin(2 * np.pi * 0.5 * time.time())
100
+ antennas = np.array([1, 1]) * np.sin(2 * np.pi * 0.5 * time.time())
101
+
102
+ reachy_mini.set_target(head=pose, antennas=antennas)
103
+
104
+ # Prepare state data for broadcasting
105
+ state_data = {
106
+ "timestamp": time.time(),
107
+ "head_pose": pose.tolist(),
108
+ "antennas": antennas.tolist(),
109
+ "euler_rotation": euler_rot,
110
+ }
111
+
112
+ # Queue state for WebSocket broadcasting (from sync context)
113
+ self.manager.queue_state(state_data)
114
+
115
+ time.sleep(0.02)
116
+ except KeyboardInterrupt:
117
+ pass
118
+
119
+
120
+ if __name__ == "__main__":
121
+ """
122
+ Standalone testing mode - run this directly to test the app with a Reachy Mini
123
+ running on the same machine (localhost).
124
+
125
+ Usage:
126
+ python reachy_mini_app_example/main.py
127
+
128
+ Options:
129
+ - localhost_only=True: Connect to Reachy on localhost
130
+ - use_sim=True: Use simulation mode (if available)
131
+ """
132
+ import signal
133
+ import sys
134
+
135
+ # Create the app instance
136
+ app = ExampleApp()
137
+
138
+ # Create a stop event for clean shutdown
139
+ stop_event = threading.Event()
140
+ shutdown_initiated = False
141
+
142
+ def signal_handler(sig, frame):
143
+ global shutdown_initiated
144
+ if not shutdown_initiated:
145
+ shutdown_initiated = True
146
+ print("\n🛑 Shutting down gracefully...")
147
+ stop_event.set()
148
+ sys.exit(0)
149
+
150
+ signal.signal(signal.SIGINT, signal_handler)
151
+
152
+ # Connect to Reachy Mini on localhost
153
+ print("🤖 Connecting to Reachy Mini...")
154
+ try:
155
+ # Connect to Reachy Mini (localhost_only=True connects to local robot)
156
+ # Set use_sim=True if you want to test with simulation
157
+ reachy_mini = ReachyMini(
158
+ localhost_only=True, # Connect to robot on localhost
159
+ spawn_daemon=False, # Don't spawn a new daemon (daemon already running)
160
+ use_sim=False, # Set to True for simulation mode
161
+ timeout=5.0,
162
+ log_level='INFO'
163
+ )
164
+ print("✅ Connected to Reachy Mini!")
165
+
166
+ # Run the app
167
+ app.run(reachy_mini, stop_event)
168
+
169
+ except KeyboardInterrupt:
170
+ print("\n🛑 Interrupted by user")
171
+ except Exception as e:
172
+ print(f"❌ Error connecting to Reachy Mini: {e}")
173
+ print("\nMake sure:")
174
+ print(" 1. Reachy Mini daemon is running on this machine")
175
+ print(" 2. The Reachy Mini service is started")
176
+ print(" 3. Try setting use_sim=True for simulation mode")
177
+ finally:
178
+ print("👋 Goodbye!")
reachy_mini_app_example/stream.html ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Joint stream</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 16px; }
9
+ #status { margin-bottom: 8px; }
10
+ table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
11
+ th, td { border: 1px solid #ddd; padding: 6px 8px; text-align: left; }
12
+ th { background: #f4f4f4; position: sticky; top: 0; }
13
+ pre { background: #f8f8f8; border: 1px solid #eee; padding: 8px; overflow: auto; max-height: 30vh; }
14
+ .ok { color: #0a0; }
15
+ .err { color: #a00; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <h3>Joint stream</h3>
20
+ <div id="status">Connecting…</div>
21
+
22
+ <table id="joint-table" aria-label="Joint table">
23
+ <thead></thead>
24
+ <tbody></tbody>
25
+ </table>
26
+
27
+ <h4>Last payload</h4>
28
+ <pre id="raw"></pre>
29
+
30
+ <script>
31
+ // Change via URL: ?ws=ws://host:port/api/state/ws/full
32
+ const WS_URL = new URLSearchParams(location.search).get("ws")
33
+ || "ws://127.0.0.1:8000/api/state/ws/full";
34
+
35
+ const statusEl = document.getElementById("status");
36
+ const rawEl = document.getElementById("raw");
37
+ const thead = document.querySelector("#joint-table thead");
38
+ const tbody = document.querySelector("#joint-table tbody");
39
+
40
+ let ws;
41
+ let latestPayload = null;
42
+ let rafScheduled = false;
43
+ let columns = ["Joint", "position", "velocity", "torque"]; // default preferred columns
44
+
45
+ function setStatus(text, cls="") {
46
+ statusEl.className = cls;
47
+ statusEl.textContent = text;
48
+ }
49
+
50
+ function connect() {
51
+ try {
52
+ ws = new WebSocket(WS_URL);
53
+ } catch (e) {
54
+ setStatus("Invalid WebSocket URL: " + e.message, "err");
55
+ return;
56
+ }
57
+
58
+ ws.onopen = () => setStatus("Connected: " + WS_URL, "ok");
59
+ ws.onclose = (e) => setStatus(`Disconnected (${e.code}) — reconnecting in 1s…`, "err");
60
+ ws.onerror = () => setStatus("WebSocket error — see console", "err");
61
+ ws.onmessage = (event) => {
62
+ try {
63
+ latestPayload = JSON.parse(event.data);
64
+ scheduleRender();
65
+ } catch (e) {
66
+ setStatus("Bad JSON from server", "err");
67
+ }
68
+ };
69
+
70
+ // simple autoreconnect
71
+ ws.addEventListener("close", () => setTimeout(connect, 1000), { once: true });
72
+ }
73
+
74
+ function scheduleRender() {
75
+ if (rafScheduled) return;
76
+ rafScheduled = true;
77
+ requestAnimationFrame(() => {
78
+ rafScheduled = false;
79
+ if (latestPayload) render(latestPayload);
80
+ });
81
+ }
82
+
83
+ function render(payload) {
84
+ rawEl.textContent = JSON.stringify(payload, null, 2);
85
+ const joints = pickJoints(payload);
86
+ const rows = normalizeJoints(joints);
87
+ if (!rows.length) {
88
+ thead.innerHTML = "";
89
+ tbody.innerHTML = "";
90
+ return;
91
+ }
92
+ // build dynamic column set (prefer known order)
93
+ const fieldSet = new Set(["Joint", ...columns.slice(1)]);
94
+ for (const r of rows) for (const k of Object.keys(r)) fieldSet.add(k);
95
+ // ensure "Joint" first
96
+ const cols = ["Joint", ...[...fieldSet].filter(c => c !== "Joint")];
97
+ if (JSON.stringify(cols) !== JSON.stringify(columns)) {
98
+ columns = cols;
99
+ renderHeader(columns);
100
+ }
101
+ renderBody(rows, columns);
102
+ }
103
+
104
+ function renderHeader(cols) {
105
+ const tr = document.createElement("tr");
106
+ for (const c of cols) {
107
+ const th = document.createElement("th");
108
+ th.textContent = c;
109
+ tr.appendChild(th);
110
+ }
111
+ thead.innerHTML = "";
112
+ thead.appendChild(tr);
113
+ }
114
+
115
+ function renderBody(rows, cols) {
116
+ const frag = document.createDocumentFragment();
117
+ for (const r of rows) {
118
+ const tr = document.createElement("tr");
119
+ for (const c of cols) {
120
+ const td = document.createElement("td");
121
+ const v = r[c];
122
+ td.textContent = v === undefined ? "" : (typeof v === "number" ? Number(v).toFixed(4) : String(v));
123
+ tr.appendChild(td);
124
+ }
125
+ frag.appendChild(tr);
126
+ }
127
+ tbody.innerHTML = "";
128
+ tbody.appendChild(frag);
129
+ }
130
+
131
+ function pickJoints(obj) {
132
+ if (!obj || typeof obj !== "object") return null;
133
+ // Common placements
134
+ if (obj.joints) return obj.joints;
135
+ if (obj.state && obj.state.joints) return obj.state.joints;
136
+ if (obj.motors) return obj.motors;
137
+ if (obj.body && obj.body.joints) return obj.body.joints;
138
+ return obj.joints_state || null;
139
+ }
140
+
141
+ function normalizeJoints(joints) {
142
+ if (!joints) return [];
143
+ const out = [];
144
+
145
+ const preferredKeys = ["position","pos","angle","velocity","vel","speed","torque","effort","current","temperature","temp","value"];
146
+
147
+ const extractFields = (o) => {
148
+ const row = {};
149
+ for (const k of preferredKeys) {
150
+ if (o && typeof o === "object" && k in o && isFinite(o[k])) row[k] = Number(o[k]);
151
+ }
152
+ // fallback: include any top-level numeric fields
153
+ for (const [k,v] of Object.entries(o || {})) {
154
+ if (!(k in row) && isFinite(v)) row[k] = Number(v);
155
+ }
156
+ return row;
157
+ };
158
+
159
+ if (Array.isArray(joints)) {
160
+ for (const item of joints) {
161
+ if (item == null) continue;
162
+ let name = item.name ?? item.id ?? item.label ?? "";
163
+ // if the array is like [{jointName: {..}}, ...]
164
+ if (!name && typeof item === "object" && !Array.isArray(item)) {
165
+ const keys = Object.keys(item);
166
+ if (keys.length === 1) {
167
+ name = keys[0];
168
+ const fields = extractFields(item[name]);
169
+ out.push({ Joint: name, ...fields });
170
+ continue;
171
+ }
172
+ }
173
+ const fields = extractFields(item);
174
+ out.push({ Joint: String(name || out.length), ...fields });
175
+ }
176
+ return out;
177
+ }
178
+
179
+ if (typeof joints === "object") {
180
+ for (const [name, val] of Object.entries(joints)) {
181
+ if (val != null && typeof val === "object") {
182
+ out.push({ Joint: name, ...extractFields(val) });
183
+ } else if (isFinite(val)) {
184
+ out.push({ Joint: name, value: Number(val) });
185
+ }
186
+ }
187
+ return out;
188
+ }
189
+
190
+ return out;
191
+ }
192
+
193
+ connect();
194
+ </script>
195
+ </body>
196
+ </html>