8bitkick commited on
Commit
539edb0
Β·
1 Parent(s): e7efca5
Files changed (2) hide show
  1. pyproject.toml +1 -5
  2. reachy_mini_app_example/main.py +29 -140
pyproject.toml CHANGED
@@ -9,13 +9,9 @@ version = "0.1.0"
9
  description = "Add your description here"
10
  readme = "README.md"
11
  requires-python = ">=3.8"
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"]
21
  reachy_mini_app_example = "reachy_mini_app_example.main:ExampleApp"
 
9
  description = "Add your description here"
10
  readme = "README.md"
11
  requires-python = ">=3.8"
 
12
  dependencies = [
13
  "reachy-mini@git+https://github.com/pollen-robotics/reachy_mini",
14
+ ]
 
 
 
15
 
16
  [project.entry-points."reachy_mini_apps"]
17
  reachy_mini_app_example = "reachy_mini_app_example.main:ExampleApp"
reachy_mini_app_example/main.py CHANGED
@@ -1,178 +1,67 @@
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!")
 
1
  import threading
2
  import time
3
+ from http.server import HTTPServer, SimpleHTTPRequestHandler
4
+ from functools import partial
5
+ from pathlib import Path
 
6
 
7
  import numpy as np
 
 
 
 
8
  from reachy_mini import ReachyMiniApp
9
  from reachy_mini.reachy_mini import ReachyMini
10
  from scipy.spatial.transform import Rotation as R
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  class ExampleApp(ReachyMiniApp):
14
  def __init__(self):
15
  super().__init__()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
18
+ def start_static_server(host="127.0.0.1", port=8080, directory=None):
19
+ directory = directory or Path(__file__).resolve().parent
20
+ handler = partial(SimpleHTTPRequestHandler, directory=str(directory))
21
+ server = HTTPServer((host, port), handler)
22
+ print(f"πŸš€ Web server: http://{host}:{port}")
23
+ print("πŸ”Œ WebSocket: ws://127.0.0.1:8000/api/state/ws/full")
24
+ print("πŸ“„ Open: http://127.0.0.1:8080/stream.html\n")
25
+ server.serve_forever()
26
+
27
+ threading.Thread(
28
+ target=start_static_server,
29
+ kwargs={"directory": Path(__file__).parent},
30
+ daemon=True,
31
+ ).start()
32
 
 
 
 
 
 
 
33
  try:
34
  while not stop_event.is_set():
35
+ now = time.time()
36
+ swing = np.sin(2 * np.pi * 0.3 * now + np.pi)
37
+ bob = np.sin(2 * np.pi * 0.5 * now)
38
+
39
  pose = np.eye(4)
40
+ pose[:3, :3] = R.from_euler("z", 0.5 * swing, degrees=False).as_matrix()
41
+ pose[2, 3] = 0.005 * swing + 0.01 * bob
 
 
 
 
 
 
 
 
42
 
43
+ antennas = np.full(2, bob)
44
  reachy_mini.set_target(head=pose, antennas=antennas)
 
 
 
 
 
 
 
 
 
 
 
 
45
  time.sleep(0.02)
46
  except KeyboardInterrupt:
47
  pass
48
 
49
 
50
  if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  print("πŸ€– Connecting to Reachy Mini...")
52
  try:
 
 
53
  reachy_mini = ReachyMini(
54
+ localhost_only=True,
55
+ spawn_daemon=False,
56
+ use_sim=False,
57
  timeout=5.0,
58
+ log_level="INFO",
59
  )
60
  print("βœ… Connected to Reachy Mini!")
61
+ ExampleApp().run(reachy_mini, threading.Event())
 
 
 
62
  except KeyboardInterrupt:
63
  print("\nπŸ›‘ Interrupted by user")
64
  except Exception as e:
65
  print(f"❌ Error connecting to Reachy Mini: {e}")
 
 
 
 
66
  finally:
67
  print("πŸ‘‹ Goodbye!")