cduss commited on
Commit
5c856dc
·
1 Parent(s): 6fbdb1e

feat : offline mic cam check

Browse files
README.md CHANGED
@@ -14,106 +14,126 @@ tags:
14
  - ci
15
  ---
16
 
17
- # Reachy Mini Debug & CI Testbench
18
-
19
- A comprehensive web-based debugging and CI validation tool for Reachy Mini.
20
-
21
- ## Features
22
-
23
- ### Robot Status
24
- - Real-time head pose display (roll, pitch, yaw)
25
- - Detailed motor positions for all 9 motors
26
- - Connection status indicator
27
-
28
- ### Movement Control
29
- - Head position control with sliders (roll, pitch, yaw)
30
- - Antenna position control
31
- - Quick actions: Go to Zero, Wake Up, Go to Sleep
32
- - Adjustable movement duration
33
-
34
- ### Camera
35
- - Live camera stream (MJPEG)
36
- - Capture and save images
37
- - Download captured images
38
- - List and manage saved captures
39
-
40
- ### Audio Recording
41
- - Record audio from the robot's microphone
42
- - List saved recordings with metadata
43
- - Play recordings through the robot's speaker
44
- - Download recordings as WAV files
45
-
46
- ### Rotation Validation Test
47
- Automated test for validating head rotation accuracy:
48
- 1. Moves head to zero position and captures image
49
- 2. Rolls head by specified angle
50
- 3. Captures second image
51
- 4. Computes rotation between images using ORB feature matching
52
- 5. Compares expected vs actual rotation
53
- 6. Returns PASS/FAIL with detailed metrics
54
-
55
- ## Installation
56
 
57
- ```bash
58
- pip install -e .
59
- ```
60
 
61
- ## Usage
 
 
62
 
63
- ### As a Reachy Mini App (daemon on)
64
- The app registers as a Reachy Mini app and can be launched via the daemon:
65
  ```bash
 
66
  reachy-mini app run reachy_mini_testbench
67
  ```
68
 
69
- ### Direct Launch
70
- ```bash
71
- python -m reachy_mini_testbench.main
72
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- Then open http://localhost:8042 in your browser.
75
-
76
- ## API Endpoints
77
-
78
- ### Status
79
- - `GET /api/status` - Get robot status and head pose
80
- - `GET /api/motor_status` - Get detailed motor positions
81
-
82
- ### Movement
83
- - `POST /api/move_head` - Move head to specified orientation
84
- - `POST /api/move_antennas` - Move antennas to specified positions
85
- - `POST /api/go_to_zero` - Move to neutral position
86
- - `POST /api/wake_up` - Execute wake up behavior
87
- - `POST /api/go_to_sleep` - Execute sleep behavior
88
-
89
- ### Camera
90
- - `GET /api/camera/stream` - MJPEG camera stream
91
- - `GET /api/camera/capture` - Capture single frame as base64
92
- - `POST /api/camera/save` - Save current frame to file
93
- - `GET /api/camera/list` - List saved captures
94
- - `GET /api/camera/download/{filename}` - Download capture
95
- - `DELETE /api/camera/delete/{filename}` - Delete capture
96
-
97
- ### Audio
98
- - `POST /api/audio/start_recording` - Start recording
99
- - `POST /api/audio/stop_recording` - Stop and save recording
100
- - `GET /api/audio/list` - List recordings
101
- - `GET /api/audio/download/{filename}` - Download recording
102
- - `POST /api/audio/play/{filename}` - Play recording on robot
103
- - `DELETE /api/audio/delete/{filename}` - Delete recording
104
-
105
- ### Validation Tests
106
- - `POST /api/test/rotation_validation` - Run rotation validation test
107
- - `GET /api/test/last_rotation_result` - Get last test result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  ## File Storage
110
 
111
  - Recordings: `/tmp/reachy_mini_testbench/recordings/`
112
  - Captures: `/tmp/reachy_mini_testbench/captures/`
113
-
114
- ## Dependencies
115
-
116
- - reachy-mini
117
- - opencv-python
118
- - soundfile
119
- - scipy
 
14
  - ci
15
  ---
16
 
17
+ # Reachy Mini Testbench
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
+ Debug and CI validation tool for Reachy Mini. Works in two modes depending on daemon status.
 
 
20
 
21
+ **Access:** http://localhost:8042
22
+
23
+ ## Quick Start
24
 
 
 
25
  ```bash
26
+ pip install /path/to/reachy_mini_testbench
27
  reachy-mini app run reachy_mini_testbench
28
  ```
29
 
30
+ ---
31
+
32
+ ## Daemon ON Mode
33
+
34
+ When the daemon backend is running, test robot functionality through the SDK.
35
+
36
+ | Feature | Description |
37
+ |---------|-------------|
38
+ | **Daemon Status** | Full status: version, state, IP, control loop frequency, errors |
39
+ | **System Stats** | Live CPU/RAM/disk in header (polls every 5s) |
40
+ | **Robot Status** | Current head pose (roll/pitch/yaw) |
41
+ | **Motor Status** | All 9 motor positions |
42
+ | **Movement Control** | Head sliders, antenna control, wake/sleep/zero |
43
+ | **Continuous Movement** | Sinusoidal test motion at 20Hz |
44
+ | **Camera** | Live stream, capture/save images |
45
+ | **Audio** | Record/playback through robot's mic/speaker |
46
+ | **Rotation Validation** | Automated test: captures at zero and rolled positions, computes rotation error |
47
+
48
+ ---
49
+
50
+ ## Daemon OFF Mode
51
+
52
+ Direct hardware access for diagnostics when daemon is not running.
53
+
54
+ | Feature | Description |
55
+ |---------|-------------|
56
+ | **Camera Check** | Direct OpenCV capture - verifies camera without daemon/WebRTC |
57
+ | **Microphone Check** | Direct sounddevice recording - shows peak level (dB) |
58
+ | **Motor Scan** | Detect motors on bus (IDs 10-18) |
59
+ | **Configuration Check** | Verify motor configs match expected values |
60
+ | **Baudrate Scanner** | Find misconfigured motors at wrong baudrates |
61
+ | **Motor Reflash** | Reconfigure motor ID, baudrate, settings to preset |
62
 
63
+ ---
64
+
65
+ ## Compatibility
66
+
67
+ | Variant | Daemon ON | Daemon OFF |
68
+ |---------|-----------|------------|
69
+ | Lite | All features | All features |
70
+ | Wireless | All features | All features |
71
+ | Simulation | All features | N/A (no hardware) |
72
+
73
+ ---
74
+
75
+ ## What Each Test Validates
76
+
77
+ | Test | Mode | Validates |
78
+ |------|------|-----------|
79
+ | Camera Check | OFF | Direct camera hardware |
80
+ | Mic Check | OFF | Direct microphone hardware |
81
+ | Motor Scan | OFF | Serial bus communication |
82
+ | Config Check | OFF | Motor EEPROM settings |
83
+ | Head Movement | ON | Kinematics + motor control |
84
+ | Camera Stream | ON | WebRTC/GStreamer pipeline |
85
+ | Audio Recording | ON | Media backend pipeline |
86
+ | Rotation Test | ON | Full vision + motion system |
87
+
88
+ ---
89
+
90
+ ## API Reference
91
+
92
+ ### Connection
93
+ - `GET /api/connection_status` - Check daemon connection
94
+ - `GET /api/system_stats` - CPU/RAM/disk usage
95
+ - `GET /api/daemon_status` - Full daemon status
96
+
97
+ ### Robot (daemon ON)
98
+ - `GET /api/status` - Head pose
99
+ - `GET /api/motor_status` - Motor positions
100
+ - `GET /api/robot_info` - Variant/version info
101
+ - `POST /api/move_head` - Move head
102
+ - `POST /api/move_antennas` - Move antennas
103
+ - `POST /api/go_to_zero` - Neutral position
104
+ - `POST /api/wake_up` / `POST /api/go_to_sleep`
105
+ - `POST /api/continuous_move/start` / `stop`
106
+
107
+ ### Camera (daemon ON)
108
+ - `GET /api/camera/stream` - MJPEG stream
109
+ - `POST /api/camera/save` - Save frame
110
+ - `GET /api/camera/list` / `download/{f}` / `DELETE delete/{f}`
111
+
112
+ ### Audio (daemon ON)
113
+ - `POST /api/audio/start_recording` / `stop_recording`
114
+ - `GET /api/audio/list` / `download/{f}` / `POST play/{f}` / `DELETE delete/{f}`
115
+
116
+ ### Hardware Check (daemon OFF)
117
+ - `GET /api/hardware/camera_check` - Direct camera test
118
+ - `GET /api/hardware/mic_check` - Direct mic test
119
+ - `GET /api/hardware/list_audio_devices` - List audio devices
120
+
121
+ ### Motor Diagnostics (daemon OFF)
122
+ - `GET /api/motors/serial_info` - Serial port info
123
+ - `GET /api/motors/scan` - Scan for motors
124
+ - `GET /api/motors/scan_baudrates` - Scan all baudrates
125
+ - `GET /api/motors/check_all` - Check configurations
126
+ - `POST /api/motors/led` - Control motor LED
127
+ - `GET /api/motors/presets` - Available motor presets
128
+ - `POST /api/motors/reflash` - Reflash motor config
129
+
130
+ ### Tests
131
+ - `POST /api/test/rotation_validation` - Run rotation test
132
+ - `GET /api/test/last_rotation_result` - Last test result
133
+
134
+ ---
135
 
136
  ## File Storage
137
 
138
  - Recordings: `/tmp/reachy_mini_testbench/recordings/`
139
  - Captures: `/tmp/reachy_mini_testbench/captures/`
 
 
 
 
 
 
 
pyproject.toml CHANGED
@@ -13,7 +13,10 @@ dependencies = [
13
  "reachy-mini",
14
  "opencv-python",
15
  "soundfile",
 
16
  "scipy",
 
 
17
  ]
18
  keywords = ["reachy-mini-app", "debug", "testing", "ci", "validation"]
19
 
 
13
  "reachy-mini",
14
  "opencv-python",
15
  "soundfile",
16
+ "sounddevice",
17
  "scipy",
18
+ "psutil",
19
+ "aiohttp",
20
  ]
21
  keywords = ["reachy-mini-app", "debug", "testing", "ci", "validation"]
22
 
reachy_mini_testbench/main.py CHANGED
@@ -25,6 +25,7 @@ from typing import Any
25
 
26
  import cv2
27
  import numpy as np
 
28
  import soundfile as sf
29
  import uvicorn
30
  from fastapi import FastAPI, HTTPException, Response
@@ -1279,6 +1280,154 @@ async def reflash_motor(req: MotorReflashRequest):
1279
  raise HTTPException(500, f"Unexpected error during reflash: {str(e)}")
1280
 
1281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1282
  # ==================== ReachyMiniApp Wrapper for App Manager ====================
1283
 
1284
  from reachy_mini.apps.app import ReachyMiniApp
 
25
 
26
  import cv2
27
  import numpy as np
28
+ import sounddevice as sd
29
  import soundfile as sf
30
  import uvicorn
31
  from fastapi import FastAPI, HTTPException, Response
 
1280
  raise HTTPException(500, f"Unexpected error during reflash: {str(e)}")
1281
 
1282
 
1283
+ # ==================== Hardware Check Endpoints (work without daemon) ====================
1284
+
1285
+ @app.get("/api/hardware/camera_check")
1286
+ async def check_camera():
1287
+ """Check camera by capturing a frame directly (no daemon/webrtc).
1288
+
1289
+ Uses OpenCV to directly access the camera device.
1290
+ Returns a base64-encoded JPEG image.
1291
+ """
1292
+ cap = None
1293
+ try:
1294
+ # Try common camera indices (0 is usually the default)
1295
+ for camera_idx in [0, 1, 2]:
1296
+ cap = cv2.VideoCapture(camera_idx)
1297
+ if cap.isOpened():
1298
+ break
1299
+ cap.release()
1300
+ cap = None
1301
+
1302
+ if cap is None:
1303
+ raise HTTPException(503, "No camera found. Tried indices 0, 1, 2.")
1304
+
1305
+ # Let camera warm up
1306
+ for _ in range(5):
1307
+ cap.read()
1308
+
1309
+ # Capture frame
1310
+ ret, frame = cap.read()
1311
+ if not ret or frame is None:
1312
+ raise HTTPException(503, "Camera opened but failed to capture frame")
1313
+
1314
+ # Get camera properties
1315
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
1316
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
1317
+ fps = cap.get(cv2.CAP_PROP_FPS)
1318
+
1319
+ # Encode to JPEG
1320
+ _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
1321
+ b64_image = base64.b64encode(buffer).decode('utf-8')
1322
+
1323
+ return {
1324
+ "success": True,
1325
+ "camera_index": camera_idx,
1326
+ "resolution": f"{width}x{height}",
1327
+ "fps": fps if fps > 0 else "unknown",
1328
+ "image": b64_image,
1329
+ "timestamp": datetime.now().isoformat()
1330
+ }
1331
+
1332
+ except HTTPException:
1333
+ raise
1334
+ except Exception as e:
1335
+ logger.error(f"Camera check error: {e}")
1336
+ raise HTTPException(500, f"Camera check failed: {str(e)}")
1337
+ finally:
1338
+ if cap is not None:
1339
+ cap.release()
1340
+
1341
+
1342
+ @app.get("/api/hardware/mic_check")
1343
+ async def check_microphone(duration: float = 1.0):
1344
+ """Check microphone by recording a short sample directly (no daemon/webrtc).
1345
+
1346
+ Uses sounddevice for direct microphone access.
1347
+ Returns info about the recording and peak level.
1348
+ """
1349
+ try:
1350
+ # Get default input device info
1351
+ device_info = sd.query_devices(kind='input')
1352
+ device_name = device_info['name']
1353
+ max_channels = device_info['max_input_channels']
1354
+ default_sr = int(device_info['default_samplerate'])
1355
+
1356
+ # Use default sample rate, mono, duration clamped to 0.5-5s
1357
+ duration = max(0.5, min(5.0, duration))
1358
+ samplerate = default_sr
1359
+
1360
+ # Record audio
1361
+ logger.info(f"Recording {duration}s from mic: {device_name}")
1362
+ recording = sd.rec(
1363
+ int(duration * samplerate),
1364
+ samplerate=samplerate,
1365
+ channels=1,
1366
+ dtype='float32'
1367
+ )
1368
+ sd.wait() # Wait for recording to complete
1369
+
1370
+ # Calculate audio statistics
1371
+ audio_data = recording.flatten()
1372
+ peak_level = float(np.max(np.abs(audio_data)))
1373
+ rms_level = float(np.sqrt(np.mean(audio_data ** 2)))
1374
+
1375
+ # Convert to dB (avoid log of 0)
1376
+ peak_db = 20 * np.log10(peak_level + 1e-10)
1377
+ rms_db = 20 * np.log10(rms_level + 1e-10)
1378
+
1379
+ # Determine if we detected sound (peak above noise floor)
1380
+ sound_detected = peak_db > -40 # -40 dB is typical noise floor
1381
+
1382
+ return {
1383
+ "success": True,
1384
+ "device_name": device_name,
1385
+ "max_channels": max_channels,
1386
+ "samplerate": samplerate,
1387
+ "duration_recorded": duration,
1388
+ "samples": len(audio_data),
1389
+ "peak_level": round(peak_level, 4),
1390
+ "peak_db": round(peak_db, 1),
1391
+ "rms_level": round(rms_level, 4),
1392
+ "rms_db": round(rms_db, 1),
1393
+ "sound_detected": sound_detected,
1394
+ "timestamp": datetime.now().isoformat()
1395
+ }
1396
+
1397
+ except sd.PortAudioError as e:
1398
+ logger.error(f"Microphone access error: {e}")
1399
+ raise HTTPException(503, f"No microphone available: {str(e)}")
1400
+ except Exception as e:
1401
+ logger.error(f"Mic check error: {e}")
1402
+ raise HTTPException(500, f"Microphone check failed: {str(e)}")
1403
+
1404
+
1405
+ @app.get("/api/hardware/list_audio_devices")
1406
+ async def list_audio_devices():
1407
+ """List all available audio input devices."""
1408
+ try:
1409
+ devices = sd.query_devices()
1410
+ input_devices = []
1411
+
1412
+ for idx, dev in enumerate(devices):
1413
+ if dev['max_input_channels'] > 0:
1414
+ input_devices.append({
1415
+ "index": idx,
1416
+ "name": dev['name'],
1417
+ "channels": dev['max_input_channels'],
1418
+ "default_samplerate": int(dev['default_samplerate']),
1419
+ "is_default": idx == sd.default.device[0] # default input device
1420
+ })
1421
+
1422
+ return {
1423
+ "input_devices": input_devices,
1424
+ "default_input_index": sd.default.device[0]
1425
+ }
1426
+ except Exception as e:
1427
+ logger.error(f"List audio devices error: {e}")
1428
+ raise HTTPException(500, f"Failed to list audio devices: {str(e)}")
1429
+
1430
+
1431
  # ==================== ReachyMiniApp Wrapper for App Manager ====================
1432
 
1433
  from reachy_mini.apps.app import ReachyMiniApp
reachy_mini_testbench/static/index.html CHANGED
@@ -174,6 +174,47 @@
174
  </div>
175
  </section>
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  <!-- Motor Diagnostics Panel (daemon-off only) -->
178
  <section class="panel daemon-off-only" id="diagnostics-panel">
179
  <h2>Motor Diagnostics</h2>
 
174
  </div>
175
  </section>
176
 
177
+ <!-- Hardware Check Panel (daemon-off only) -->
178
+ <section class="panel daemon-off-only" id="hardware-check-panel">
179
+ <h2>Hardware Check</h2>
180
+ <div class="panel-content">
181
+ <p class="panel-description">
182
+ Direct camera and microphone access (no daemon/webrtc).
183
+ Use this to verify hardware works independently.
184
+ </p>
185
+
186
+ <div class="control-group">
187
+ <h3>Camera Check</h3>
188
+ <button onclick="checkCamera()" class="btn btn-primary" id="camera-check-btn">Check Camera</button>
189
+ <div id="camera-check-result" class="hardware-check-result hidden">
190
+ <div class="camera-preview-container">
191
+ <img id="camera-check-preview" src="" alt="Camera preview">
192
+ </div>
193
+ <div class="hardware-check-info" id="camera-check-info">
194
+ <!-- Info will be populated by JS -->
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="control-group">
200
+ <h3>Microphone Check</h3>
201
+ <p class="panel-description">Records 1 second of audio and shows level</p>
202
+ <button onclick="checkMicrophone()" class="btn btn-primary" id="mic-check-btn">Check Microphone</button>
203
+ <div id="mic-check-result" class="hardware-check-result hidden">
204
+ <div class="mic-level-container">
205
+ <div class="mic-level-bar">
206
+ <div class="mic-level-fill" id="mic-level-fill"></div>
207
+ </div>
208
+ <span class="mic-level-value" id="mic-level-value">-- dB</span>
209
+ </div>
210
+ <div class="hardware-check-info" id="mic-check-info">
211
+ <!-- Info will be populated by JS -->
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </section>
217
+
218
  <!-- Motor Diagnostics Panel (daemon-off only) -->
219
  <section class="panel daemon-off-only" id="diagnostics-panel">
220
  <h2>Motor Diagnostics</h2>
reachy_mini_testbench/static/main.js CHANGED
@@ -847,6 +847,157 @@ async function reflashMotor() {
847
  }
848
  }
849
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
850
  // ==================== Connection Status ====================
851
 
852
  async function fetchConnectionStatus() {
 
847
  }
848
  }
849
 
850
+ // ==================== Hardware Check Functions ====================
851
+
852
+ async function checkCamera() {
853
+ const btn = document.getElementById('camera-check-btn');
854
+ const resultDiv = document.getElementById('camera-check-result');
855
+ const previewImg = document.getElementById('camera-check-preview');
856
+ const infoDiv = document.getElementById('camera-check-info');
857
+
858
+ try {
859
+ btn.disabled = true;
860
+ btn.textContent = 'Checking...';
861
+ resultDiv.classList.add('hidden');
862
+ showToast('Checking camera...', 'info');
863
+
864
+ const data = await apiCall('/api/hardware/camera_check');
865
+
866
+ // Show result
867
+ resultDiv.classList.remove('hidden');
868
+
869
+ // Set image
870
+ previewImg.src = `data:image/jpeg;base64,${data.image}`;
871
+
872
+ // Build info HTML
873
+ infoDiv.innerHTML = `
874
+ <div class="check-info-item ok">
875
+ <span class="label">Status</span>
876
+ <span class="value">Camera Working</span>
877
+ </div>
878
+ <div class="check-info-item">
879
+ <span class="label">Device Index</span>
880
+ <span class="value">${data.camera_index}</span>
881
+ </div>
882
+ <div class="check-info-item">
883
+ <span class="label">Resolution</span>
884
+ <span class="value">${data.resolution}</span>
885
+ </div>
886
+ <div class="check-info-item">
887
+ <span class="label">FPS</span>
888
+ <span class="value">${typeof data.fps === 'number' ? data.fps.toFixed(0) : data.fps}</span>
889
+ </div>
890
+ `;
891
+
892
+ showToast('Camera check passed!', 'success');
893
+
894
+ } catch (error) {
895
+ resultDiv.classList.remove('hidden');
896
+ previewImg.src = '';
897
+ infoDiv.innerHTML = `
898
+ <div class="check-info-item error">
899
+ <span class="label">Status</span>
900
+ <span class="value">Camera Failed</span>
901
+ </div>
902
+ <div class="check-info-item error">
903
+ <span class="label">Error</span>
904
+ <span class="value">${error.message}</span>
905
+ </div>
906
+ `;
907
+ console.error('Camera check failed:', error);
908
+ } finally {
909
+ btn.disabled = false;
910
+ btn.textContent = 'Check Camera';
911
+ }
912
+ }
913
+
914
+ async function checkMicrophone() {
915
+ const btn = document.getElementById('mic-check-btn');
916
+ const resultDiv = document.getElementById('mic-check-result');
917
+ const levelFill = document.getElementById('mic-level-fill');
918
+ const levelValue = document.getElementById('mic-level-value');
919
+ const infoDiv = document.getElementById('mic-check-info');
920
+
921
+ try {
922
+ btn.disabled = true;
923
+ btn.textContent = 'Recording...';
924
+ resultDiv.classList.add('hidden');
925
+ showToast('Recording from microphone...', 'info');
926
+
927
+ const data = await apiCall('/api/hardware/mic_check?duration=1.0');
928
+
929
+ // Show result
930
+ resultDiv.classList.remove('hidden');
931
+
932
+ // Calculate level bar width (map -60dB to 0dB to 0% to 100%)
933
+ const peakDb = data.peak_db;
934
+ const barPercent = Math.max(0, Math.min(100, ((peakDb + 60) / 60) * 100));
935
+ levelFill.style.width = `${barPercent}%`;
936
+
937
+ // Color based on level
938
+ if (data.sound_detected) {
939
+ levelFill.className = 'mic-level-fill detected';
940
+ } else {
941
+ levelFill.className = 'mic-level-fill quiet';
942
+ }
943
+
944
+ levelValue.textContent = `${peakDb.toFixed(1)} dB`;
945
+
946
+ // Build info HTML
947
+ const soundStatus = data.sound_detected ? 'Sound Detected' : 'Very Quiet/No Sound';
948
+ const statusClass = data.sound_detected ? 'ok' : 'warning';
949
+
950
+ infoDiv.innerHTML = `
951
+ <div class="check-info-item ${statusClass}">
952
+ <span class="label">Status</span>
953
+ <span class="value">${soundStatus}</span>
954
+ </div>
955
+ <div class="check-info-item">
956
+ <span class="label">Device</span>
957
+ <span class="value">${data.device_name}</span>
958
+ </div>
959
+ <div class="check-info-item">
960
+ <span class="label">Sample Rate</span>
961
+ <span class="value">${data.samplerate} Hz</span>
962
+ </div>
963
+ <div class="check-info-item">
964
+ <span class="label">Peak Level</span>
965
+ <span class="value">${data.peak_db.toFixed(1)} dB</span>
966
+ </div>
967
+ <div class="check-info-item">
968
+ <span class="label">RMS Level</span>
969
+ <span class="value">${data.rms_db.toFixed(1)} dB</span>
970
+ </div>
971
+ `;
972
+
973
+ if (data.sound_detected) {
974
+ showToast('Microphone check passed - sound detected!', 'success');
975
+ } else {
976
+ showToast('Microphone working but very quiet', 'warning');
977
+ }
978
+
979
+ } catch (error) {
980
+ resultDiv.classList.remove('hidden');
981
+ levelFill.style.width = '0%';
982
+ levelFill.className = 'mic-level-fill';
983
+ levelValue.textContent = '-- dB';
984
+ infoDiv.innerHTML = `
985
+ <div class="check-info-item error">
986
+ <span class="label">Status</span>
987
+ <span class="value">Microphone Failed</span>
988
+ </div>
989
+ <div class="check-info-item error">
990
+ <span class="label">Error</span>
991
+ <span class="value">${error.message}</span>
992
+ </div>
993
+ `;
994
+ console.error('Microphone check failed:', error);
995
+ } finally {
996
+ btn.disabled = false;
997
+ btn.textContent = 'Check Microphone';
998
+ }
999
+ }
1000
+
1001
  // ==================== Connection Status ====================
1002
 
1003
  async function fetchConnectionStatus() {
reachy_mini_testbench/static/style.css CHANGED
@@ -852,6 +852,125 @@ main {
852
  }
853
  }
854
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
855
  /* ==================== Motor Diagnostics ==================== */
856
  .serial-info {
857
  display: flex;
 
852
  }
853
  }
854
 
855
+ /* ==================== Hardware Check ==================== */
856
+ .hardware-check-result {
857
+ margin-top: 1rem;
858
+ padding: 1rem;
859
+ background: var(--bg-panel);
860
+ border-radius: 8px;
861
+ }
862
+
863
+ .hardware-check-result.hidden {
864
+ display: none;
865
+ }
866
+
867
+ .camera-preview-container {
868
+ margin-bottom: 1rem;
869
+ background: #000;
870
+ border-radius: 6px;
871
+ overflow: hidden;
872
+ aspect-ratio: 4/3;
873
+ }
874
+
875
+ .camera-preview-container img {
876
+ width: 100%;
877
+ height: 100%;
878
+ object-fit: contain;
879
+ }
880
+
881
+ .hardware-check-info {
882
+ display: grid;
883
+ grid-template-columns: repeat(2, 1fr);
884
+ gap: 0.5rem;
885
+ }
886
+
887
+ .check-info-item {
888
+ display: flex;
889
+ flex-direction: column;
890
+ padding: 0.5rem 0.75rem;
891
+ background: var(--bg-secondary);
892
+ border-radius: 4px;
893
+ border-left: 3px solid var(--border);
894
+ }
895
+
896
+ .check-info-item.ok {
897
+ border-left-color: var(--success);
898
+ }
899
+
900
+ .check-info-item.warning {
901
+ border-left-color: var(--warning);
902
+ }
903
+
904
+ .check-info-item.error {
905
+ border-left-color: var(--error);
906
+ }
907
+
908
+ .check-info-item .label {
909
+ font-size: 0.65rem;
910
+ color: var(--text-secondary);
911
+ text-transform: uppercase;
912
+ margin-bottom: 0.2rem;
913
+ }
914
+
915
+ .check-info-item .value {
916
+ font-size: 0.85rem;
917
+ font-weight: 500;
918
+ color: var(--text-primary);
919
+ }
920
+
921
+ .check-info-item.ok .value {
922
+ color: var(--success);
923
+ }
924
+
925
+ .check-info-item.warning .value {
926
+ color: var(--warning);
927
+ }
928
+
929
+ .check-info-item.error .value {
930
+ color: var(--error);
931
+ }
932
+
933
+ /* Microphone Level Bar */
934
+ .mic-level-container {
935
+ display: flex;
936
+ align-items: center;
937
+ gap: 1rem;
938
+ margin-bottom: 1rem;
939
+ }
940
+
941
+ .mic-level-bar {
942
+ flex: 1;
943
+ height: 24px;
944
+ background: var(--bg-secondary);
945
+ border-radius: 12px;
946
+ overflow: hidden;
947
+ border: 1px solid var(--border);
948
+ }
949
+
950
+ .mic-level-fill {
951
+ height: 100%;
952
+ width: 0%;
953
+ background: var(--border);
954
+ transition: width 0.3s ease, background 0.3s ease;
955
+ border-radius: 12px;
956
+ }
957
+
958
+ .mic-level-fill.detected {
959
+ background: linear-gradient(90deg, var(--success), #22c55e);
960
+ }
961
+
962
+ .mic-level-fill.quiet {
963
+ background: linear-gradient(90deg, var(--warning), #f59e0b);
964
+ }
965
+
966
+ .mic-level-value {
967
+ font-size: 1rem;
968
+ font-weight: 600;
969
+ color: var(--text-primary);
970
+ min-width: 70px;
971
+ text-align: right;
972
+ }
973
+
974
  /* ==================== Motor Diagnostics ==================== */
975
  .serial-info {
976
  display: flex;