cduss commited on
Commit
bee5688
Β·
1 Parent(s): 66e0694
Files changed (5) hide show
  1. README.md +55 -34
  2. app.js +270 -0
  3. app.py +20 -465
  4. index.html +324 -0
  5. server.py +43 -0
README.md CHANGED
@@ -1,55 +1,76 @@
1
- ---
2
- title: Reachy Mini Simple Control Panel
3
- emoji: πŸ€–
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: gradio
7
- sdk_version: "6.0.0"
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
  # Reachy Mini Simple Control Panel
13
 
14
- A simple web-based control panel for the Reachy Mini robot.
15
 
16
  ## Features
17
 
18
- - **Connect to Robot**: Connect to your Reachy Mini robot at localhost:8000
19
- - **Real-time Status**: Monitor robot status with 5Hz polling
20
- - **Motor Control**: Enable/disable motors (torque on/off)
21
- - **Movement Commands**: Wake up and go to sleep commands
22
- - **Task Space Control**: Control robot's head pose (X, Y, Z, Roll, Pitch, Yaw)
23
- - **Body Control**: Control body yaw rotation
24
- - **Antenna Control**: Control left and right antennas
25
 
26
  ## How to Use
27
 
28
- 1. Make sure your Reachy Mini robot daemon is running on localhost:8000
29
- 2. Click "Connect to Robot" button
30
- 3. Use the buttons and sliders to control your robot
 
 
 
 
 
 
31
 
32
- ## Local Testing
33
 
34
- To test locally:
35
 
36
  ```bash
37
- pip install -r requirements.txt
38
- python app.py
39
  ```
40
 
41
  Then open http://localhost:7860 in your browser.
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  ## Architecture
44
 
45
- This app uses the Reachy Mini HTTP REST API. It communicates with the following endpoints:
 
 
 
 
 
 
 
46
 
47
- - `GET /api/state/full` - Get robot state (polled at 5Hz)
48
- - `POST /api/motors/set_mode/{mode}` - Set motor mode (enabled/disabled)
49
- - `POST /api/move/play/wake_up` - Wake up robot
50
- - `POST /api/move/play/goto_sleep` - Put robot to sleep
51
- - `POST /api/move/set_target` - Set target pose
52
 
53
- ## HuggingFace Spaces Deployment
54
 
55
- To deploy on HuggingFace Spaces, you'll need to set up a tunnel or VPN to make your robot accessible from the internet. The app assumes the robot is at localhost:8000.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Reachy Mini Simple Control Panel
2
 
3
+ A lightweight, static HTML/JS control panel for Reachy Mini robot using WebSockets for real-time control.
4
 
5
  ## Features
6
 
7
+ - **Real-time WebSocket Control**: Instant response using WebSocket streaming
8
+ - **Wake Up / Go to Sleep**: Movement command buttons
9
+ - **Task Space Control**: Real-time head pose control (X, Y, Z, Roll, Pitch, Yaw)
10
+ - **Body & Antennas**: Control body yaw and antenna positions
11
+ - **Auto-reconnect**: Automatically reconnects if connection is lost
12
+ - **No Dependencies**: Pure HTML/CSS/JavaScript
 
13
 
14
  ## How to Use
15
 
16
+ ### Option 1: Open Directly (Simple)
17
+
18
+ Just open `index.html` in your web browser:
19
+
20
+ ```bash
21
+ open index.html # macOS
22
+ xdg-open index.html # Linux
23
+ start index.html # Windows
24
+ ```
25
 
26
+ **Note**: The robot daemon must be running on `localhost:8000`
27
 
28
+ ### Option 2: Serve with Python (Recommended for HuggingFace Spaces)
29
 
30
  ```bash
31
+ python server.py
 
32
  ```
33
 
34
  Then open http://localhost:7860 in your browser.
35
 
36
+ ### Option 3: Serve with any HTTP server
37
+
38
+ ```bash
39
+ # Using Python 3
40
+ python3 -m http.server 7860
41
+
42
+ # Using Node.js
43
+ npx http-server -p 7860
44
+
45
+ # Using PHP
46
+ php -S localhost:7860
47
+ ```
48
+
49
+ ## Requirements
50
+
51
+ - Reachy Mini daemon running on `localhost:8000`
52
+ - Modern web browser with WebSocket support
53
+ - Motors must be enabled before using wake/sleep commands
54
+
55
  ## Architecture
56
 
57
+ This app uses:
58
+ - **WebSocket** (`ws://localhost:8000/api/move/ws/set_target`) for real-time pose streaming
59
+ - **HTTP POST** for wake_up and goto_sleep commands
60
+ - Pure client-side JavaScript (no backend needed)
61
+
62
+ The app automatically connects to the robot when loaded and reconnects if the connection is lost.
63
+
64
+ ## Deployment on HuggingFace Spaces
65
 
66
+ 1. Copy `index.html`, `app.js`, and `server.py` to your Space
67
+ 2. Set SDK to `gradio` with sdk_version `6.0.0` (only needed for the server)
68
+ 3. Or use `static` SDK if HF supports it
69
+ 4. Users will need to have the robot daemon accessible from their machine
 
70
 
71
+ ## Technical Details
72
 
73
+ - **Slider Updates**: Use `input` event for real-time updates
74
+ - **Auto-reconnect**: WebSocket reconnects every 2 seconds if disconnected
75
+ - **State Management**: Client-side state tracking prevents feedback loops
76
+ - **Error Handling**: Graceful error messages and automatic recovery
app.js ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Reachy Mini Control Panel - WebSocket Version
2
+ // Connects to localhost:8000 WebSocket and HTTP APIs
3
+
4
+ const ROBOT_URL = 'localhost:8000';
5
+ const WS_URL = `ws://${ROBOT_URL}/api/move/ws/set_target`;
6
+ const HTTP_URL = `http://${ROBOT_URL}`;
7
+
8
+ // Global state
9
+ const state = {
10
+ ws: null,
11
+ connected: false,
12
+ isRefreshing: false,
13
+ currentPose: {
14
+ head: { x: 0, y: 0, z: 0, roll: 0, pitch: 0, yaw: 0 },
15
+ bodyYaw: 0,
16
+ antennas: [0, 0]
17
+ }
18
+ };
19
+
20
+ // DOM elements
21
+ const elements = {
22
+ status: document.getElementById('connectionStatus'),
23
+ wakeUpBtn: document.getElementById('wakeUpBtn'),
24
+ sleepBtn: document.getElementById('sleepBtn'),
25
+ sliders: {
26
+ headX: document.getElementById('headX'),
27
+ headY: document.getElementById('headY'),
28
+ headZ: document.getElementById('headZ'),
29
+ headRoll: document.getElementById('headRoll'),
30
+ headPitch: document.getElementById('headPitch'),
31
+ headYaw: document.getElementById('headYaw'),
32
+ bodyYaw: document.getElementById('bodyYaw'),
33
+ antennaLeft: document.getElementById('antennaLeft'),
34
+ antennaRight: document.getElementById('antennaRight')
35
+ },
36
+ values: {
37
+ headX: document.getElementById('headXValue'),
38
+ headY: document.getElementById('headYValue'),
39
+ headZ: document.getElementById('headZValue'),
40
+ headRoll: document.getElementById('headRollValue'),
41
+ headPitch: document.getElementById('headPitchValue'),
42
+ headYaw: document.getElementById('headYawValue'),
43
+ bodyYaw: document.getElementById('bodyYawValue'),
44
+ antennaLeft: document.getElementById('antennaLeftValue'),
45
+ antennaRight: document.getElementById('antennaRightValue')
46
+ }
47
+ };
48
+
49
+ // WebSocket connection
50
+ function connectWebSocket() {
51
+ console.log('Connecting to WebSocket:', WS_URL);
52
+
53
+ state.ws = new WebSocket(WS_URL);
54
+
55
+ state.ws.onopen = () => {
56
+ console.log('WebSocket connected');
57
+ state.connected = true;
58
+ updateConnectionStatus(true);
59
+ enableControls(true);
60
+ };
61
+
62
+ state.ws.onclose = () => {
63
+ console.log('WebSocket disconnected');
64
+ state.connected = false;
65
+ updateConnectionStatus(false);
66
+ enableControls(false);
67
+
68
+ // Reconnect after 2 seconds
69
+ setTimeout(connectWebSocket, 2000);
70
+ };
71
+
72
+ state.ws.onerror = (error) => {
73
+ console.error('WebSocket error:', error);
74
+ };
75
+
76
+ state.ws.onmessage = (event) => {
77
+ try {
78
+ const message = JSON.parse(event.data);
79
+ if (message.status === 'error') {
80
+ console.error('Server error:', message.detail);
81
+ }
82
+ } catch (e) {
83
+ console.error('Failed to parse message:', e);
84
+ }
85
+ };
86
+ }
87
+
88
+ // Update connection status UI
89
+ function updateConnectionStatus(connected) {
90
+ if (connected) {
91
+ elements.status.className = 'status connected';
92
+ elements.status.innerHTML = '<span><span class="status-dot green"></span>Connected to robot</span>';
93
+ } else {
94
+ elements.status.className = 'status disconnected';
95
+ elements.status.innerHTML = '<span><span class="status-dot red"></span>Disconnected - Reconnecting...</span>';
96
+ }
97
+ }
98
+
99
+ // Enable/disable controls
100
+ function enableControls(enabled) {
101
+ elements.wakeUpBtn.disabled = !enabled;
102
+ elements.sleepBtn.disabled = !enabled;
103
+
104
+ Object.values(elements.sliders).forEach(slider => {
105
+ slider.disabled = !enabled;
106
+ });
107
+ }
108
+
109
+ // Send target pose via WebSocket
110
+ function sendTargetPose() {
111
+ if (!state.connected || !state.ws || state.ws.readyState !== WebSocket.OPEN) {
112
+ console.warn('WebSocket not connected');
113
+ return;
114
+ }
115
+
116
+ if (state.isRefreshing) {
117
+ return; // Don't send during refresh
118
+ }
119
+
120
+ const message = {
121
+ target_head_pose: {
122
+ x: state.currentPose.head.x,
123
+ y: state.currentPose.head.y,
124
+ z: state.currentPose.head.z,
125
+ roll: state.currentPose.head.roll,
126
+ pitch: state.currentPose.head.pitch,
127
+ yaw: state.currentPose.head.yaw
128
+ },
129
+ target_body_yaw: state.currentPose.bodyYaw,
130
+ target_antennas: state.currentPose.antennas
131
+ };
132
+
133
+ try {
134
+ state.ws.send(JSON.stringify(message));
135
+ } catch (error) {
136
+ console.error('Failed to send message:', error);
137
+ }
138
+ }
139
+
140
+ // Slider change handlers
141
+ function setupSliderHandlers() {
142
+ // Head pose sliders
143
+ elements.sliders.headX.addEventListener('input', (e) => {
144
+ const value = parseFloat(e.target.value);
145
+ state.currentPose.head.x = value;
146
+ elements.values.headX.textContent = value.toFixed(3);
147
+ sendTargetPose();
148
+ });
149
+
150
+ elements.sliders.headY.addEventListener('input', (e) => {
151
+ const value = parseFloat(e.target.value);
152
+ state.currentPose.head.y = value;
153
+ elements.values.headY.textContent = value.toFixed(3);
154
+ sendTargetPose();
155
+ });
156
+
157
+ elements.sliders.headZ.addEventListener('input', (e) => {
158
+ const value = parseFloat(e.target.value);
159
+ state.currentPose.head.z = value;
160
+ elements.values.headZ.textContent = value.toFixed(3);
161
+ sendTargetPose();
162
+ });
163
+
164
+ elements.sliders.headRoll.addEventListener('input', (e) => {
165
+ const value = parseFloat(e.target.value);
166
+ state.currentPose.head.roll = value;
167
+ elements.values.headRoll.textContent = value.toFixed(2);
168
+ sendTargetPose();
169
+ });
170
+
171
+ elements.sliders.headPitch.addEventListener('input', (e) => {
172
+ const value = parseFloat(e.target.value);
173
+ state.currentPose.head.pitch = value;
174
+ elements.values.headPitch.textContent = value.toFixed(2);
175
+ sendTargetPose();
176
+ });
177
+
178
+ elements.sliders.headYaw.addEventListener('input', (e) => {
179
+ const value = parseFloat(e.target.value);
180
+ state.currentPose.head.yaw = value;
181
+ elements.values.headYaw.textContent = value.toFixed(2);
182
+ sendTargetPose();
183
+ });
184
+
185
+ // Body yaw slider
186
+ elements.sliders.bodyYaw.addEventListener('input', (e) => {
187
+ const value = parseFloat(e.target.value);
188
+ state.currentPose.bodyYaw = value;
189
+ elements.values.bodyYaw.textContent = value.toFixed(2);
190
+ sendTargetPose();
191
+ });
192
+
193
+ // Antenna sliders
194
+ elements.sliders.antennaLeft.addEventListener('input', (e) => {
195
+ const value = parseFloat(e.target.value);
196
+ state.currentPose.antennas[0] = value;
197
+ elements.values.antennaLeft.textContent = value.toFixed(2);
198
+ sendTargetPose();
199
+ });
200
+
201
+ elements.sliders.antennaRight.addEventListener('input', (e) => {
202
+ const value = parseFloat(e.target.value);
203
+ state.currentPose.antennas[1] = value;
204
+ elements.values.antennaRight.textContent = value.toFixed(2);
205
+ sendTargetPose();
206
+ });
207
+ }
208
+
209
+ // HTTP API calls for movement commands
210
+ async function playWakeUp() {
211
+ try {
212
+ const response = await fetch(`${HTTP_URL}/api/move/play/wake_up`, {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json' }
215
+ });
216
+
217
+ if (!response.ok) {
218
+ throw new Error(`HTTP ${response.status}`);
219
+ }
220
+
221
+ const result = await response.json();
222
+ console.log('Wake up started:', result.uuid);
223
+ } catch (error) {
224
+ console.error('Wake up failed:', error);
225
+ alert('Failed to wake up robot: ' + error.message);
226
+ }
227
+ }
228
+
229
+ async function playGotoSleep() {
230
+ try {
231
+ const response = await fetch(`${HTTP_URL}/api/move/play/goto_sleep`, {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' }
234
+ });
235
+
236
+ if (!response.ok) {
237
+ throw new Error(`HTTP ${response.status}`);
238
+ }
239
+
240
+ const result = await response.json();
241
+ console.log('Go to sleep started:', result.uuid);
242
+ } catch (error) {
243
+ console.error('Go to sleep failed:', error);
244
+ alert('Failed to put robot to sleep: ' + error.message);
245
+ }
246
+ }
247
+
248
+ // Button handlers
249
+ function setupButtonHandlers() {
250
+ elements.wakeUpBtn.addEventListener('click', playWakeUp);
251
+ elements.sleepBtn.addEventListener('click', playGotoSleep);
252
+ }
253
+
254
+ // Initialize app
255
+ function init() {
256
+ console.log('Initializing Reachy Mini Control Panel');
257
+
258
+ setupSliderHandlers();
259
+ setupButtonHandlers();
260
+ connectWebSocket();
261
+
262
+ console.log('Control panel ready');
263
+ }
264
+
265
+ // Start when DOM is loaded
266
+ if (document.readyState === 'loading') {
267
+ document.addEventListener('DOMContentLoaded', init);
268
+ } else {
269
+ init();
270
+ }
app.py CHANGED
@@ -1,478 +1,33 @@
1
  """
2
- Reachy Mini Simple Control Panel - Gradio App
3
-
4
- A simple web-based control panel for Reachy Mini robot using HTTP API.
5
- Uses client-side JavaScript to connect to localhost:8000 on user's machine.
6
- Works on HuggingFace Spaces by making requests from the browser.
7
  """
8
 
9
  import gradio as gr
 
10
 
11
- # All HTTP requests are made from JavaScript (client-side)
12
- # to allow connection to localhost on user's computer when running on HF Spaces
13
-
14
- def create_app():
15
- """Create the Gradio application."""
16
-
17
- with gr.Blocks(title="Reachy Mini Simple Control Panel") as app:
18
-
19
- gr.Markdown("# Reachy Mini Simple Control Panel")
20
- gr.Markdown("Control your Reachy Mini robot via HTTP API (localhost:8000)")
21
-
22
- # Hidden state to track if user is controlling (prevents polling interference)
23
- user_controlling = gr.State(False)
24
-
25
- # Connection Section
26
- with gr.Group():
27
- gr.Markdown("### Connection")
28
- connect_btn = gr.Button("Connect to Robot", variant="primary")
29
- status_text = gr.Textbox(label="Connection Status", value="Disconnected", interactive=False)
30
- motor_status = gr.Textbox(label="Motor Status", value="N/A", interactive=False)
31
-
32
- # Motor Control Section
33
- with gr.Group():
34
- gr.Markdown("### Motor Control")
35
- with gr.Row():
36
- enable_btn = gr.Button("Torque On", variant="primary")
37
- disable_btn = gr.Button("Torque Off", variant="stop")
38
- log_output = gr.Textbox(label="Log", lines=2, interactive=False)
39
-
40
- # Movement Commands Section
41
- with gr.Group():
42
- gr.Markdown("### Movement Commands")
43
- with gr.Row():
44
- wakeup_btn = gr.Button("Wake Up", variant="primary")
45
- sleep_btn = gr.Button("Go to Sleep")
46
- test_btn = gr.Button("Test Movement", variant="secondary")
47
-
48
- # Task Space Control Section
49
- with gr.Group():
50
- gr.Markdown("### Task Space Control - Head Pose")
51
-
52
- head_x = gr.Slider(
53
- minimum=-0.2, maximum=0.2, value=0, step=0.001,
54
- label="X (forward/backward) [m]"
55
- )
56
- head_y = gr.Slider(
57
- minimum=-0.2, maximum=0.2, value=0, step=0.001,
58
- label="Y (left/right) [m]"
59
- )
60
- head_z = gr.Slider(
61
- minimum=-0.2, maximum=0.2, value=0, step=0.001,
62
- label="Z (up/down) [m]"
63
- )
64
- head_roll = gr.Slider(
65
- minimum=-3.2, maximum=3.2, value=0, step=0.01,
66
- label="Roll [rad]"
67
- )
68
- head_pitch = gr.Slider(
69
- minimum=-3.2, maximum=3.2, value=0, step=0.01,
70
- label="Pitch [rad]"
71
- )
72
- head_yaw = gr.Slider(
73
- minimum=-3.2, maximum=3.2, value=0, step=0.01,
74
- label="Yaw [rad]"
75
- )
76
-
77
- # Body & Antennas Section
78
- with gr.Group():
79
- gr.Markdown("### Body & Antennas")
80
-
81
- body_yaw = gr.Slider(
82
- minimum=-3.2, maximum=3.2, value=0, step=0.01,
83
- label="Body Yaw [rad]"
84
- )
85
- antenna_left = gr.Slider(
86
- minimum=-3.2, maximum=3.2, value=0, step=0.01,
87
- label="Left Antenna [rad]"
88
- )
89
- antenna_right = gr.Slider(
90
- minimum=-3.2, maximum=3.2, value=0, step=0.01,
91
- label="Right Antenna [rad]"
92
- )
93
-
94
- # JavaScript helper - initialization wrapped in async IIFE
95
- def wrap_js(handler_code):
96
- return f"""
97
- (async () => {{
98
- if (!window.reachyApi) {{
99
- window.reachyApi = {{
100
- url: 'http://localhost:8000',
101
- connected: false,
102
- controlling: false,
103
- isRefreshing: false,
104
- currentHeadPose: {{ x: 0, y: 0, z: 0, roll: 0, pitch: 0, yaw: 0 }},
105
- currentBodyYaw: 0,
106
- currentAntennas: [0, 0],
107
- request: async function(endpoint, method = 'GET', body = null) {{
108
- try {{
109
- const options = {{
110
- method: method,
111
- headers: {{'Content-Type': 'application/json'}},
112
- }};
113
- if (body) {{
114
- options.body = JSON.stringify(body);
115
- console.log('Request body:', JSON.stringify(body, null, 2));
116
- }}
117
- const response = await fetch(window.reachyApi.url + endpoint, options);
118
- if (!response.ok) {{
119
- const errorText = await response.text();
120
- console.error('API Error Response:', errorText);
121
- throw new Error(`HTTP ${{response.status}}: ${{errorText}}`);
122
- }}
123
- return await response.json();
124
- }} catch (error) {{
125
- console.error('API Error:', error);
126
- throw error;
127
- }}
128
- }}
129
- }};
130
- }}
131
-
132
- // Execute handler
133
- const handler = {handler_code};
134
- return await handler(...arguments);
135
- }})
136
- """
137
-
138
- # Connect button handler
139
- connect_btn.click(
140
- fn=None,
141
- inputs=[],
142
- outputs=[status_text, motor_status, head_x, head_y, head_z, head_roll, head_pitch, head_yaw, body_yaw, antenna_left, antenna_right],
143
- js=wrap_js("""
144
- async () => {
145
- try {
146
- const state = await window.reachyApi.request('/api/state/full?with_control_mode=true&with_head_pose=true&with_body_yaw=true&with_antenna_positions=true');
147
- const daemonStatus = await window.reachyApi.request('/api/daemon/status');
148
- const daemonState = daemonStatus.state || 'unknown';
149
-
150
- window.reachyApi.connected = true;
151
- const statusMsg = daemonState !== 'running'
152
- ? `Connected | Daemon: ${daemonState} ⚠️ NOT RUNNING`
153
- : `Connected | Daemon: ${daemonState}`;
154
-
155
- const headPose = state.head_pose || {};
156
- const bodyYaw = state.body_yaw || 0;
157
- const antennas = state.antennas_position || [0, 0];
158
- const motorMode = state.control_mode || 'unknown';
159
-
160
- // Initialize current state
161
- window.reachyApi.currentHeadPose = {
162
- x: headPose.x || 0,
163
- y: headPose.y || 0,
164
- z: headPose.z || 0,
165
- roll: headPose.roll || 0,
166
- pitch: headPose.pitch || 0,
167
- yaw: headPose.yaw || 0
168
- };
169
- window.reachyApi.currentBodyYaw = bodyYaw;
170
- window.reachyApi.currentAntennas = [antennas[0] || 0, antennas[1] || 0];
171
-
172
- return [
173
- statusMsg,
174
- `Motor Mode: ${motorMode}`,
175
- headPose.x || 0,
176
- headPose.y || 0,
177
- headPose.z || 0,
178
- headPose.roll || 0,
179
- headPose.pitch || 0,
180
- headPose.yaw || 0,
181
- bodyYaw,
182
- antennas[0] || 0,
183
- antennas[1] || 0
184
- ];
185
- } catch (error) {
186
- window.reachyApi.connected = false;
187
- return ['Connection failed: ' + error.message, 'Disconnected', 0, 0, 0, 0, 0, 0, 0, 0, 0];
188
- }
189
- }
190
- """)
191
- )
192
-
193
- # Motor control handlers
194
- enable_btn.click(
195
- fn=None,
196
- inputs=[],
197
- outputs=[log_output],
198
- js=wrap_js("""
199
- async () => {
200
- if (!window.reachyApi.connected) return 'Not connected';
201
- try {
202
- await window.reachyApi.request('/api/motors/set_mode/enabled', 'POST');
203
- return 'Motors enabled';
204
- } catch (error) {
205
- return 'Error: ' + error.message;
206
- }
207
- }
208
- """)
209
- )
210
-
211
- disable_btn.click(
212
- fn=None,
213
- inputs=[],
214
- outputs=[log_output],
215
- js=wrap_js("""
216
- async () => {
217
- if (!window.reachyApi.connected) return 'Not connected';
218
- try {
219
- await window.reachyApi.request('/api/motors/set_mode/disabled', 'POST');
220
- return 'Motors disabled';
221
- } catch (error) {
222
- return 'Error: ' + error.message;
223
- }
224
- }
225
- """)
226
- )
227
-
228
- # Movement command handlers
229
- wakeup_btn.click(
230
- fn=None,
231
- inputs=[],
232
- outputs=[log_output],
233
- js=wrap_js("""
234
- async () => {
235
- if (!window.reachyApi.connected) return 'Not connected';
236
- try {
237
- const motorStatus = await window.reachyApi.request('/api/motors/status');
238
- if (motorStatus.mode === 'disabled') {
239
- return 'Cannot wake up: motors disabled. Enable motors first!';
240
- }
241
- const result = await window.reachyApi.request('/api/move/play/wake_up', 'POST');
242
- return 'Wake up started (ID: ' + result.uuid.substring(0, 8) + '...)';
243
- } catch (error) {
244
- return 'Error: ' + error.message;
245
- }
246
- }
247
- """)
248
- )
249
-
250
- sleep_btn.click(
251
- fn=None,
252
- inputs=[],
253
- outputs=[log_output],
254
- js=wrap_js("""
255
- async () => {
256
- if (!window.reachyApi.connected) return 'Not connected';
257
- try {
258
- const motorStatus = await window.reachyApi.request('/api/motors/status');
259
- if (motorStatus.mode === 'disabled') {
260
- return 'Cannot sleep: motors disabled. Enable motors first!';
261
- }
262
- const result = await window.reachyApi.request('/api/move/play/goto_sleep', 'POST');
263
- return 'Sleep started (ID: ' + result.uuid.substring(0, 8) + '...)';
264
- } catch (error) {
265
- return 'Error: ' + error.message;
266
- }
267
- }
268
- """)
269
- )
270
-
271
- test_btn.click(
272
- fn=None,
273
- inputs=[],
274
- outputs=[log_output],
275
- js=wrap_js("""
276
- async () => {
277
- if (!window.reachyApi.connected) return 'Not connected';
278
- try {
279
- const result = await window.reachyApi.request('/api/move/goto', 'POST', {
280
- head_pose: {x: 0.02, y: 0, z: 0, roll: 0, pitch: 0, yaw: 0},
281
- duration: 1.0,
282
- interpolation: 'minjerk'
283
- });
284
- return 'Test movement started: ' + result.uuid.substring(0, 8) + '...';
285
- } catch (error) {
286
- return 'Error: ' + error.message;
287
- }
288
- }
289
- """)
290
- )
291
-
292
- # Head pose slider handlers - individual handlers like in working control panel
293
- def create_pose_handler(key):
294
- return f"""
295
- async (value) => {{
296
- if (!window.reachyApi || !window.reachyApi.connected) return;
297
- if (window.reachyApi.isRefreshing) return;
298
- window.reachyApi.controlling = true;
299
- window.reachyApi.currentHeadPose.{key} = value;
300
- try {{
301
- await window.reachyApi.request('/api/move/set_target', 'POST', {{
302
- target_head_pose: window.reachyApi.currentHeadPose,
303
- target_body_yaw: window.reachyApi.currentBodyYaw,
304
- target_antennas: window.reachyApi.currentAntennas
305
- }});
306
- }} catch (error) {{
307
- console.error('Set target error:', error);
308
- }}
309
- }}
310
- """
311
-
312
- head_x.change(fn=None, inputs=[head_x], outputs=[], js=create_pose_handler('x'))
313
- head_y.change(fn=None, inputs=[head_y], outputs=[], js=create_pose_handler('y'))
314
- head_z.change(fn=None, inputs=[head_z], outputs=[], js=create_pose_handler('z'))
315
- head_roll.change(fn=None, inputs=[head_roll], outputs=[], js=create_pose_handler('roll'))
316
- head_pitch.change(fn=None, inputs=[head_pitch], outputs=[], js=create_pose_handler('pitch'))
317
- head_yaw.change(fn=None, inputs=[head_yaw], outputs=[], js=create_pose_handler('yaw'))
318
-
319
- # Body yaw handler
320
- body_yaw.change(
321
- fn=None,
322
- inputs=[body_yaw],
323
- outputs=[],
324
- js="""
325
- async (value) => {
326
- if (!window.reachyApi || !window.reachyApi.connected) return;
327
- if (window.reachyApi.isRefreshing) return;
328
- window.reachyApi.controlling = true;
329
- window.reachyApi.currentBodyYaw = value;
330
- try {
331
- await window.reachyApi.request('/api/move/set_target', 'POST', {
332
- target_head_pose: window.reachyApi.currentHeadPose,
333
- target_body_yaw: window.reachyApi.currentBodyYaw,
334
- target_antennas: window.reachyApi.currentAntennas
335
- });
336
- } catch (error) {
337
- console.error('Set target error:', error);
338
- }
339
- }
340
- """
341
- )
342
-
343
- # Antenna handlers
344
- antenna_left.change(
345
- fn=None,
346
- inputs=[antenna_left],
347
- outputs=[],
348
- js="""
349
- async (value) => {
350
- if (!window.reachyApi || !window.reachyApi.connected) return;
351
- if (window.reachyApi.isRefreshing) return;
352
- window.reachyApi.controlling = true;
353
- window.reachyApi.currentAntennas[0] = value;
354
- try {
355
- await window.reachyApi.request('/api/move/set_target', 'POST', {
356
- target_head_pose: window.reachyApi.currentHeadPose,
357
- target_body_yaw: window.reachyApi.currentBodyYaw,
358
- target_antennas: window.reachyApi.currentAntennas
359
- });
360
- } catch (error) {
361
- console.error('Set target error:', error);
362
- }
363
- }
364
- """
365
- )
366
-
367
- antenna_right.change(
368
- fn=None,
369
- inputs=[antenna_right],
370
- outputs=[],
371
- js="""
372
- async (value) => {
373
- if (!window.reachyApi || !window.reachyApi.connected) return;
374
- if (window.reachyApi.isRefreshing) return;
375
- window.reachyApi.controlling = true;
376
- window.reachyApi.currentAntennas[1] = value;
377
- try {
378
- await window.reachyApi.request('/api/move/set_target', 'POST', {
379
- target_head_pose: window.reachyApi.currentHeadPose,
380
- target_body_yaw: window.reachyApi.currentBodyYaw,
381
- target_antennas: window.reachyApi.currentAntennas
382
- });
383
- } catch (error) {
384
- console.error('Set target error:', error);
385
- }
386
- }
387
- """
388
- )
389
-
390
- # Release handler to stop controlling
391
- all_sliders = [head_x, head_y, head_z, head_roll, head_pitch, head_yaw, body_yaw, antenna_left, antenna_right]
392
- for slider in all_sliders:
393
- slider.release(
394
- fn=None,
395
- inputs=[],
396
- outputs=[],
397
- js="async () => { if (window.reachyApi) window.reachyApi.controlling = false; }"
398
- )
399
-
400
- # Polling timer - updates state at 5Hz (every 200ms)
401
- timer = gr.Timer(value=0.2)
402
- timer.tick(
403
- fn=None,
404
- inputs=[],
405
- outputs=[status_text, motor_status, head_x, head_y, head_z, head_roll, head_pitch, head_yaw, body_yaw, antenna_left, antenna_right],
406
- js=wrap_js("""
407
- async () => {
408
- if (!window.reachyApi.connected) {
409
- return ['Disconnected', 'N/A', null, null, null, null, null, null, null, null, null];
410
- }
411
-
412
- // Don't update sliders if user is controlling
413
- if (window.reachyApi.controlling) {
414
- try {
415
- const state = await window.reachyApi.request('/api/state/full?with_control_mode=true');
416
- const motorMode = state.control_mode || 'unknown';
417
- return ['Connected', `Motor Mode: ${motorMode}`, null, null, null, null, null, null, null, null, null];
418
- } catch (error) {
419
- window.reachyApi.connected = false;
420
- return ['Connection lost', 'Disconnected', null, null, null, null, null, null, null, null, null];
421
- }
422
- }
423
-
424
- try {
425
- // Set flag to prevent input handlers from triggering during update
426
- window.reachyApi.isRefreshing = true;
427
-
428
- const state = await window.reachyApi.request('/api/state/full?with_control_mode=true&with_head_pose=true&with_body_yaw=true&with_antenna_positions=true');
429
- const headPose = state.head_pose || {};
430
- const bodyYaw = state.body_yaw || 0;
431
- const antennas = state.antennas_position || [0, 0];
432
- const motorMode = state.control_mode || 'unknown';
433
-
434
- // Update current state
435
- window.reachyApi.currentHeadPose = {
436
- x: headPose.x || 0,
437
- y: headPose.y || 0,
438
- z: headPose.z || 0,
439
- roll: headPose.roll || 0,
440
- pitch: headPose.pitch || 0,
441
- yaw: headPose.yaw || 0
442
- };
443
- window.reachyApi.currentBodyYaw = bodyYaw;
444
- window.reachyApi.currentAntennas = [antennas[0] || 0, antennas[1] || 0];
445
 
446
- // Clear flag after a short delay
447
- setTimeout(() => { window.reachyApi.isRefreshing = false; }, 100);
 
448
 
449
- return [
450
- 'Connected',
451
- `Motor Mode: ${motorMode}`,
452
- headPose.x || 0,
453
- headPose.y || 0,
454
- headPose.z || 0,
455
- headPose.roll || 0,
456
- headPose.pitch || 0,
457
- headPose.yaw || 0,
458
- bodyYaw,
459
- antennas[0] || 0,
460
- antennas[1] || 0
461
- ];
462
- } catch (error) {
463
- window.reachyApi.connected = false;
464
- window.reachyApi.isRefreshing = false;
465
- return ['Connection lost', 'Disconnected', 0, 0, 0, 0, 0, 0, 0, 0, 0];
466
- }
467
- }
468
- """)
469
- )
470
 
471
- return app
 
472
 
 
 
 
 
 
473
 
474
- # Create and launch the app
475
- app = create_app()
 
476
 
477
  if __name__ == "__main__":
478
  app.launch(
 
1
  """
2
+ Reachy Mini Simple Control Panel - Static File Server for HuggingFace Spaces
3
+ Serves the static HTML/JS control panel that uses WebSockets to connect to localhost:8000
 
 
 
4
  """
5
 
6
  import gradio as gr
7
+ from pathlib import Path
8
 
9
+ # Get the directory where this file is located
10
+ BASE_DIR = Path(__file__).parent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # Read the HTML file
13
+ html_path = BASE_DIR / "index.html"
14
+ js_path = BASE_DIR / "app.js"
15
 
16
+ with open(html_path, 'r') as f:
17
+ html_content = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ with open(js_path, 'r') as f:
20
+ js_content = f.read()
21
 
22
+ # Inject JavaScript directly into HTML
23
+ full_html = html_content.replace(
24
+ '<script src="app.js"></script>',
25
+ f'<script>{js_content}</script>'
26
+ )
27
 
28
+ # Create Gradio app with just the HTML
29
+ with gr.Blocks(title="Reachy Mini Simple Control Panel") as app:
30
+ gr.HTML(full_html)
31
 
32
  if __name__ == "__main__":
33
  app.launch(
index.html ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Reachy Mini Simple Control Panel</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ background: white;
26
+ border-radius: 20px;
27
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
28
+ max-width: 600px;
29
+ width: 100%;
30
+ padding: 40px;
31
+ }
32
+
33
+ h1 {
34
+ color: #333;
35
+ margin-bottom: 10px;
36
+ font-size: 28px;
37
+ }
38
+
39
+ .subtitle {
40
+ color: #666;
41
+ margin-bottom: 30px;
42
+ font-size: 14px;
43
+ }
44
+
45
+ .section {
46
+ margin-bottom: 30px;
47
+ }
48
+
49
+ .section h2 {
50
+ color: #555;
51
+ font-size: 16px;
52
+ margin-bottom: 15px;
53
+ text-transform: uppercase;
54
+ letter-spacing: 1px;
55
+ }
56
+
57
+ .status {
58
+ padding: 12px;
59
+ border-radius: 8px;
60
+ background: #f5f5f5;
61
+ margin-bottom: 10px;
62
+ font-size: 14px;
63
+ display: flex;
64
+ justify-content: space-between;
65
+ align-items: center;
66
+ }
67
+
68
+ .status.connected {
69
+ background: #d4edda;
70
+ color: #155724;
71
+ }
72
+
73
+ .status.disconnected {
74
+ background: #f8d7da;
75
+ color: #721c24;
76
+ }
77
+
78
+ .status-dot {
79
+ width: 10px;
80
+ height: 10px;
81
+ border-radius: 50%;
82
+ margin-right: 8px;
83
+ display: inline-block;
84
+ }
85
+
86
+ .status-dot.green {
87
+ background: #28a745;
88
+ box-shadow: 0 0 10px #28a745;
89
+ }
90
+
91
+ .status-dot.red {
92
+ background: #dc3545;
93
+ }
94
+
95
+ button {
96
+ background: #667eea;
97
+ color: white;
98
+ border: none;
99
+ padding: 12px 24px;
100
+ border-radius: 8px;
101
+ font-size: 14px;
102
+ font-weight: 600;
103
+ cursor: pointer;
104
+ transition: all 0.3s;
105
+ margin-right: 10px;
106
+ margin-bottom: 10px;
107
+ }
108
+
109
+ button:hover {
110
+ background: #5568d3;
111
+ transform: translateY(-2px);
112
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
113
+ }
114
+
115
+ button:active {
116
+ transform: translateY(0);
117
+ }
118
+
119
+ button:disabled {
120
+ background: #ccc;
121
+ cursor: not-allowed;
122
+ transform: none;
123
+ }
124
+
125
+ button.secondary {
126
+ background: #6c757d;
127
+ }
128
+
129
+ button.secondary:hover {
130
+ background: #5a6268;
131
+ }
132
+
133
+ button.danger {
134
+ background: #dc3545;
135
+ }
136
+
137
+ button.danger:hover {
138
+ background: #c82333;
139
+ }
140
+
141
+ .slider-group {
142
+ margin-bottom: 20px;
143
+ }
144
+
145
+ label {
146
+ display: block;
147
+ color: #555;
148
+ font-size: 13px;
149
+ margin-bottom: 8px;
150
+ font-weight: 500;
151
+ }
152
+
153
+ .slider-container {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 10px;
157
+ }
158
+
159
+ input[type="range"] {
160
+ flex: 1;
161
+ height: 6px;
162
+ border-radius: 3px;
163
+ background: #ddd;
164
+ outline: none;
165
+ -webkit-appearance: none;
166
+ }
167
+
168
+ input[type="range"]::-webkit-slider-thumb {
169
+ -webkit-appearance: none;
170
+ appearance: none;
171
+ width: 18px;
172
+ height: 18px;
173
+ border-radius: 50%;
174
+ background: #667eea;
175
+ cursor: pointer;
176
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
177
+ }
178
+
179
+ input[type="range"]::-moz-range-thumb {
180
+ width: 18px;
181
+ height: 18px;
182
+ border-radius: 50%;
183
+ background: #667eea;
184
+ cursor: pointer;
185
+ border: none;
186
+ }
187
+
188
+ .slider-value {
189
+ min-width: 60px;
190
+ text-align: right;
191
+ color: #666;
192
+ font-size: 13px;
193
+ font-family: 'Courier New', monospace;
194
+ }
195
+
196
+ .button-row {
197
+ display: flex;
198
+ flex-wrap: wrap;
199
+ gap: 10px;
200
+ }
201
+
202
+ @media (max-width: 600px) {
203
+ .container {
204
+ padding: 20px;
205
+ }
206
+
207
+ h1 {
208
+ font-size: 24px;
209
+ }
210
+
211
+ button {
212
+ width: 100%;
213
+ margin-right: 0;
214
+ }
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <div class="container">
220
+ <h1>πŸ€– Reachy Mini Control Panel</h1>
221
+ <p class="subtitle">WebSocket Real-time Control</p>
222
+
223
+ <!-- Connection Status -->
224
+ <div class="section">
225
+ <div id="connectionStatus" class="status disconnected">
226
+ <span><span class="status-dot red"></span>Disconnected</span>
227
+ </div>
228
+ </div>
229
+
230
+ <!-- Movement Commands -->
231
+ <div class="section">
232
+ <h2>Movement Commands</h2>
233
+ <div class="button-row">
234
+ <button id="wakeUpBtn" disabled>Wake Up</button>
235
+ <button id="sleepBtn" class="secondary" disabled>Go to Sleep</button>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- Head Pose Control -->
240
+ <div class="section">
241
+ <h2>Head Pose Control</h2>
242
+
243
+ <div class="slider-group">
244
+ <label>X (forward/backward) [m]</label>
245
+ <div class="slider-container">
246
+ <input type="range" id="headX" min="-0.2" max="0.2" step="0.001" value="0" disabled>
247
+ <span class="slider-value" id="headXValue">0.000</span>
248
+ </div>
249
+ </div>
250
+
251
+ <div class="slider-group">
252
+ <label>Y (left/right) [m]</label>
253
+ <div class="slider-container">
254
+ <input type="range" id="headY" min="-0.2" max="0.2" step="0.001" value="0" disabled>
255
+ <span class="slider-value" id="headYValue">0.000</span>
256
+ </div>
257
+ </div>
258
+
259
+ <div class="slider-group">
260
+ <label>Z (up/down) [m]</label>
261
+ <div class="slider-container">
262
+ <input type="range" id="headZ" min="-0.2" max="0.2" step="0.001" value="0" disabled>
263
+ <span class="slider-value" id="headZValue">0.000</span>
264
+ </div>
265
+ </div>
266
+
267
+ <div class="slider-group">
268
+ <label>Roll [rad]</label>
269
+ <div class="slider-container">
270
+ <input type="range" id="headRoll" min="-3.2" max="3.2" step="0.01" value="0" disabled>
271
+ <span class="slider-value" id="headRollValue">0.00</span>
272
+ </div>
273
+ </div>
274
+
275
+ <div class="slider-group">
276
+ <label>Pitch [rad]</label>
277
+ <div class="slider-container">
278
+ <input type="range" id="headPitch" min="-3.2" max="3.2" step="0.01" value="0" disabled>
279
+ <span class="slider-value" id="headPitchValue">0.00</span>
280
+ </div>
281
+ </div>
282
+
283
+ <div class="slider-group">
284
+ <label>Yaw [rad]</label>
285
+ <div class="slider-container">
286
+ <input type="range" id="headYaw" min="-3.2" max="3.2" step="0.01" value="0" disabled>
287
+ <span class="slider-value" id="headYawValue">0.00</span>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ <!-- Body & Antennas -->
293
+ <div class="section">
294
+ <h2>Body & Antennas</h2>
295
+
296
+ <div class="slider-group">
297
+ <label>Body Yaw [rad]</label>
298
+ <div class="slider-container">
299
+ <input type="range" id="bodyYaw" min="-3.2" max="3.2" step="0.01" value="0" disabled>
300
+ <span class="slider-value" id="bodyYawValue">0.00</span>
301
+ </div>
302
+ </div>
303
+
304
+ <div class="slider-group">
305
+ <label>Left Antenna [rad]</label>
306
+ <div class="slider-container">
307
+ <input type="range" id="antennaLeft" min="-3.2" max="3.2" step="0.01" value="0" disabled>
308
+ <span class="slider-value" id="antennaLeftValue">0.00</span>
309
+ </div>
310
+ </div>
311
+
312
+ <div class="slider-group">
313
+ <label>Right Antenna [rad]</label>
314
+ <div class="slider-container">
315
+ <input type="range" id="antennaRight" min="-3.2" max="3.2" step="0.01" value="0" disabled>
316
+ <span class="slider-value" id="antennaRightValue">0.00</span>
317
+ </div>
318
+ </div>
319
+ </div>
320
+ </div>
321
+
322
+ <script src="app.js"></script>
323
+ </body>
324
+ </html>
server.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Simple HTTP server to serve the static Reachy Mini Control Panel.
4
+ This is only needed if you want to serve the files, otherwise you can open index.html directly.
5
+ """
6
+
7
+ import http.server
8
+ import socketserver
9
+ import os
10
+ from pathlib import Path
11
+
12
+ PORT = 7860
13
+ DIRECTORY = Path(__file__).parent
14
+
15
+ class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, directory=str(DIRECTORY), **kwargs)
18
+
19
+ def end_headers(self):
20
+ # Add CORS headers to allow localhost:8000 WebSocket connections
21
+ self.send_header('Access-Control-Allow-Origin', '*')
22
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
23
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
24
+ super().end_headers()
25
+
26
+ def main():
27
+ os.chdir(DIRECTORY)
28
+
29
+ with socketserver.TCPServer(("", PORT), MyHTTPRequestHandler) as httpd:
30
+ print(f"πŸ€– Reachy Mini Control Panel")
31
+ print(f"πŸ“‘ Serving at http://localhost:{PORT}")
32
+ print(f"πŸ“ Directory: {DIRECTORY}")
33
+ print(f"\nπŸ”— Open http://localhost:{PORT} in your browser")
34
+ print(f"⚠️ Make sure robot daemon is running on localhost:8000")
35
+ print(f"\nPress Ctrl+C to stop\n")
36
+
37
+ try:
38
+ httpd.serve_forever()
39
+ except KeyboardInterrupt:
40
+ print("\n\nπŸ‘‹ Shutting down server...")
41
+
42
+ if __name__ == "__main__":
43
+ main()