Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
*.stl filter=lfs diff=lfs merge=lfs -text
*.npz filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
src/reachy_mini/daemon/app/wasm/bin/** filter=lfs diff=lfs merge=lfs -text
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "src/reachy_mini/daemon/app/wasm/mujoco_web"]
path = src/reachy_mini/daemon/app/wasm/mujoco_web
url = [email protected]:pollen-robotics/mujoco_web.git
branch = mirror
93 changes: 93 additions & 0 deletions src/reachy_mini/daemon/app/dashboard/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,97 @@ ul#examples-list li a {

ul#examples-list li a:hover {
text-decoration: underline;
}

/* Tab styles */
.tab-container {
margin: 0 2em;
}

.tab-nav {
display: flex;
border-bottom: 2px solid #ddd;
margin-bottom: 1em;
}

.tab-button {
padding: 0.8em 1.5em;
border: none;
background: #f0f0f0;
cursor: pointer;
margin-right: 2px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
font-size: 0.9em;
transition: background-color 0.2s;
}

.tab-button:hover {
background: #e0e0e0;
}

.tab-button.active {
background: #2d3e50;
color: white;
}

.tab-content {
display: none;
padding: 1em 0;
}

.tab-content.active {
display: block;
}

/* MuJoCo container styles */
#mujoco-container {
background: #000;
border-radius: 8px;
overflow: hidden;
border: 1px solid #ddd;
}

/* Daemon control styles */
#daemon-control {
background: #fff;
padding: 1.5em;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.07);
margin-bottom: 1em;
}

#daemon-control h2 {
margin-top: 0;
color: #2d3e50;
}

#daemon-control button {
padding: 0.5em 1em;
margin-right: 0.5em;
margin-bottom: 0.5em;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}

#start-daemon {
background: #27ae60;
color: white;
}

#stop-daemon {
background: #e74c3c;
color: white;
}

#restart-daemon {
background: #f39c12;
color: white;
}

#daemon-control label {
display: block;
margin: 0.5em 0;
}
79 changes: 79 additions & 0 deletions src/reachy_mini/daemon/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,85 @@ async def list_examples(request: Request):
name="dashboard",
)

# Mount WASM files - always use bin directory
wasm_bin_dir = Path(__file__).parent / "wasm" / "bin"
original_mjcf_dir = Path(__file__).parent.parent.parent / "descriptions" / "reachy_mini" / "mjcf"

if wasm_bin_dir.exists():
# Mount original model files from descriptions directory
if original_mjcf_dir.exists():
app.mount(
"/wasm/dist/examples/scenes/reachy",
StaticFiles(directory=str(original_mjcf_dir)),
name="wasm_original_models",
)

# Mount general path last
app.mount(
"/wasm/dist",
StaticFiles(directory=str(wasm_bin_dir)),
name="wasm",
)

# Add redirects for React app assets when accessed from iframe
from fastapi.responses import RedirectResponse

@app.get("/assets/{file_path}")
async def get_asset_redirect(file_path: str):
# Handle versioned files by mapping to generic names
if file_path.startswith("mujoco_wasm-") and file_path.endswith(".wasm"):
return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.wasm")
elif file_path.startswith("mujoco_wasm-") and file_path.endswith(".js"):
return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.js")
elif file_path.startswith("index-") and file_path.endswith(".js"):
return RedirectResponse(url="/wasm/dist/assets/index.js")
elif file_path.startswith("index-") and file_path.endswith(".css"):
return RedirectResponse(url="/wasm/dist/assets/index.css")
else:
return RedirectResponse(url=f"/wasm/dist/assets/{file_path}")

@app.get("/vite.svg")
async def get_vite_svg():
return RedirectResponse(url="/wasm/dist/vite.svg")

# Also add direct redirects for the full paths
@app.get("/wasm/dist/assets/mujoco_wasm-{hash}.wasm")
async def get_versioned_mujoco_wasm(hash: str):
return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.wasm")

@app.get("/wasm/dist/assets/mujoco_wasm-{hash}.js")
async def get_versioned_mujoco_js(hash: str):
return RedirectResponse(url="/wasm/dist/assets/mujoco_wasm.js")

@app.get("/wasm/dist/assets/index-{hash}.js")
async def get_versioned_index_js(hash: str):
return RedirectResponse(url="/wasm/dist/assets/index.js")

@app.get("/wasm/dist/assets/index-{hash}.css")
async def get_versioned_index_css(hash: str):
return RedirectResponse(url="/wasm/dist/assets/index.css")

# Add proper MIME types and COOP/COEP headers for WASM
@app.middleware("http")
async def add_wasm_headers(request: Request, call_next):
response = await call_next(request)

# Add MIME types
if request.url.path.endswith('.wasm'):
response.headers["content-type"] = "application/wasm"
elif request.url.path.endswith('.js') and '/wasm/' in request.url.path:
response.headers["content-type"] = "text/javascript"

# Add Cross-Origin headers for WASM Web Workers
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"

return response

else:
print(f"Warning: WASM bin directory not found at {wasm_bin_dir}")
print("Run the build script to generate the bin directory with generic asset names")

return app


Expand Down
153 changes: 131 additions & 22 deletions src/reachy_mini/daemon/app/templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,141 @@

<body>
<h1>Dashboard</h1>
<section id="daemon-control">
<h2>Daemon Control</h2>
<div id="daemon-status">Loading daemon status...</div>
<button id="start-daemon">Start Daemon</button>
<button id="stop-daemon">Stop Daemon</button>
<button id="restart-daemon">Restart Daemon</button>
<label><input type="checkbox" id="wake-up-on-start" checked> Wake up on start</label>
<label><input type="checkbox" id="goto-sleep-on-stop" checked> Go to sleep on stop</label>
</section>
<hr>
<h2>Video stream</h2>
<div id="video-stream">
<div class="video">
<video id="video"></video>

<!-- Tab Navigation -->
<div class="tab-container">
<div class="tab-nav">
<button class="tab-button active" data-tab="control">Control</button>
<button class="tab-button" data-tab="renderer">Renderer</button>
<button class="tab-button" data-tab="video">Video</button>
<button class="tab-button" data-tab="examples">Examples</button>
</div>

<!-- Control Tab -->
<div class="tab-content active" id="control-tab">
<section id="daemon-control">
<h2>Daemon Control</h2>
<div id="daemon-status">Loading daemon status...</div>
<button id="start-daemon">Start Daemon</button>
<button id="stop-daemon">Stop Daemon</button>
<button id="restart-daemon">Restart Daemon</button>
<label><input type="checkbox" id="wake-up-on-start" checked> Wake up on start</label>
<label><input type="checkbox" id="goto-sleep-on-stop" checked> Go to sleep on stop</label>
</section>
</div>

<!-- Renderer Tab -->
<div class="tab-content" id="renderer-tab">
<h2>MuJoCo Renderer</h2>
<iframe id="mujoco-iframe" src="" style="width: 100%; height: 80vh; border: none; display: none;"></iframe>
<div id="mujoco-container" style="width: 100%; height: 80vh; position: relative; background: #264059;">
<div id="loading-text" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white;">Loading MuJoCo Renderer...</div>
</div>
</div>

<!-- Video Tab -->
<div class="tab-content" id="video-tab">
<h2>Video Stream</h2>
<div id="video-stream">
<div class="video">
<video id="video"></video>
</div>
</div>
<input type="text" id="video-ip" placeholder="IP address" value="pierre-pi.local" style="margin-right:8px;">
<button id="connect-video">Connect</button>
</div>

<!-- Examples Tab -->
<div class="tab-content" id="examples-tab">
<h2>Examples</h2>
<ul id="examples-list">
{% for fname in files %}
<li><a href="/dashboard/{{ fname }}">{{ fname }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<input type="text" id="video-ip" placeholder="IP address" value="pierre-pi.local" style="margin-right:8px;">
<button id="connect-video">Connect</button>
<h2>Examples</h2>
<ul id="examples-list">
{% for fname in files %}
<li><a href="/dashboard/{{ fname }}">{{ fname }}</a></li>
{% endfor %}
</ul>
<script src="/dashboard/js/3rdparty/gstwebrtc-api-2.0.0.min.js"></script>
<script src="/dashboard/js/dashboard.js"></script>
<script>
// Tab functionality
function initTabs() {
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');

tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.getAttribute('data-tab');

// Remove active class from all buttons and contents
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));

// Add active class to clicked button and corresponding content
button.classList.add('active');
document.getElementById(targetTab + '-tab').classList.add('active');

// Initialize MuJoCo when renderer tab is activated
if (targetTab === 'renderer' && !window.mujocoInitialized) {
initMujocoRenderer();
window.mujocoInitialized = true;
}
});
});
}

// Simple MuJoCo renderer using iframe as fallback
function initMujocoRenderer() {
const iframe = document.getElementById('mujoco-iframe');
const container = document.getElementById('mujoco-container');
const loadingText = document.getElementById('loading-text');

try {
// Load the MuJoCo React app in iframe to avoid Web Worker issues
iframe.src = '/wasm/dist/index.html';
iframe.style.display = 'block';
container.style.display = 'none';

iframe.onload = function() {
console.log('MuJoCo renderer loaded in iframe');
// Try to communicate with the iframe to sync robot state
try {
setInterval(() => {
// Send robot state to iframe (if same-origin)
if (iframe.contentWindow) {
fetch('/api/state/full?with_head_joints=true&with_antenna_positions=true&with_head_pose=true')
.then(response => response.json())
.then(robotState => {
iframe.contentWindow.postMessage({
type: 'robotState',
data: robotState
}, '*');
})
.catch(console.warn);
}
}, 100);
} catch (e) {
console.warn('Cannot communicate with iframe:', e);
}
};

iframe.onerror = function() {
iframe.style.display = 'none';
container.style.display = 'block';
container.innerHTML = '<div style="text-align: center; color: red; padding: 20px;">Failed to load MuJoCo renderer</div>';
};

} catch (error) {
console.error('Error initializing MuJoCo renderer:', error);
iframe.style.display = 'none';
container.style.display = 'block';
container.innerHTML = `<div style="text-align: center; color: red; padding: 20px;">Error: ${error.message}</div>`;
}
}

// Initialize tabs when DOM is loaded
document.addEventListener('DOMContentLoaded', initTabs);
</script>
</body>

</html>
35 changes: 35 additions & 0 deletions src/reachy_mini/daemon/app/wasm/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
*.7z filter=lfs diff=lfs merge=lfs -text
*.arrow filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.ckpt filter=lfs diff=lfs merge=lfs -text
*.ftz filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.h5 filter=lfs diff=lfs merge=lfs -text
*.joblib filter=lfs diff=lfs merge=lfs -text
*.lfs.* filter=lfs diff=lfs merge=lfs -text
*.mlmodel filter=lfs diff=lfs merge=lfs -text
*.model filter=lfs diff=lfs merge=lfs -text
*.msgpack filter=lfs diff=lfs merge=lfs -text
*.npy filter=lfs diff=lfs merge=lfs -text
*.npz filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
*.ot filter=lfs diff=lfs merge=lfs -text
*.parquet filter=lfs diff=lfs merge=lfs -text
*.pb filter=lfs diff=lfs merge=lfs -text
*.pickle filter=lfs diff=lfs merge=lfs -text
*.pkl filter=lfs diff=lfs merge=lfs -text
*.pt filter=lfs diff=lfs merge=lfs -text
*.pth filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.safetensors filter=lfs diff=lfs merge=lfs -text
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.tar.* filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.tflite filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
*.wasm filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
Loading