Georg commited on
Commit ·
eaf03f4
1
Parent(s): 9013e5d
Update Docker configuration and add frontend build process
Browse files- Changed the exposed port for the nova-sim service from 3004 to 5001 in both docker-compose files to avoid conflicts.
- Added a multi-stage build process in the Dockerfile to compile frontend assets using Node.js and Vite, improving deployment efficiency.
- Introduced a Makefile for simplified development commands, including `make dev` for starting backend and frontend servers with hot reload.
- Updated .gitignore to exclude frontend build artifacts and development logs, ensuring a cleaner repository.
- Enhanced README.md to reflect new setup instructions and project structure, emphasizing the use of the Makefile for development.
- .gitignore +10 -0
- Dockerfile +23 -2
- Makefile +143 -0
- README.md +145 -17
- docker-compose.gpu.yml +1 -1
- docker-compose.yml +1 -1
- frontend/package-lock.json +1002 -0
- frontend/package.json +16 -0
- frontend/src/api/client.js +259 -0
- frontend/src/api/client.ts +308 -0
- frontend/src/index.html +317 -0
- frontend/src/main.js +1282 -0
- frontend/src/main.ts +1533 -0
- frontend/src/styles.css +415 -0
- frontend/src/styles.css.tmp +417 -0
- frontend/src/types/protocol.js +13 -0
- frontend/src/types/protocol.ts +416 -0
- frontend/tsconfig.json +21 -0
- frontend/vite.config.ts +23 -0
- server.log +873 -0
- templates/index.html +41 -0
.gitignore
CHANGED
|
@@ -47,3 +47,13 @@ Thumbs.db
|
|
| 47 |
*.mjb
|
| 48 |
.env.local
|
| 49 |
.env.off.local
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
*.mjb
|
| 48 |
.env.local
|
| 49 |
.env.off.local
|
| 50 |
+
|
| 51 |
+
# Frontend
|
| 52 |
+
frontend/node_modules/
|
| 53 |
+
frontend/dist/
|
| 54 |
+
frontend/src/*.js.tmp
|
| 55 |
+
mujoco_server.py.backup
|
| 56 |
+
|
| 57 |
+
# Makefile development artifacts
|
| 58 |
+
.pid-*
|
| 59 |
+
.dev-*.log
|
Dockerfile
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FROM python:3.11-slim-bookworm
|
| 2 |
|
| 3 |
# Install system dependencies for MuJoCo and OpenCV
|
|
@@ -24,9 +42,12 @@ COPY requirements.txt .
|
|
| 24 |
# Install Python dependencies
|
| 25 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 26 |
|
| 27 |
-
# Copy application code
|
| 28 |
COPY . .
|
| 29 |
|
|
|
|
|
|
|
|
|
|
| 30 |
# Environment variables for headless rendering
|
| 31 |
# Options: osmesa (software, slow), egl (GPU if available), glx (X11 GPU)
|
| 32 |
ENV MUJOCO_GL=osmesa
|
|
@@ -37,7 +58,7 @@ ENV OMP_NUM_THREADS=4
|
|
| 37 |
ENV MKL_NUM_THREADS=4
|
| 38 |
|
| 39 |
# Expose port
|
| 40 |
-
EXPOSE
|
| 41 |
|
| 42 |
# Run the server
|
| 43 |
CMD ["python", "mujoco_server.py"]
|
|
|
|
| 1 |
+
# Stage 1: Build frontend assets
|
| 2 |
+
FROM node:20-slim AS frontend-builder
|
| 3 |
+
|
| 4 |
+
WORKDIR /app/frontend
|
| 5 |
+
|
| 6 |
+
# Copy frontend package files
|
| 7 |
+
COPY frontend/package*.json ./
|
| 8 |
+
|
| 9 |
+
# Install dependencies
|
| 10 |
+
RUN npm ci
|
| 11 |
+
|
| 12 |
+
# Copy frontend source
|
| 13 |
+
COPY frontend/ ./
|
| 14 |
+
|
| 15 |
+
# Build frontend
|
| 16 |
+
RUN npm run build
|
| 17 |
+
|
| 18 |
+
# Stage 2: Python runtime
|
| 19 |
FROM python:3.11-slim-bookworm
|
| 20 |
|
| 21 |
# Install system dependencies for MuJoCo and OpenCV
|
|
|
|
| 42 |
# Install Python dependencies
|
| 43 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 44 |
|
| 45 |
+
# Copy application code
|
| 46 |
COPY . .
|
| 47 |
|
| 48 |
+
# Copy built frontend assets from builder stage
|
| 49 |
+
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
| 50 |
+
|
| 51 |
# Environment variables for headless rendering
|
| 52 |
# Options: osmesa (software, slow), egl (GPU if available), glx (X11 GPU)
|
| 53 |
ENV MUJOCO_GL=osmesa
|
|
|
|
| 58 |
ENV MKL_NUM_THREADS=4
|
| 59 |
|
| 60 |
# Expose port
|
| 61 |
+
EXPOSE 5001
|
| 62 |
|
| 63 |
# Run the server
|
| 64 |
CMD ["python", "mujoco_server.py"]
|
Makefile
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.PHONY: dev build clean kill install help test docker docker-gpu docker-down docker-logs
|
| 2 |
+
|
| 3 |
+
# Default target: run in development mode
|
| 4 |
+
dev: kill
|
| 5 |
+
@echo "Starting Nova Sim in development mode..."
|
| 6 |
+
@echo "Backend: http://localhost:5001"
|
| 7 |
+
@echo "Frontend Dev Server: http://localhost:3004"
|
| 8 |
+
@echo ""
|
| 9 |
+
@echo "Access the application at: http://localhost:3004/nova-sim"
|
| 10 |
+
@echo "Press Ctrl+C to stop all servers"
|
| 11 |
+
@echo ""
|
| 12 |
+
@# Start Flask backend
|
| 13 |
+
@.venv/bin/python mujoco_server.py > .dev-backend.log 2>&1 & echo $$! > .pid-backend
|
| 14 |
+
@sleep 2
|
| 15 |
+
@# Start Vite dev server
|
| 16 |
+
@cd frontend && npm run dev > ../.dev-frontend.log 2>&1 & echo $$! > ../.pid-frontend
|
| 17 |
+
@sleep 2
|
| 18 |
+
@# Monitor both log files and kill processes on exit
|
| 19 |
+
@trap 'make kill' INT TERM; \
|
| 20 |
+
tail -f .dev-backend.log -f .dev-frontend.log & \
|
| 21 |
+
TAIL_PID=$$!; \
|
| 22 |
+
wait $$TAIL_PID
|
| 23 |
+
|
| 24 |
+
# Build production assets
|
| 25 |
+
build:
|
| 26 |
+
@echo "Building frontend for production..."
|
| 27 |
+
@cd frontend && npm run build
|
| 28 |
+
@echo ""
|
| 29 |
+
@echo "✓ Production build complete!"
|
| 30 |
+
@echo " Output: frontend/dist/"
|
| 31 |
+
@echo ""
|
| 32 |
+
@echo "Start production server with: make prod"
|
| 33 |
+
|
| 34 |
+
# Run production server
|
| 35 |
+
prod: build
|
| 36 |
+
@echo "Starting Nova Sim production server..."
|
| 37 |
+
@echo "Access at: http://localhost:5001/nova-sim"
|
| 38 |
+
@echo ""
|
| 39 |
+
@.venv/bin/python mujoco_server.py
|
| 40 |
+
|
| 41 |
+
# Install dependencies
|
| 42 |
+
install:
|
| 43 |
+
@echo "Installing Python dependencies..."
|
| 44 |
+
@pip install -r requirements.txt
|
| 45 |
+
@echo ""
|
| 46 |
+
@echo "Installing frontend dependencies..."
|
| 47 |
+
@cd frontend && npm install
|
| 48 |
+
@echo ""
|
| 49 |
+
@echo "✓ All dependencies installed!"
|
| 50 |
+
|
| 51 |
+
# Clean build artifacts and logs
|
| 52 |
+
clean:
|
| 53 |
+
@echo "Cleaning build artifacts..."
|
| 54 |
+
@rm -rf frontend/dist
|
| 55 |
+
@rm -rf frontend/node_modules/.vite
|
| 56 |
+
@rm -f .dev-*.log
|
| 57 |
+
@rm -f .pid-*
|
| 58 |
+
@echo "✓ Clean complete!"
|
| 59 |
+
|
| 60 |
+
# Kill running dev servers
|
| 61 |
+
kill:
|
| 62 |
+
@if [ -f .pid-backend ]; then \
|
| 63 |
+
kill $$(cat .pid-backend) 2>/dev/null || true; \
|
| 64 |
+
rm -f .pid-backend; \
|
| 65 |
+
fi
|
| 66 |
+
@if [ -f .pid-frontend ]; then \
|
| 67 |
+
kill $$(cat .pid-frontend) 2>/dev/null || true; \
|
| 68 |
+
rm -f .pid-frontend; \
|
| 69 |
+
fi
|
| 70 |
+
@# Also kill any orphaned processes
|
| 71 |
+
@pkill -f "mujoco_server.py" 2>/dev/null || true
|
| 72 |
+
@pkill -f "vite" 2>/dev/null || true
|
| 73 |
+
@rm -f .dev-*.log
|
| 74 |
+
@echo "✓ All servers stopped"
|
| 75 |
+
|
| 76 |
+
# Run tests
|
| 77 |
+
test:
|
| 78 |
+
@echo "Running tests with pytest..."
|
| 79 |
+
@pytest -v
|
| 80 |
+
|
| 81 |
+
# Docker commands
|
| 82 |
+
docker:
|
| 83 |
+
@echo "Building and starting Nova Sim in Docker (CPU mode)..."
|
| 84 |
+
@docker-compose up --build -d
|
| 85 |
+
@echo ""
|
| 86 |
+
@echo "✓ Docker container started!"
|
| 87 |
+
@echo " Access at: http://localhost:3004/nova-sim"
|
| 88 |
+
@echo ""
|
| 89 |
+
@echo "View logs: make docker-logs"
|
| 90 |
+
@echo "Stop container: make docker-down"
|
| 91 |
+
|
| 92 |
+
docker-gpu:
|
| 93 |
+
@echo "Building and starting Nova Sim in Docker (GPU mode)..."
|
| 94 |
+
@docker-compose -f docker-compose.gpu.yml up --build -d
|
| 95 |
+
@echo ""
|
| 96 |
+
@echo "✓ Docker container started with GPU acceleration!"
|
| 97 |
+
@echo " Access at: http://localhost:3004/nova-sim"
|
| 98 |
+
@echo ""
|
| 99 |
+
@echo "View logs: make docker-logs"
|
| 100 |
+
@echo "Stop container: make docker-down"
|
| 101 |
+
|
| 102 |
+
docker-down:
|
| 103 |
+
@echo "Stopping Docker containers..."
|
| 104 |
+
@docker-compose down 2>/dev/null || true
|
| 105 |
+
@docker-compose -f docker-compose.gpu.yml down 2>/dev/null || true
|
| 106 |
+
@echo "✓ Docker containers stopped"
|
| 107 |
+
|
| 108 |
+
docker-logs:
|
| 109 |
+
@docker-compose logs -f
|
| 110 |
+
|
| 111 |
+
# Show help
|
| 112 |
+
help:
|
| 113 |
+
@echo "Nova Sim Makefile Commands:"
|
| 114 |
+
@echo ""
|
| 115 |
+
@echo "Development:"
|
| 116 |
+
@echo " make dev - Start development servers (backend + frontend with hot reload)"
|
| 117 |
+
@echo " make build - Build frontend for production"
|
| 118 |
+
@echo " make prod - Build and run production server"
|
| 119 |
+
@echo " make install - Install all dependencies (Python + Node.js)"
|
| 120 |
+
@echo " make clean - Clean build artifacts and logs"
|
| 121 |
+
@echo " make kill - Stop all running development servers"
|
| 122 |
+
@echo " make test - Run tests with pytest"
|
| 123 |
+
@echo ""
|
| 124 |
+
@echo "Docker:"
|
| 125 |
+
@echo " make docker - Build and run in Docker (CPU/OSMesa rendering)"
|
| 126 |
+
@echo " make docker-gpu - Build and run in Docker (GPU/EGL rendering)"
|
| 127 |
+
@echo " make docker-down - Stop and remove Docker containers"
|
| 128 |
+
@echo " make docker-logs - View Docker container logs"
|
| 129 |
+
@echo ""
|
| 130 |
+
@echo "Other:"
|
| 131 |
+
@echo " make help - Show this help message"
|
| 132 |
+
@echo ""
|
| 133 |
+
@echo "Development workflow:"
|
| 134 |
+
@echo " 1. make install (first time only)"
|
| 135 |
+
@echo " 2. make dev (daily development)"
|
| 136 |
+
@echo ""
|
| 137 |
+
@echo "Production workflow:"
|
| 138 |
+
@echo " 1. make build (build assets)"
|
| 139 |
+
@echo " 2. make prod (run server)"
|
| 140 |
+
@echo ""
|
| 141 |
+
@echo "Docker workflow:"
|
| 142 |
+
@echo " 1. make docker (CPU mode, works everywhere)"
|
| 143 |
+
@echo " OR make docker-gpu (GPU mode, requires NVIDIA GPU)"
|
README.md
CHANGED
|
@@ -26,8 +26,9 @@ Nova Sim combines MuJoCo physics, a Flask/WebSocket server, and a browser UI so
|
|
| 26 |
## Highlights
|
| 27 |
|
| 28 |
- Real-time MuJoCo physics simulation
|
| 29 |
-
- Web-based video streaming interface
|
| 30 |
- WebSocket-based state/command communication
|
|
|
|
| 31 |
- Gym-style WebSocket API for RL/IL clients
|
| 32 |
- Interactive camera controls (rotate, zoom, pan)
|
| 33 |
- Robot switching without restart
|
|
@@ -67,6 +68,20 @@ nova_sim/
|
|
| 67 |
├── docker-compose.yml # CPU/OSMesa configuration
|
| 68 |
├── docker-compose.gpu.yml # GPU/EGL configuration
|
| 69 |
├── requirements.txt # Python dependencies
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
├── robots/
|
| 71 |
│ ├── g1/ # Unitree G1 humanoid
|
| 72 |
│ │ ├── g1_env.py # Gymnasium environment
|
|
@@ -98,40 +113,90 @@ nova_sim/
|
|
| 98 |
|
| 99 |
## Quick Start
|
| 100 |
|
| 101 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
```bash
|
| 104 |
# Create and activate a virtualenv
|
| 105 |
python3 -m venv .venv
|
| 106 |
source .venv/bin/activate
|
| 107 |
|
| 108 |
-
# Install dependencies
|
| 109 |
pip install mujoco gymnasium flask flask-sock opencv-python torch numpy
|
| 110 |
|
| 111 |
-
#
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
#
|
| 115 |
-
python mujoco_server.py
|
| 116 |
-
python mujoco_server.py --reward-threshold -0.2 # Lenient (20cm from target)
|
| 117 |
|
| 118 |
-
# Open browser at http://localhost:
|
| 119 |
```
|
| 120 |
|
| 121 |
**Reward Threshold**: Episodes automatically terminate when the robot reaches within the specified distance of the target. See [REWARD_THRESHOLD.md](REWARD_THRESHOLD.md) for details.
|
| 122 |
|
| 123 |
-
```
|
| 124 |
-
|
| 125 |
## Docker Deployment
|
| 126 |
|
| 127 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
```bash
|
| 130 |
# Build and run (CPU/software rendering)
|
| 131 |
docker-compose up --build
|
| 132 |
|
| 133 |
# Or with docker run
|
| 134 |
-
docker run -p 3004:
|
|
|
|
|
|
|
| 135 |
```
|
| 136 |
|
| 137 |
### Configuration & Tuning
|
|
@@ -156,7 +221,7 @@ services:
|
|
| 156 |
nova-sim:
|
| 157 |
build: .
|
| 158 |
ports:
|
| 159 |
-
- "3004:
|
| 160 |
environment:
|
| 161 |
- MUJOCO_GL=osmesa
|
| 162 |
- PYOPENGL_PLATFORM=osmesa
|
|
@@ -172,6 +237,7 @@ For significantly better performance, use GPU acceleration:
|
|
| 172 |
```bash
|
| 173 |
# Requires: NVIDIA GPU + nvidia-container-toolkit
|
| 174 |
docker-compose -f docker-compose.gpu.yml up
|
|
|
|
| 175 |
```
|
| 176 |
|
| 177 |
```yaml
|
|
@@ -179,7 +245,7 @@ services:
|
|
| 179 |
nova-sim:
|
| 180 |
build: .
|
| 181 |
ports:
|
| 182 |
-
- "3004:
|
| 183 |
environment:
|
| 184 |
- MUJOCO_GL=egl # GPU rendering
|
| 185 |
- PYOPENGL_PLATFORM=egl
|
|
@@ -208,14 +274,14 @@ services:
|
|
| 208 |
|
| 209 |
```bash
|
| 210 |
# Ultra-low resolution for maximum speed
|
| 211 |
-
docker run -p 3004:
|
| 212 |
-e RENDER_WIDTH=320 \
|
| 213 |
-e RENDER_HEIGHT=180 \
|
| 214 |
-e MUJOCO_GL=osmesa \
|
| 215 |
nova-sim
|
| 216 |
|
| 217 |
# High quality with GPU
|
| 218 |
-
docker run --gpus all -p 3004:
|
| 219 |
-e RENDER_WIDTH=1920 \
|
| 220 |
-e RENDER_HEIGHT=1080 \
|
| 221 |
-e MUJOCO_GL=egl \
|
|
@@ -880,6 +946,68 @@ The Nova API integration is implemented in:
|
|
| 880 |
|
| 881 |
The tests assume the server is accessible via `http://localhost:3004/nova-sim/api/v1` and will skip automatically if the API is unreachable.
|
| 882 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
## License
|
| 884 |
|
| 885 |
This project uses models from:
|
|
|
|
| 26 |
## Highlights
|
| 27 |
|
| 28 |
- Real-time MuJoCo physics simulation
|
| 29 |
+
- Web-based video streaming interface with TypeScript frontend
|
| 30 |
- WebSocket-based state/command communication
|
| 31 |
+
- **One-command setup**: `make dev` starts both backend and frontend with hot reload
|
| 32 |
- Gym-style WebSocket API for RL/IL clients
|
| 33 |
- Interactive camera controls (rotate, zoom, pan)
|
| 34 |
- Robot switching without restart
|
|
|
|
| 68 |
├── docker-compose.yml # CPU/OSMesa configuration
|
| 69 |
├── docker-compose.gpu.yml # GPU/EGL configuration
|
| 70 |
├── requirements.txt # Python dependencies
|
| 71 |
+
├── frontend/ # Web UI (TypeScript + Vite)
|
| 72 |
+
│ ├── src/
|
| 73 |
+
│ │ ├── index.html # HTML template
|
| 74 |
+
│ │ ├── main.ts # Main TypeScript entry point
|
| 75 |
+
│ │ ├── styles.css # CSS styles
|
| 76 |
+
│ │ ├── api/
|
| 77 |
+
│ │ │ └── client.ts # WebSocket client
|
| 78 |
+
│ │ └── types/
|
| 79 |
+
│ │ └── protocol.ts # TypeScript protocol types
|
| 80 |
+
│ ├── package.json # Node.js dependencies
|
| 81 |
+
│ ├── tsconfig.json # TypeScript configuration
|
| 82 |
+
│ └── vite.config.ts # Vite bundler configuration
|
| 83 |
+
├── templates/ # Flask templates
|
| 84 |
+
│ └── index.html # Main HTML template
|
| 85 |
├── robots/
|
| 86 |
│ ├── g1/ # Unitree G1 humanoid
|
| 87 |
│ │ ├── g1_env.py # Gymnasium environment
|
|
|
|
| 113 |
|
| 114 |
## Quick Start
|
| 115 |
|
| 116 |
+
### Using Makefile (Recommended)
|
| 117 |
+
|
| 118 |
+
The easiest way to get started is using the included Makefile:
|
| 119 |
+
|
| 120 |
+
```bash
|
| 121 |
+
# First time setup: install all dependencies
|
| 122 |
+
make install
|
| 123 |
+
|
| 124 |
+
# Start development servers (backend + frontend with hot reload)
|
| 125 |
+
make dev
|
| 126 |
+
# Backend: http://localhost:5000
|
| 127 |
+
# Frontend dev server: http://localhost:3004
|
| 128 |
+
# Access the application at: http://localhost:3004/nova-sim
|
| 129 |
+
|
| 130 |
+
# Press Ctrl+C to stop all servers
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
**Production build:**
|
| 134 |
+
```bash
|
| 135 |
+
make build # Build frontend assets
|
| 136 |
+
make prod # Run production server
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
**Other commands:**
|
| 140 |
+
```bash
|
| 141 |
+
make help # Show all available commands
|
| 142 |
+
make clean # Clean build artifacts
|
| 143 |
+
make kill # Stop all running servers
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### Manual Setup (Alternative)
|
| 147 |
+
|
| 148 |
+
If you prefer not to use the Makefile:
|
| 149 |
|
| 150 |
```bash
|
| 151 |
# Create and activate a virtualenv
|
| 152 |
python3 -m venv .venv
|
| 153 |
source .venv/bin/activate
|
| 154 |
|
| 155 |
+
# Install Python dependencies
|
| 156 |
pip install mujoco gymnasium flask flask-sock opencv-python torch numpy
|
| 157 |
|
| 158 |
+
# Install frontend dependencies
|
| 159 |
+
cd frontend && npm install && cd ..
|
| 160 |
+
|
| 161 |
+
# Build frontend
|
| 162 |
+
cd frontend && npm run build && cd ..
|
| 163 |
|
| 164 |
+
# Start the server
|
| 165 |
+
python mujoco_server.py
|
|
|
|
| 166 |
|
| 167 |
+
# Open browser at http://localhost:5000/nova-sim
|
| 168 |
```
|
| 169 |
|
| 170 |
**Reward Threshold**: Episodes automatically terminate when the robot reaches within the specified distance of the target. See [REWARD_THRESHOLD.md](REWARD_THRESHOLD.md) for details.
|
| 171 |
|
|
|
|
|
|
|
| 172 |
## Docker Deployment
|
| 173 |
|
| 174 |
+
### Quick Start with Makefile
|
| 175 |
+
|
| 176 |
+
```bash
|
| 177 |
+
# Build and run in Docker (CPU mode)
|
| 178 |
+
make docker
|
| 179 |
+
|
| 180 |
+
# Or with GPU acceleration (requires NVIDIA GPU)
|
| 181 |
+
make docker-gpu
|
| 182 |
+
|
| 183 |
+
# View logs
|
| 184 |
+
make docker-logs
|
| 185 |
+
|
| 186 |
+
# Stop containers
|
| 187 |
+
make docker-down
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
### Manual Docker Commands
|
| 191 |
|
| 192 |
```bash
|
| 193 |
# Build and run (CPU/software rendering)
|
| 194 |
docker-compose up --build
|
| 195 |
|
| 196 |
# Or with docker run
|
| 197 |
+
docker run -p 3004:5000 nova-sim
|
| 198 |
+
|
| 199 |
+
# Access at: http://localhost:3004/nova-sim
|
| 200 |
```
|
| 201 |
|
| 202 |
### Configuration & Tuning
|
|
|
|
| 221 |
nova-sim:
|
| 222 |
build: .
|
| 223 |
ports:
|
| 224 |
+
- "3004:5000" # Host:Container
|
| 225 |
environment:
|
| 226 |
- MUJOCO_GL=osmesa
|
| 227 |
- PYOPENGL_PLATFORM=osmesa
|
|
|
|
| 237 |
```bash
|
| 238 |
# Requires: NVIDIA GPU + nvidia-container-toolkit
|
| 239 |
docker-compose -f docker-compose.gpu.yml up
|
| 240 |
+
# Or: make docker-gpu
|
| 241 |
```
|
| 242 |
|
| 243 |
```yaml
|
|
|
|
| 245 |
nova-sim:
|
| 246 |
build: .
|
| 247 |
ports:
|
| 248 |
+
- "3004:5000" # Host:Container
|
| 249 |
environment:
|
| 250 |
- MUJOCO_GL=egl # GPU rendering
|
| 251 |
- PYOPENGL_PLATFORM=egl
|
|
|
|
| 274 |
|
| 275 |
```bash
|
| 276 |
# Ultra-low resolution for maximum speed
|
| 277 |
+
docker run -p 3004:5000 \
|
| 278 |
-e RENDER_WIDTH=320 \
|
| 279 |
-e RENDER_HEIGHT=180 \
|
| 280 |
-e MUJOCO_GL=osmesa \
|
| 281 |
nova-sim
|
| 282 |
|
| 283 |
# High quality with GPU
|
| 284 |
+
docker run --gpus all -p 3004:5000 \
|
| 285 |
-e RENDER_WIDTH=1920 \
|
| 286 |
-e RENDER_HEIGHT=1080 \
|
| 287 |
-e MUJOCO_GL=egl \
|
|
|
|
| 946 |
|
| 947 |
The tests assume the server is accessible via `http://localhost:3004/nova-sim/api/v1` and will skip automatically if the API is unreachable.
|
| 948 |
|
| 949 |
+
## Frontend Development
|
| 950 |
+
|
| 951 |
+
The Nova Sim web UI is built with TypeScript, Vite, and vanilla JavaScript (no framework dependencies).
|
| 952 |
+
|
| 953 |
+
### Development Setup
|
| 954 |
+
|
| 955 |
+
1. **Install Node.js dependencies:**
|
| 956 |
+
```bash
|
| 957 |
+
cd frontend
|
| 958 |
+
npm install
|
| 959 |
+
```
|
| 960 |
+
|
| 961 |
+
2. **Start development server with hot reload:**
|
| 962 |
+
```bash
|
| 963 |
+
# Terminal 1: Start Flask backend
|
| 964 |
+
python mujoco_server.py
|
| 965 |
+
|
| 966 |
+
# Terminal 2: Start Vite dev server
|
| 967 |
+
cd frontend
|
| 968 |
+
npm run dev
|
| 969 |
+
```
|
| 970 |
+
|
| 971 |
+
3. **Access the development UI:**
|
| 972 |
+
- Open [http://localhost:3004/nova-sim](http://localhost:3004/nova-sim)
|
| 973 |
+
- All API/WebSocket calls are automatically proxied to Flask at `:5000`
|
| 974 |
+
- Changes to TypeScript/CSS auto-reload in the browser
|
| 975 |
+
|
| 976 |
+
### Production Build
|
| 977 |
+
|
| 978 |
+
```bash
|
| 979 |
+
cd frontend
|
| 980 |
+
npm run build
|
| 981 |
+
```
|
| 982 |
+
|
| 983 |
+
This compiles TypeScript and bundles all assets to `frontend/dist/`. The Flask server automatically serves these built files from the `/static` route.
|
| 984 |
+
|
| 985 |
+
### Frontend Structure
|
| 986 |
+
|
| 987 |
+
- **`src/main.ts`**: Main application entry point with WebSocket handling and UI logic
|
| 988 |
+
- **`src/api/client.ts`**: Type-safe WebSocket client for server communication
|
| 989 |
+
- **`src/types/protocol.ts`**: TypeScript definitions mirroring Python protocol types
|
| 990 |
+
- **`src/styles.css`**: All CSS styles (Wandelbots design system)
|
| 991 |
+
- **`src/index.html`**: HTML template with viewport structure
|
| 992 |
+
|
| 993 |
+
### Type Safety
|
| 994 |
+
|
| 995 |
+
The frontend uses TypeScript for compile-time type checking:
|
| 996 |
+
|
| 997 |
+
```bash
|
| 998 |
+
# Run type checker
|
| 999 |
+
cd frontend
|
| 1000 |
+
npm run type-check
|
| 1001 |
+
```
|
| 1002 |
+
|
| 1003 |
+
All WebSocket messages are strongly typed to match the Python protocol definitions in `protocol_types.py`.
|
| 1004 |
+
|
| 1005 |
+
### Modifying the UI
|
| 1006 |
+
|
| 1007 |
+
- **Styles**: Edit `frontend/src/styles.css`
|
| 1008 |
+
- **Behavior**: Edit `frontend/src/main.ts`
|
| 1009 |
+
- **Protocol**: Update both `protocol_types.py` (Python) and `frontend/src/types/protocol.ts` (TypeScript)
|
| 1010 |
+
|
| 1011 |
## License
|
| 1012 |
|
| 1013 |
This project uses models from:
|
docker-compose.gpu.yml
CHANGED
|
@@ -7,7 +7,7 @@ services:
|
|
| 7 |
nova-sim:
|
| 8 |
build: .
|
| 9 |
ports:
|
| 10 |
-
- "3004:
|
| 11 |
environment:
|
| 12 |
# Use EGL for GPU rendering (much faster than OSMesa)
|
| 13 |
- MUJOCO_GL=egl
|
|
|
|
| 7 |
nova-sim:
|
| 8 |
build: .
|
| 9 |
ports:
|
| 10 |
+
- "3004:5001"
|
| 11 |
environment:
|
| 12 |
# Use EGL for GPU rendering (much faster than OSMesa)
|
| 13 |
- MUJOCO_GL=egl
|
docker-compose.yml
CHANGED
|
@@ -4,7 +4,7 @@ services:
|
|
| 4 |
nova-sim:
|
| 5 |
build: .
|
| 6 |
ports:
|
| 7 |
-
- "3004:
|
| 8 |
environment:
|
| 9 |
# Rendering backend (osmesa = software, slow but compatible)
|
| 10 |
- MUJOCO_GL=osmesa
|
|
|
|
| 4 |
nova-sim:
|
| 5 |
build: .
|
| 6 |
ports:
|
| 7 |
+
- "3004:5001"
|
| 8 |
environment:
|
| 9 |
# Rendering backend (osmesa = software, slow but compatible)
|
| 10 |
- MUJOCO_GL=osmesa
|
frontend/package-lock.json
ADDED
|
@@ -0,0 +1,1002 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "nova-sim-frontend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "nova-sim-frontend",
|
| 9 |
+
"version": "1.0.0",
|
| 10 |
+
"devDependencies": {
|
| 11 |
+
"typescript": "^5.3.3",
|
| 12 |
+
"vite": "^5.0.11"
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 16 |
+
"version": "0.21.5",
|
| 17 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 18 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 19 |
+
"cpu": [
|
| 20 |
+
"ppc64"
|
| 21 |
+
],
|
| 22 |
+
"dev": true,
|
| 23 |
+
"license": "MIT",
|
| 24 |
+
"optional": true,
|
| 25 |
+
"os": [
|
| 26 |
+
"aix"
|
| 27 |
+
],
|
| 28 |
+
"engines": {
|
| 29 |
+
"node": ">=12"
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"node_modules/@esbuild/android-arm": {
|
| 33 |
+
"version": "0.21.5",
|
| 34 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 35 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 36 |
+
"cpu": [
|
| 37 |
+
"arm"
|
| 38 |
+
],
|
| 39 |
+
"dev": true,
|
| 40 |
+
"license": "MIT",
|
| 41 |
+
"optional": true,
|
| 42 |
+
"os": [
|
| 43 |
+
"android"
|
| 44 |
+
],
|
| 45 |
+
"engines": {
|
| 46 |
+
"node": ">=12"
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
"node_modules/@esbuild/android-arm64": {
|
| 50 |
+
"version": "0.21.5",
|
| 51 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 52 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 53 |
+
"cpu": [
|
| 54 |
+
"arm64"
|
| 55 |
+
],
|
| 56 |
+
"dev": true,
|
| 57 |
+
"license": "MIT",
|
| 58 |
+
"optional": true,
|
| 59 |
+
"os": [
|
| 60 |
+
"android"
|
| 61 |
+
],
|
| 62 |
+
"engines": {
|
| 63 |
+
"node": ">=12"
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"node_modules/@esbuild/android-x64": {
|
| 67 |
+
"version": "0.21.5",
|
| 68 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 69 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 70 |
+
"cpu": [
|
| 71 |
+
"x64"
|
| 72 |
+
],
|
| 73 |
+
"dev": true,
|
| 74 |
+
"license": "MIT",
|
| 75 |
+
"optional": true,
|
| 76 |
+
"os": [
|
| 77 |
+
"android"
|
| 78 |
+
],
|
| 79 |
+
"engines": {
|
| 80 |
+
"node": ">=12"
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 84 |
+
"version": "0.21.5",
|
| 85 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 86 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 87 |
+
"cpu": [
|
| 88 |
+
"arm64"
|
| 89 |
+
],
|
| 90 |
+
"dev": true,
|
| 91 |
+
"license": "MIT",
|
| 92 |
+
"optional": true,
|
| 93 |
+
"os": [
|
| 94 |
+
"darwin"
|
| 95 |
+
],
|
| 96 |
+
"engines": {
|
| 97 |
+
"node": ">=12"
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 101 |
+
"version": "0.21.5",
|
| 102 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 103 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 104 |
+
"cpu": [
|
| 105 |
+
"x64"
|
| 106 |
+
],
|
| 107 |
+
"dev": true,
|
| 108 |
+
"license": "MIT",
|
| 109 |
+
"optional": true,
|
| 110 |
+
"os": [
|
| 111 |
+
"darwin"
|
| 112 |
+
],
|
| 113 |
+
"engines": {
|
| 114 |
+
"node": ">=12"
|
| 115 |
+
}
|
| 116 |
+
},
|
| 117 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 118 |
+
"version": "0.21.5",
|
| 119 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 120 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 121 |
+
"cpu": [
|
| 122 |
+
"arm64"
|
| 123 |
+
],
|
| 124 |
+
"dev": true,
|
| 125 |
+
"license": "MIT",
|
| 126 |
+
"optional": true,
|
| 127 |
+
"os": [
|
| 128 |
+
"freebsd"
|
| 129 |
+
],
|
| 130 |
+
"engines": {
|
| 131 |
+
"node": ">=12"
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 135 |
+
"version": "0.21.5",
|
| 136 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 137 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 138 |
+
"cpu": [
|
| 139 |
+
"x64"
|
| 140 |
+
],
|
| 141 |
+
"dev": true,
|
| 142 |
+
"license": "MIT",
|
| 143 |
+
"optional": true,
|
| 144 |
+
"os": [
|
| 145 |
+
"freebsd"
|
| 146 |
+
],
|
| 147 |
+
"engines": {
|
| 148 |
+
"node": ">=12"
|
| 149 |
+
}
|
| 150 |
+
},
|
| 151 |
+
"node_modules/@esbuild/linux-arm": {
|
| 152 |
+
"version": "0.21.5",
|
| 153 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 154 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 155 |
+
"cpu": [
|
| 156 |
+
"arm"
|
| 157 |
+
],
|
| 158 |
+
"dev": true,
|
| 159 |
+
"license": "MIT",
|
| 160 |
+
"optional": true,
|
| 161 |
+
"os": [
|
| 162 |
+
"linux"
|
| 163 |
+
],
|
| 164 |
+
"engines": {
|
| 165 |
+
"node": ">=12"
|
| 166 |
+
}
|
| 167 |
+
},
|
| 168 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 169 |
+
"version": "0.21.5",
|
| 170 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 171 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 172 |
+
"cpu": [
|
| 173 |
+
"arm64"
|
| 174 |
+
],
|
| 175 |
+
"dev": true,
|
| 176 |
+
"license": "MIT",
|
| 177 |
+
"optional": true,
|
| 178 |
+
"os": [
|
| 179 |
+
"linux"
|
| 180 |
+
],
|
| 181 |
+
"engines": {
|
| 182 |
+
"node": ">=12"
|
| 183 |
+
}
|
| 184 |
+
},
|
| 185 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 186 |
+
"version": "0.21.5",
|
| 187 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 188 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 189 |
+
"cpu": [
|
| 190 |
+
"ia32"
|
| 191 |
+
],
|
| 192 |
+
"dev": true,
|
| 193 |
+
"license": "MIT",
|
| 194 |
+
"optional": true,
|
| 195 |
+
"os": [
|
| 196 |
+
"linux"
|
| 197 |
+
],
|
| 198 |
+
"engines": {
|
| 199 |
+
"node": ">=12"
|
| 200 |
+
}
|
| 201 |
+
},
|
| 202 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 203 |
+
"version": "0.21.5",
|
| 204 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 205 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 206 |
+
"cpu": [
|
| 207 |
+
"loong64"
|
| 208 |
+
],
|
| 209 |
+
"dev": true,
|
| 210 |
+
"license": "MIT",
|
| 211 |
+
"optional": true,
|
| 212 |
+
"os": [
|
| 213 |
+
"linux"
|
| 214 |
+
],
|
| 215 |
+
"engines": {
|
| 216 |
+
"node": ">=12"
|
| 217 |
+
}
|
| 218 |
+
},
|
| 219 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 220 |
+
"version": "0.21.5",
|
| 221 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 222 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 223 |
+
"cpu": [
|
| 224 |
+
"mips64el"
|
| 225 |
+
],
|
| 226 |
+
"dev": true,
|
| 227 |
+
"license": "MIT",
|
| 228 |
+
"optional": true,
|
| 229 |
+
"os": [
|
| 230 |
+
"linux"
|
| 231 |
+
],
|
| 232 |
+
"engines": {
|
| 233 |
+
"node": ">=12"
|
| 234 |
+
}
|
| 235 |
+
},
|
| 236 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 237 |
+
"version": "0.21.5",
|
| 238 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 239 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 240 |
+
"cpu": [
|
| 241 |
+
"ppc64"
|
| 242 |
+
],
|
| 243 |
+
"dev": true,
|
| 244 |
+
"license": "MIT",
|
| 245 |
+
"optional": true,
|
| 246 |
+
"os": [
|
| 247 |
+
"linux"
|
| 248 |
+
],
|
| 249 |
+
"engines": {
|
| 250 |
+
"node": ">=12"
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 254 |
+
"version": "0.21.5",
|
| 255 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 256 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 257 |
+
"cpu": [
|
| 258 |
+
"riscv64"
|
| 259 |
+
],
|
| 260 |
+
"dev": true,
|
| 261 |
+
"license": "MIT",
|
| 262 |
+
"optional": true,
|
| 263 |
+
"os": [
|
| 264 |
+
"linux"
|
| 265 |
+
],
|
| 266 |
+
"engines": {
|
| 267 |
+
"node": ">=12"
|
| 268 |
+
}
|
| 269 |
+
},
|
| 270 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 271 |
+
"version": "0.21.5",
|
| 272 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 273 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 274 |
+
"cpu": [
|
| 275 |
+
"s390x"
|
| 276 |
+
],
|
| 277 |
+
"dev": true,
|
| 278 |
+
"license": "MIT",
|
| 279 |
+
"optional": true,
|
| 280 |
+
"os": [
|
| 281 |
+
"linux"
|
| 282 |
+
],
|
| 283 |
+
"engines": {
|
| 284 |
+
"node": ">=12"
|
| 285 |
+
}
|
| 286 |
+
},
|
| 287 |
+
"node_modules/@esbuild/linux-x64": {
|
| 288 |
+
"version": "0.21.5",
|
| 289 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 290 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 291 |
+
"cpu": [
|
| 292 |
+
"x64"
|
| 293 |
+
],
|
| 294 |
+
"dev": true,
|
| 295 |
+
"license": "MIT",
|
| 296 |
+
"optional": true,
|
| 297 |
+
"os": [
|
| 298 |
+
"linux"
|
| 299 |
+
],
|
| 300 |
+
"engines": {
|
| 301 |
+
"node": ">=12"
|
| 302 |
+
}
|
| 303 |
+
},
|
| 304 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 305 |
+
"version": "0.21.5",
|
| 306 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 307 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 308 |
+
"cpu": [
|
| 309 |
+
"x64"
|
| 310 |
+
],
|
| 311 |
+
"dev": true,
|
| 312 |
+
"license": "MIT",
|
| 313 |
+
"optional": true,
|
| 314 |
+
"os": [
|
| 315 |
+
"netbsd"
|
| 316 |
+
],
|
| 317 |
+
"engines": {
|
| 318 |
+
"node": ">=12"
|
| 319 |
+
}
|
| 320 |
+
},
|
| 321 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 322 |
+
"version": "0.21.5",
|
| 323 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 324 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 325 |
+
"cpu": [
|
| 326 |
+
"x64"
|
| 327 |
+
],
|
| 328 |
+
"dev": true,
|
| 329 |
+
"license": "MIT",
|
| 330 |
+
"optional": true,
|
| 331 |
+
"os": [
|
| 332 |
+
"openbsd"
|
| 333 |
+
],
|
| 334 |
+
"engines": {
|
| 335 |
+
"node": ">=12"
|
| 336 |
+
}
|
| 337 |
+
},
|
| 338 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 339 |
+
"version": "0.21.5",
|
| 340 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 341 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 342 |
+
"cpu": [
|
| 343 |
+
"x64"
|
| 344 |
+
],
|
| 345 |
+
"dev": true,
|
| 346 |
+
"license": "MIT",
|
| 347 |
+
"optional": true,
|
| 348 |
+
"os": [
|
| 349 |
+
"sunos"
|
| 350 |
+
],
|
| 351 |
+
"engines": {
|
| 352 |
+
"node": ">=12"
|
| 353 |
+
}
|
| 354 |
+
},
|
| 355 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 356 |
+
"version": "0.21.5",
|
| 357 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 358 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 359 |
+
"cpu": [
|
| 360 |
+
"arm64"
|
| 361 |
+
],
|
| 362 |
+
"dev": true,
|
| 363 |
+
"license": "MIT",
|
| 364 |
+
"optional": true,
|
| 365 |
+
"os": [
|
| 366 |
+
"win32"
|
| 367 |
+
],
|
| 368 |
+
"engines": {
|
| 369 |
+
"node": ">=12"
|
| 370 |
+
}
|
| 371 |
+
},
|
| 372 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 373 |
+
"version": "0.21.5",
|
| 374 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 375 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 376 |
+
"cpu": [
|
| 377 |
+
"ia32"
|
| 378 |
+
],
|
| 379 |
+
"dev": true,
|
| 380 |
+
"license": "MIT",
|
| 381 |
+
"optional": true,
|
| 382 |
+
"os": [
|
| 383 |
+
"win32"
|
| 384 |
+
],
|
| 385 |
+
"engines": {
|
| 386 |
+
"node": ">=12"
|
| 387 |
+
}
|
| 388 |
+
},
|
| 389 |
+
"node_modules/@esbuild/win32-x64": {
|
| 390 |
+
"version": "0.21.5",
|
| 391 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 392 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 393 |
+
"cpu": [
|
| 394 |
+
"x64"
|
| 395 |
+
],
|
| 396 |
+
"dev": true,
|
| 397 |
+
"license": "MIT",
|
| 398 |
+
"optional": true,
|
| 399 |
+
"os": [
|
| 400 |
+
"win32"
|
| 401 |
+
],
|
| 402 |
+
"engines": {
|
| 403 |
+
"node": ">=12"
|
| 404 |
+
}
|
| 405 |
+
},
|
| 406 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 407 |
+
"version": "4.55.3",
|
| 408 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.3.tgz",
|
| 409 |
+
"integrity": "sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg==",
|
| 410 |
+
"cpu": [
|
| 411 |
+
"arm"
|
| 412 |
+
],
|
| 413 |
+
"dev": true,
|
| 414 |
+
"license": "MIT",
|
| 415 |
+
"optional": true,
|
| 416 |
+
"os": [
|
| 417 |
+
"android"
|
| 418 |
+
]
|
| 419 |
+
},
|
| 420 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 421 |
+
"version": "4.55.3",
|
| 422 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.3.tgz",
|
| 423 |
+
"integrity": "sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g==",
|
| 424 |
+
"cpu": [
|
| 425 |
+
"arm64"
|
| 426 |
+
],
|
| 427 |
+
"dev": true,
|
| 428 |
+
"license": "MIT",
|
| 429 |
+
"optional": true,
|
| 430 |
+
"os": [
|
| 431 |
+
"android"
|
| 432 |
+
]
|
| 433 |
+
},
|
| 434 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 435 |
+
"version": "4.55.3",
|
| 436 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.3.tgz",
|
| 437 |
+
"integrity": "sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw==",
|
| 438 |
+
"cpu": [
|
| 439 |
+
"arm64"
|
| 440 |
+
],
|
| 441 |
+
"dev": true,
|
| 442 |
+
"license": "MIT",
|
| 443 |
+
"optional": true,
|
| 444 |
+
"os": [
|
| 445 |
+
"darwin"
|
| 446 |
+
]
|
| 447 |
+
},
|
| 448 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 449 |
+
"version": "4.55.3",
|
| 450 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.3.tgz",
|
| 451 |
+
"integrity": "sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA==",
|
| 452 |
+
"cpu": [
|
| 453 |
+
"x64"
|
| 454 |
+
],
|
| 455 |
+
"dev": true,
|
| 456 |
+
"license": "MIT",
|
| 457 |
+
"optional": true,
|
| 458 |
+
"os": [
|
| 459 |
+
"darwin"
|
| 460 |
+
]
|
| 461 |
+
},
|
| 462 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 463 |
+
"version": "4.55.3",
|
| 464 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.3.tgz",
|
| 465 |
+
"integrity": "sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q==",
|
| 466 |
+
"cpu": [
|
| 467 |
+
"arm64"
|
| 468 |
+
],
|
| 469 |
+
"dev": true,
|
| 470 |
+
"license": "MIT",
|
| 471 |
+
"optional": true,
|
| 472 |
+
"os": [
|
| 473 |
+
"freebsd"
|
| 474 |
+
]
|
| 475 |
+
},
|
| 476 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 477 |
+
"version": "4.55.3",
|
| 478 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.3.tgz",
|
| 479 |
+
"integrity": "sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA==",
|
| 480 |
+
"cpu": [
|
| 481 |
+
"x64"
|
| 482 |
+
],
|
| 483 |
+
"dev": true,
|
| 484 |
+
"license": "MIT",
|
| 485 |
+
"optional": true,
|
| 486 |
+
"os": [
|
| 487 |
+
"freebsd"
|
| 488 |
+
]
|
| 489 |
+
},
|
| 490 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 491 |
+
"version": "4.55.3",
|
| 492 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.3.tgz",
|
| 493 |
+
"integrity": "sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==",
|
| 494 |
+
"cpu": [
|
| 495 |
+
"arm"
|
| 496 |
+
],
|
| 497 |
+
"dev": true,
|
| 498 |
+
"license": "MIT",
|
| 499 |
+
"optional": true,
|
| 500 |
+
"os": [
|
| 501 |
+
"linux"
|
| 502 |
+
]
|
| 503 |
+
},
|
| 504 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 505 |
+
"version": "4.55.3",
|
| 506 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.3.tgz",
|
| 507 |
+
"integrity": "sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==",
|
| 508 |
+
"cpu": [
|
| 509 |
+
"arm"
|
| 510 |
+
],
|
| 511 |
+
"dev": true,
|
| 512 |
+
"license": "MIT",
|
| 513 |
+
"optional": true,
|
| 514 |
+
"os": [
|
| 515 |
+
"linux"
|
| 516 |
+
]
|
| 517 |
+
},
|
| 518 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 519 |
+
"version": "4.55.3",
|
| 520 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.3.tgz",
|
| 521 |
+
"integrity": "sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==",
|
| 522 |
+
"cpu": [
|
| 523 |
+
"arm64"
|
| 524 |
+
],
|
| 525 |
+
"dev": true,
|
| 526 |
+
"license": "MIT",
|
| 527 |
+
"optional": true,
|
| 528 |
+
"os": [
|
| 529 |
+
"linux"
|
| 530 |
+
]
|
| 531 |
+
},
|
| 532 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 533 |
+
"version": "4.55.3",
|
| 534 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.3.tgz",
|
| 535 |
+
"integrity": "sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==",
|
| 536 |
+
"cpu": [
|
| 537 |
+
"arm64"
|
| 538 |
+
],
|
| 539 |
+
"dev": true,
|
| 540 |
+
"license": "MIT",
|
| 541 |
+
"optional": true,
|
| 542 |
+
"os": [
|
| 543 |
+
"linux"
|
| 544 |
+
]
|
| 545 |
+
},
|
| 546 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 547 |
+
"version": "4.55.3",
|
| 548 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.3.tgz",
|
| 549 |
+
"integrity": "sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==",
|
| 550 |
+
"cpu": [
|
| 551 |
+
"loong64"
|
| 552 |
+
],
|
| 553 |
+
"dev": true,
|
| 554 |
+
"license": "MIT",
|
| 555 |
+
"optional": true,
|
| 556 |
+
"os": [
|
| 557 |
+
"linux"
|
| 558 |
+
]
|
| 559 |
+
},
|
| 560 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 561 |
+
"version": "4.55.3",
|
| 562 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.3.tgz",
|
| 563 |
+
"integrity": "sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==",
|
| 564 |
+
"cpu": [
|
| 565 |
+
"loong64"
|
| 566 |
+
],
|
| 567 |
+
"dev": true,
|
| 568 |
+
"license": "MIT",
|
| 569 |
+
"optional": true,
|
| 570 |
+
"os": [
|
| 571 |
+
"linux"
|
| 572 |
+
]
|
| 573 |
+
},
|
| 574 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 575 |
+
"version": "4.55.3",
|
| 576 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.3.tgz",
|
| 577 |
+
"integrity": "sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==",
|
| 578 |
+
"cpu": [
|
| 579 |
+
"ppc64"
|
| 580 |
+
],
|
| 581 |
+
"dev": true,
|
| 582 |
+
"license": "MIT",
|
| 583 |
+
"optional": true,
|
| 584 |
+
"os": [
|
| 585 |
+
"linux"
|
| 586 |
+
]
|
| 587 |
+
},
|
| 588 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 589 |
+
"version": "4.55.3",
|
| 590 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.3.tgz",
|
| 591 |
+
"integrity": "sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==",
|
| 592 |
+
"cpu": [
|
| 593 |
+
"ppc64"
|
| 594 |
+
],
|
| 595 |
+
"dev": true,
|
| 596 |
+
"license": "MIT",
|
| 597 |
+
"optional": true,
|
| 598 |
+
"os": [
|
| 599 |
+
"linux"
|
| 600 |
+
]
|
| 601 |
+
},
|
| 602 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 603 |
+
"version": "4.55.3",
|
| 604 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.3.tgz",
|
| 605 |
+
"integrity": "sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==",
|
| 606 |
+
"cpu": [
|
| 607 |
+
"riscv64"
|
| 608 |
+
],
|
| 609 |
+
"dev": true,
|
| 610 |
+
"license": "MIT",
|
| 611 |
+
"optional": true,
|
| 612 |
+
"os": [
|
| 613 |
+
"linux"
|
| 614 |
+
]
|
| 615 |
+
},
|
| 616 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 617 |
+
"version": "4.55.3",
|
| 618 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.3.tgz",
|
| 619 |
+
"integrity": "sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==",
|
| 620 |
+
"cpu": [
|
| 621 |
+
"riscv64"
|
| 622 |
+
],
|
| 623 |
+
"dev": true,
|
| 624 |
+
"license": "MIT",
|
| 625 |
+
"optional": true,
|
| 626 |
+
"os": [
|
| 627 |
+
"linux"
|
| 628 |
+
]
|
| 629 |
+
},
|
| 630 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 631 |
+
"version": "4.55.3",
|
| 632 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.3.tgz",
|
| 633 |
+
"integrity": "sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==",
|
| 634 |
+
"cpu": [
|
| 635 |
+
"s390x"
|
| 636 |
+
],
|
| 637 |
+
"dev": true,
|
| 638 |
+
"license": "MIT",
|
| 639 |
+
"optional": true,
|
| 640 |
+
"os": [
|
| 641 |
+
"linux"
|
| 642 |
+
]
|
| 643 |
+
},
|
| 644 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 645 |
+
"version": "4.55.3",
|
| 646 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.3.tgz",
|
| 647 |
+
"integrity": "sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==",
|
| 648 |
+
"cpu": [
|
| 649 |
+
"x64"
|
| 650 |
+
],
|
| 651 |
+
"dev": true,
|
| 652 |
+
"license": "MIT",
|
| 653 |
+
"optional": true,
|
| 654 |
+
"os": [
|
| 655 |
+
"linux"
|
| 656 |
+
]
|
| 657 |
+
},
|
| 658 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 659 |
+
"version": "4.55.3",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.3.tgz",
|
| 661 |
+
"integrity": "sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==",
|
| 662 |
+
"cpu": [
|
| 663 |
+
"x64"
|
| 664 |
+
],
|
| 665 |
+
"dev": true,
|
| 666 |
+
"license": "MIT",
|
| 667 |
+
"optional": true,
|
| 668 |
+
"os": [
|
| 669 |
+
"linux"
|
| 670 |
+
]
|
| 671 |
+
},
|
| 672 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 673 |
+
"version": "4.55.3",
|
| 674 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.3.tgz",
|
| 675 |
+
"integrity": "sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==",
|
| 676 |
+
"cpu": [
|
| 677 |
+
"x64"
|
| 678 |
+
],
|
| 679 |
+
"dev": true,
|
| 680 |
+
"license": "MIT",
|
| 681 |
+
"optional": true,
|
| 682 |
+
"os": [
|
| 683 |
+
"openbsd"
|
| 684 |
+
]
|
| 685 |
+
},
|
| 686 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 687 |
+
"version": "4.55.3",
|
| 688 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.3.tgz",
|
| 689 |
+
"integrity": "sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw==",
|
| 690 |
+
"cpu": [
|
| 691 |
+
"arm64"
|
| 692 |
+
],
|
| 693 |
+
"dev": true,
|
| 694 |
+
"license": "MIT",
|
| 695 |
+
"optional": true,
|
| 696 |
+
"os": [
|
| 697 |
+
"openharmony"
|
| 698 |
+
]
|
| 699 |
+
},
|
| 700 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 701 |
+
"version": "4.55.3",
|
| 702 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.3.tgz",
|
| 703 |
+
"integrity": "sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A==",
|
| 704 |
+
"cpu": [
|
| 705 |
+
"arm64"
|
| 706 |
+
],
|
| 707 |
+
"dev": true,
|
| 708 |
+
"license": "MIT",
|
| 709 |
+
"optional": true,
|
| 710 |
+
"os": [
|
| 711 |
+
"win32"
|
| 712 |
+
]
|
| 713 |
+
},
|
| 714 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 715 |
+
"version": "4.55.3",
|
| 716 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.3.tgz",
|
| 717 |
+
"integrity": "sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA==",
|
| 718 |
+
"cpu": [
|
| 719 |
+
"ia32"
|
| 720 |
+
],
|
| 721 |
+
"dev": true,
|
| 722 |
+
"license": "MIT",
|
| 723 |
+
"optional": true,
|
| 724 |
+
"os": [
|
| 725 |
+
"win32"
|
| 726 |
+
]
|
| 727 |
+
},
|
| 728 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 729 |
+
"version": "4.55.3",
|
| 730 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.3.tgz",
|
| 731 |
+
"integrity": "sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA==",
|
| 732 |
+
"cpu": [
|
| 733 |
+
"x64"
|
| 734 |
+
],
|
| 735 |
+
"dev": true,
|
| 736 |
+
"license": "MIT",
|
| 737 |
+
"optional": true,
|
| 738 |
+
"os": [
|
| 739 |
+
"win32"
|
| 740 |
+
]
|
| 741 |
+
},
|
| 742 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 743 |
+
"version": "4.55.3",
|
| 744 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.3.tgz",
|
| 745 |
+
"integrity": "sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg==",
|
| 746 |
+
"cpu": [
|
| 747 |
+
"x64"
|
| 748 |
+
],
|
| 749 |
+
"dev": true,
|
| 750 |
+
"license": "MIT",
|
| 751 |
+
"optional": true,
|
| 752 |
+
"os": [
|
| 753 |
+
"win32"
|
| 754 |
+
]
|
| 755 |
+
},
|
| 756 |
+
"node_modules/@types/estree": {
|
| 757 |
+
"version": "1.0.8",
|
| 758 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 759 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 760 |
+
"dev": true,
|
| 761 |
+
"license": "MIT"
|
| 762 |
+
},
|
| 763 |
+
"node_modules/esbuild": {
|
| 764 |
+
"version": "0.21.5",
|
| 765 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 766 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 767 |
+
"dev": true,
|
| 768 |
+
"hasInstallScript": true,
|
| 769 |
+
"license": "MIT",
|
| 770 |
+
"bin": {
|
| 771 |
+
"esbuild": "bin/esbuild"
|
| 772 |
+
},
|
| 773 |
+
"engines": {
|
| 774 |
+
"node": ">=12"
|
| 775 |
+
},
|
| 776 |
+
"optionalDependencies": {
|
| 777 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 778 |
+
"@esbuild/android-arm": "0.21.5",
|
| 779 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 780 |
+
"@esbuild/android-x64": "0.21.5",
|
| 781 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 782 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 783 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 784 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 785 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 786 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 787 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 788 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 789 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 790 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 791 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 792 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 793 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 794 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 795 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 796 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 797 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 798 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 799 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 800 |
+
}
|
| 801 |
+
},
|
| 802 |
+
"node_modules/fsevents": {
|
| 803 |
+
"version": "2.3.3",
|
| 804 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 805 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 806 |
+
"dev": true,
|
| 807 |
+
"hasInstallScript": true,
|
| 808 |
+
"license": "MIT",
|
| 809 |
+
"optional": true,
|
| 810 |
+
"os": [
|
| 811 |
+
"darwin"
|
| 812 |
+
],
|
| 813 |
+
"engines": {
|
| 814 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 815 |
+
}
|
| 816 |
+
},
|
| 817 |
+
"node_modules/nanoid": {
|
| 818 |
+
"version": "3.3.11",
|
| 819 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 820 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 821 |
+
"dev": true,
|
| 822 |
+
"funding": [
|
| 823 |
+
{
|
| 824 |
+
"type": "github",
|
| 825 |
+
"url": "https://github.com/sponsors/ai"
|
| 826 |
+
}
|
| 827 |
+
],
|
| 828 |
+
"license": "MIT",
|
| 829 |
+
"bin": {
|
| 830 |
+
"nanoid": "bin/nanoid.cjs"
|
| 831 |
+
},
|
| 832 |
+
"engines": {
|
| 833 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 834 |
+
}
|
| 835 |
+
},
|
| 836 |
+
"node_modules/picocolors": {
|
| 837 |
+
"version": "1.1.1",
|
| 838 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 839 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 840 |
+
"dev": true,
|
| 841 |
+
"license": "ISC"
|
| 842 |
+
},
|
| 843 |
+
"node_modules/postcss": {
|
| 844 |
+
"version": "8.5.6",
|
| 845 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
| 846 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 847 |
+
"dev": true,
|
| 848 |
+
"funding": [
|
| 849 |
+
{
|
| 850 |
+
"type": "opencollective",
|
| 851 |
+
"url": "https://opencollective.com/postcss/"
|
| 852 |
+
},
|
| 853 |
+
{
|
| 854 |
+
"type": "tidelift",
|
| 855 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 856 |
+
},
|
| 857 |
+
{
|
| 858 |
+
"type": "github",
|
| 859 |
+
"url": "https://github.com/sponsors/ai"
|
| 860 |
+
}
|
| 861 |
+
],
|
| 862 |
+
"license": "MIT",
|
| 863 |
+
"dependencies": {
|
| 864 |
+
"nanoid": "^3.3.11",
|
| 865 |
+
"picocolors": "^1.1.1",
|
| 866 |
+
"source-map-js": "^1.2.1"
|
| 867 |
+
},
|
| 868 |
+
"engines": {
|
| 869 |
+
"node": "^10 || ^12 || >=14"
|
| 870 |
+
}
|
| 871 |
+
},
|
| 872 |
+
"node_modules/rollup": {
|
| 873 |
+
"version": "4.55.3",
|
| 874 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz",
|
| 875 |
+
"integrity": "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==",
|
| 876 |
+
"dev": true,
|
| 877 |
+
"license": "MIT",
|
| 878 |
+
"dependencies": {
|
| 879 |
+
"@types/estree": "1.0.8"
|
| 880 |
+
},
|
| 881 |
+
"bin": {
|
| 882 |
+
"rollup": "dist/bin/rollup"
|
| 883 |
+
},
|
| 884 |
+
"engines": {
|
| 885 |
+
"node": ">=18.0.0",
|
| 886 |
+
"npm": ">=8.0.0"
|
| 887 |
+
},
|
| 888 |
+
"optionalDependencies": {
|
| 889 |
+
"@rollup/rollup-android-arm-eabi": "4.55.3",
|
| 890 |
+
"@rollup/rollup-android-arm64": "4.55.3",
|
| 891 |
+
"@rollup/rollup-darwin-arm64": "4.55.3",
|
| 892 |
+
"@rollup/rollup-darwin-x64": "4.55.3",
|
| 893 |
+
"@rollup/rollup-freebsd-arm64": "4.55.3",
|
| 894 |
+
"@rollup/rollup-freebsd-x64": "4.55.3",
|
| 895 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.55.3",
|
| 896 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.55.3",
|
| 897 |
+
"@rollup/rollup-linux-arm64-gnu": "4.55.3",
|
| 898 |
+
"@rollup/rollup-linux-arm64-musl": "4.55.3",
|
| 899 |
+
"@rollup/rollup-linux-loong64-gnu": "4.55.3",
|
| 900 |
+
"@rollup/rollup-linux-loong64-musl": "4.55.3",
|
| 901 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.55.3",
|
| 902 |
+
"@rollup/rollup-linux-ppc64-musl": "4.55.3",
|
| 903 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.55.3",
|
| 904 |
+
"@rollup/rollup-linux-riscv64-musl": "4.55.3",
|
| 905 |
+
"@rollup/rollup-linux-s390x-gnu": "4.55.3",
|
| 906 |
+
"@rollup/rollup-linux-x64-gnu": "4.55.3",
|
| 907 |
+
"@rollup/rollup-linux-x64-musl": "4.55.3",
|
| 908 |
+
"@rollup/rollup-openbsd-x64": "4.55.3",
|
| 909 |
+
"@rollup/rollup-openharmony-arm64": "4.55.3",
|
| 910 |
+
"@rollup/rollup-win32-arm64-msvc": "4.55.3",
|
| 911 |
+
"@rollup/rollup-win32-ia32-msvc": "4.55.3",
|
| 912 |
+
"@rollup/rollup-win32-x64-gnu": "4.55.3",
|
| 913 |
+
"@rollup/rollup-win32-x64-msvc": "4.55.3",
|
| 914 |
+
"fsevents": "~2.3.2"
|
| 915 |
+
}
|
| 916 |
+
},
|
| 917 |
+
"node_modules/source-map-js": {
|
| 918 |
+
"version": "1.2.1",
|
| 919 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 920 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 921 |
+
"dev": true,
|
| 922 |
+
"license": "BSD-3-Clause",
|
| 923 |
+
"engines": {
|
| 924 |
+
"node": ">=0.10.0"
|
| 925 |
+
}
|
| 926 |
+
},
|
| 927 |
+
"node_modules/typescript": {
|
| 928 |
+
"version": "5.9.3",
|
| 929 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
| 930 |
+
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
| 931 |
+
"dev": true,
|
| 932 |
+
"license": "Apache-2.0",
|
| 933 |
+
"bin": {
|
| 934 |
+
"tsc": "bin/tsc",
|
| 935 |
+
"tsserver": "bin/tsserver"
|
| 936 |
+
},
|
| 937 |
+
"engines": {
|
| 938 |
+
"node": ">=14.17"
|
| 939 |
+
}
|
| 940 |
+
},
|
| 941 |
+
"node_modules/vite": {
|
| 942 |
+
"version": "5.4.21",
|
| 943 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
| 944 |
+
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
| 945 |
+
"dev": true,
|
| 946 |
+
"license": "MIT",
|
| 947 |
+
"dependencies": {
|
| 948 |
+
"esbuild": "^0.21.3",
|
| 949 |
+
"postcss": "^8.4.43",
|
| 950 |
+
"rollup": "^4.20.0"
|
| 951 |
+
},
|
| 952 |
+
"bin": {
|
| 953 |
+
"vite": "bin/vite.js"
|
| 954 |
+
},
|
| 955 |
+
"engines": {
|
| 956 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 957 |
+
},
|
| 958 |
+
"funding": {
|
| 959 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 960 |
+
},
|
| 961 |
+
"optionalDependencies": {
|
| 962 |
+
"fsevents": "~2.3.3"
|
| 963 |
+
},
|
| 964 |
+
"peerDependencies": {
|
| 965 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 966 |
+
"less": "*",
|
| 967 |
+
"lightningcss": "^1.21.0",
|
| 968 |
+
"sass": "*",
|
| 969 |
+
"sass-embedded": "*",
|
| 970 |
+
"stylus": "*",
|
| 971 |
+
"sugarss": "*",
|
| 972 |
+
"terser": "^5.4.0"
|
| 973 |
+
},
|
| 974 |
+
"peerDependenciesMeta": {
|
| 975 |
+
"@types/node": {
|
| 976 |
+
"optional": true
|
| 977 |
+
},
|
| 978 |
+
"less": {
|
| 979 |
+
"optional": true
|
| 980 |
+
},
|
| 981 |
+
"lightningcss": {
|
| 982 |
+
"optional": true
|
| 983 |
+
},
|
| 984 |
+
"sass": {
|
| 985 |
+
"optional": true
|
| 986 |
+
},
|
| 987 |
+
"sass-embedded": {
|
| 988 |
+
"optional": true
|
| 989 |
+
},
|
| 990 |
+
"stylus": {
|
| 991 |
+
"optional": true
|
| 992 |
+
},
|
| 993 |
+
"sugarss": {
|
| 994 |
+
"optional": true
|
| 995 |
+
},
|
| 996 |
+
"terser": {
|
| 997 |
+
"optional": true
|
| 998 |
+
}
|
| 999 |
+
}
|
| 1000 |
+
}
|
| 1001 |
+
}
|
| 1002 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "nova-sim-frontend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "vite",
|
| 7 |
+
"build": "vite build",
|
| 8 |
+
"build:check": "tsc && vite build",
|
| 9 |
+
"preview": "vite preview",
|
| 10 |
+
"type-check": "tsc --noEmit"
|
| 11 |
+
},
|
| 12 |
+
"devDependencies": {
|
| 13 |
+
"typescript": "^5.3.3",
|
| 14 |
+
"vite": "^5.0.11"
|
| 15 |
+
}
|
| 16 |
+
}
|
frontend/src/api/client.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* WebSocket client for Nova-Sim server
|
| 3 |
+
*/
|
| 4 |
+
export class NovaSimClient {
|
| 5 |
+
constructor(apiPrefix = '/nova-sim/api/v1') {
|
| 6 |
+
this.ws = null;
|
| 7 |
+
this.reconnectAttempts = 0;
|
| 8 |
+
this.maxReconnectAttempts = 5;
|
| 9 |
+
this.reconnectTimeout = null;
|
| 10 |
+
this.apiPrefix = apiPrefix;
|
| 11 |
+
}
|
| 12 |
+
/**
|
| 13 |
+
* Connect to WebSocket server
|
| 14 |
+
*/
|
| 15 |
+
connect() {
|
| 16 |
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
| 17 |
+
console.log('[WS] Already connected');
|
| 18 |
+
return;
|
| 19 |
+
}
|
| 20 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 21 |
+
const wsUrl = `${protocol}//${window.location.host}${this.apiPrefix}/ws`;
|
| 22 |
+
console.log('[WS] Connecting to:', wsUrl);
|
| 23 |
+
this.ws = new WebSocket(wsUrl);
|
| 24 |
+
this.ws.onopen = () => {
|
| 25 |
+
console.log('[WS] Connected');
|
| 26 |
+
this.reconnectAttempts = 0;
|
| 27 |
+
this.onConnectionChange?.(true);
|
| 28 |
+
};
|
| 29 |
+
this.ws.onclose = () => {
|
| 30 |
+
console.log('[WS] Disconnected');
|
| 31 |
+
this.onConnectionChange?.(false);
|
| 32 |
+
this.attemptReconnect();
|
| 33 |
+
};
|
| 34 |
+
this.ws.onerror = (error) => {
|
| 35 |
+
console.error('[WS] Error:', error);
|
| 36 |
+
};
|
| 37 |
+
this.ws.onmessage = (event) => {
|
| 38 |
+
try {
|
| 39 |
+
const message = JSON.parse(event.data);
|
| 40 |
+
this.handleMessage(message);
|
| 41 |
+
}
|
| 42 |
+
catch (error) {
|
| 43 |
+
console.error('[WS] Failed to parse message:', error);
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
}
|
| 47 |
+
/**
|
| 48 |
+
* Disconnect from WebSocket server
|
| 49 |
+
*/
|
| 50 |
+
disconnect() {
|
| 51 |
+
if (this.reconnectTimeout) {
|
| 52 |
+
clearTimeout(this.reconnectTimeout);
|
| 53 |
+
this.reconnectTimeout = null;
|
| 54 |
+
}
|
| 55 |
+
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnect
|
| 56 |
+
this.ws?.close();
|
| 57 |
+
this.ws = null;
|
| 58 |
+
}
|
| 59 |
+
/**
|
| 60 |
+
* Attempt to reconnect after disconnect
|
| 61 |
+
*/
|
| 62 |
+
attemptReconnect() {
|
| 63 |
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
| 64 |
+
console.log('[WS] Max reconnect attempts reached');
|
| 65 |
+
return;
|
| 66 |
+
}
|
| 67 |
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000);
|
| 68 |
+
console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`);
|
| 69 |
+
this.reconnectTimeout = window.setTimeout(() => {
|
| 70 |
+
this.reconnectAttempts++;
|
| 71 |
+
this.connect();
|
| 72 |
+
}, delay);
|
| 73 |
+
}
|
| 74 |
+
/**
|
| 75 |
+
* Handle incoming WebSocket messages
|
| 76 |
+
*/
|
| 77 |
+
handleMessage(message) {
|
| 78 |
+
switch (message.type) {
|
| 79 |
+
case 'state':
|
| 80 |
+
this.onStateUpdate?.(message.data);
|
| 81 |
+
break;
|
| 82 |
+
case 'trainer_status':
|
| 83 |
+
this.onTrainerStatus?.(message.data);
|
| 84 |
+
break;
|
| 85 |
+
case 'trainer_notification':
|
| 86 |
+
this.onTrainerNotification?.(message.data);
|
| 87 |
+
break;
|
| 88 |
+
default:
|
| 89 |
+
console.warn('[WS] Unknown message type:', message);
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
/**
|
| 93 |
+
* Send message to server
|
| 94 |
+
*/
|
| 95 |
+
send(message) {
|
| 96 |
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
| 97 |
+
this.ws.send(JSON.stringify(message));
|
| 98 |
+
}
|
| 99 |
+
else {
|
| 100 |
+
console.warn('[WS] Cannot send message: not connected');
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
// ============================================================================
|
| 104 |
+
// Typed message senders
|
| 105 |
+
// ============================================================================
|
| 106 |
+
/**
|
| 107 |
+
* Send action command (locomotion robots)
|
| 108 |
+
*/
|
| 109 |
+
sendAction(vx, vy, vyaw) {
|
| 110 |
+
this.send({
|
| 111 |
+
type: 'action',
|
| 112 |
+
data: { vx, vy, vyaw },
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
/**
|
| 116 |
+
* Send teleop action (UR5 Cartesian control)
|
| 117 |
+
*/
|
| 118 |
+
sendTeleopAction(vx, vy, vz) {
|
| 119 |
+
this.send({
|
| 120 |
+
type: 'teleop_action',
|
| 121 |
+
data: { vx, vy, vz },
|
| 122 |
+
});
|
| 123 |
+
}
|
| 124 |
+
/**
|
| 125 |
+
* Reset environment
|
| 126 |
+
*/
|
| 127 |
+
sendReset(seed) {
|
| 128 |
+
this.send({
|
| 129 |
+
type: 'reset',
|
| 130 |
+
data: seed !== undefined ? { seed } : {},
|
| 131 |
+
});
|
| 132 |
+
}
|
| 133 |
+
/**
|
| 134 |
+
* Switch robot
|
| 135 |
+
*/
|
| 136 |
+
switchRobot(robot, scene) {
|
| 137 |
+
this.send({
|
| 138 |
+
type: 'switch_robot',
|
| 139 |
+
data: { robot, scene },
|
| 140 |
+
});
|
| 141 |
+
}
|
| 142 |
+
/**
|
| 143 |
+
* Send camera control
|
| 144 |
+
*/
|
| 145 |
+
sendCameraControl(data) {
|
| 146 |
+
this.send({
|
| 147 |
+
type: 'camera',
|
| 148 |
+
data,
|
| 149 |
+
});
|
| 150 |
+
}
|
| 151 |
+
/**
|
| 152 |
+
* Toggle camera follow mode
|
| 153 |
+
*/
|
| 154 |
+
setCameraFollow(follow) {
|
| 155 |
+
this.send({
|
| 156 |
+
type: 'camera_follow',
|
| 157 |
+
data: { follow },
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
/**
|
| 161 |
+
* Start homing sequence (UR5)
|
| 162 |
+
*/
|
| 163 |
+
startHome() {
|
| 164 |
+
this.send({ type: 'home' });
|
| 165 |
+
}
|
| 166 |
+
/**
|
| 167 |
+
* Stop homing sequence (UR5)
|
| 168 |
+
*/
|
| 169 |
+
stopHome() {
|
| 170 |
+
this.send({ type: 'stop_home' });
|
| 171 |
+
}
|
| 172 |
+
/**
|
| 173 |
+
* Set arm target position (UR5 IK mode)
|
| 174 |
+
*/
|
| 175 |
+
setArmTarget(x, y, z) {
|
| 176 |
+
this.send({
|
| 177 |
+
type: 'arm_target',
|
| 178 |
+
data: { x, y, z },
|
| 179 |
+
});
|
| 180 |
+
}
|
| 181 |
+
/**
|
| 182 |
+
* Set arm orientation (UR5 IK mode)
|
| 183 |
+
*/
|
| 184 |
+
setArmOrientation(roll, pitch, yaw) {
|
| 185 |
+
this.send({
|
| 186 |
+
type: 'arm_orientation',
|
| 187 |
+
data: { roll, pitch, yaw },
|
| 188 |
+
});
|
| 189 |
+
}
|
| 190 |
+
/**
|
| 191 |
+
* Toggle orientation control (UR5)
|
| 192 |
+
*/
|
| 193 |
+
setUseOrientation(enabled) {
|
| 194 |
+
this.send({
|
| 195 |
+
type: 'use_orientation',
|
| 196 |
+
data: { enabled },
|
| 197 |
+
});
|
| 198 |
+
}
|
| 199 |
+
/**
|
| 200 |
+
* Set joint positions (UR5 joint mode)
|
| 201 |
+
*/
|
| 202 |
+
setJointPositions(positions) {
|
| 203 |
+
this.send({
|
| 204 |
+
type: 'joint_positions',
|
| 205 |
+
data: { positions },
|
| 206 |
+
});
|
| 207 |
+
}
|
| 208 |
+
/**
|
| 209 |
+
* Set control mode (UR5)
|
| 210 |
+
*/
|
| 211 |
+
setControlMode(mode) {
|
| 212 |
+
this.send({
|
| 213 |
+
type: 'control_mode',
|
| 214 |
+
data: { mode },
|
| 215 |
+
});
|
| 216 |
+
}
|
| 217 |
+
/**
|
| 218 |
+
* Control gripper (UR5)
|
| 219 |
+
*/
|
| 220 |
+
setGripper(action, value) {
|
| 221 |
+
this.send({
|
| 222 |
+
type: 'gripper',
|
| 223 |
+
data: { action, value },
|
| 224 |
+
});
|
| 225 |
+
}
|
| 226 |
+
/**
|
| 227 |
+
* Start jogging (UR5)
|
| 228 |
+
*/
|
| 229 |
+
startJog(jogType, params) {
|
| 230 |
+
this.send({
|
| 231 |
+
type: 'start_jog',
|
| 232 |
+
data: { jog_type: jogType, params },
|
| 233 |
+
});
|
| 234 |
+
}
|
| 235 |
+
/**
|
| 236 |
+
* Stop jogging (UR5)
|
| 237 |
+
*/
|
| 238 |
+
stopJog() {
|
| 239 |
+
this.send({ type: 'stop_jog' });
|
| 240 |
+
}
|
| 241 |
+
/**
|
| 242 |
+
* Set Nova API mode (UR5)
|
| 243 |
+
*/
|
| 244 |
+
setNovaMode(enabled) {
|
| 245 |
+
this.send({
|
| 246 |
+
type: 'set_nova_mode',
|
| 247 |
+
data: { enabled },
|
| 248 |
+
});
|
| 249 |
+
}
|
| 250 |
+
/**
|
| 251 |
+
* Send episode control (trainer)
|
| 252 |
+
*/
|
| 253 |
+
sendEpisodeControl(action) {
|
| 254 |
+
this.send({
|
| 255 |
+
type: 'episode_control',
|
| 256 |
+
data: { action },
|
| 257 |
+
});
|
| 258 |
+
}
|
| 259 |
+
}
|
frontend/src/api/client.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
ClientMessage,
|
| 3 |
+
ServerMessage,
|
| 4 |
+
StateData,
|
| 5 |
+
TrainerStatusData,
|
| 6 |
+
TrainerNotificationData,
|
| 7 |
+
RobotType,
|
| 8 |
+
CameraData,
|
| 9 |
+
JogParams,
|
| 10 |
+
} from '../types/protocol';
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* WebSocket client for Nova-Sim server
|
| 14 |
+
*/
|
| 15 |
+
export class NovaSimClient {
|
| 16 |
+
private ws: WebSocket | null = null;
|
| 17 |
+
private reconnectAttempts = 0;
|
| 18 |
+
private readonly maxReconnectAttempts = 5;
|
| 19 |
+
private reconnectTimeout: number | null = null;
|
| 20 |
+
private readonly apiPrefix: string;
|
| 21 |
+
|
| 22 |
+
// Event handlers (set by main.ts)
|
| 23 |
+
public onStateUpdate?: (data: StateData) => void;
|
| 24 |
+
public onConnectionChange?: (connected: boolean) => void;
|
| 25 |
+
public onTrainerStatus?: (data: TrainerStatusData) => void;
|
| 26 |
+
public onTrainerNotification?: (data: TrainerNotificationData) => void;
|
| 27 |
+
|
| 28 |
+
constructor(apiPrefix: string = '/nova-sim/api/v1') {
|
| 29 |
+
this.apiPrefix = apiPrefix;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* Connect to WebSocket server
|
| 34 |
+
*/
|
| 35 |
+
connect(): void {
|
| 36 |
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
| 37 |
+
console.log('[WS] Already connected');
|
| 38 |
+
return;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 42 |
+
const wsUrl = `${protocol}//${window.location.host}${this.apiPrefix}/ws`;
|
| 43 |
+
|
| 44 |
+
console.log('[WS] Connecting to:', wsUrl);
|
| 45 |
+
this.ws = new WebSocket(wsUrl);
|
| 46 |
+
|
| 47 |
+
this.ws.onopen = () => {
|
| 48 |
+
console.log('[WS] Connected');
|
| 49 |
+
this.reconnectAttempts = 0;
|
| 50 |
+
this.onConnectionChange?.(true);
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
this.ws.onclose = () => {
|
| 54 |
+
console.log('[WS] Disconnected');
|
| 55 |
+
this.onConnectionChange?.(false);
|
| 56 |
+
this.attemptReconnect();
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
this.ws.onerror = (error) => {
|
| 60 |
+
console.error('[WS] Error:', error);
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
this.ws.onmessage = (event) => {
|
| 64 |
+
try {
|
| 65 |
+
const message: ServerMessage = JSON.parse(event.data);
|
| 66 |
+
this.handleMessage(message);
|
| 67 |
+
} catch (error) {
|
| 68 |
+
console.error('[WS] Failed to parse message:', error);
|
| 69 |
+
}
|
| 70 |
+
};
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/**
|
| 74 |
+
* Disconnect from WebSocket server
|
| 75 |
+
*/
|
| 76 |
+
disconnect(): void {
|
| 77 |
+
if (this.reconnectTimeout) {
|
| 78 |
+
clearTimeout(this.reconnectTimeout);
|
| 79 |
+
this.reconnectTimeout = null;
|
| 80 |
+
}
|
| 81 |
+
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnect
|
| 82 |
+
this.ws?.close();
|
| 83 |
+
this.ws = null;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/**
|
| 87 |
+
* Attempt to reconnect after disconnect
|
| 88 |
+
*/
|
| 89 |
+
private attemptReconnect(): void {
|
| 90 |
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
| 91 |
+
console.log('[WS] Max reconnect attempts reached');
|
| 92 |
+
return;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 10000);
|
| 96 |
+
console.log(`[WS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`);
|
| 97 |
+
|
| 98 |
+
this.reconnectTimeout = window.setTimeout(() => {
|
| 99 |
+
this.reconnectAttempts++;
|
| 100 |
+
this.connect();
|
| 101 |
+
}, delay);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/**
|
| 105 |
+
* Handle incoming WebSocket messages
|
| 106 |
+
*/
|
| 107 |
+
private handleMessage(message: ServerMessage): void {
|
| 108 |
+
switch (message.type) {
|
| 109 |
+
case 'state':
|
| 110 |
+
this.onStateUpdate?.(message.data);
|
| 111 |
+
break;
|
| 112 |
+
case 'trainer_status':
|
| 113 |
+
this.onTrainerStatus?.(message.data);
|
| 114 |
+
break;
|
| 115 |
+
case 'trainer_notification':
|
| 116 |
+
this.onTrainerNotification?.(message.data);
|
| 117 |
+
break;
|
| 118 |
+
default:
|
| 119 |
+
console.warn('[WS] Unknown message type:', message);
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/**
|
| 124 |
+
* Send message to server
|
| 125 |
+
*/
|
| 126 |
+
private send(message: ClientMessage): void {
|
| 127 |
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
| 128 |
+
this.ws.send(JSON.stringify(message));
|
| 129 |
+
} else {
|
| 130 |
+
console.warn('[WS] Cannot send message: not connected');
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// ============================================================================
|
| 135 |
+
// Typed message senders
|
| 136 |
+
// ============================================================================
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Send action command (locomotion robots)
|
| 140 |
+
*/
|
| 141 |
+
sendAction(vx: number, vy: number, vyaw: number): void {
|
| 142 |
+
this.send({
|
| 143 |
+
type: 'action',
|
| 144 |
+
data: { vx, vy, vyaw },
|
| 145 |
+
});
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/**
|
| 149 |
+
* Send teleop action (UR5 Cartesian control)
|
| 150 |
+
*/
|
| 151 |
+
sendTeleopAction(vx: number, vy: number, vz: number): void {
|
| 152 |
+
this.send({
|
| 153 |
+
type: 'teleop_action',
|
| 154 |
+
data: { vx, vy, vz },
|
| 155 |
+
});
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* Reset environment
|
| 160 |
+
*/
|
| 161 |
+
sendReset(seed?: number): void {
|
| 162 |
+
this.send({
|
| 163 |
+
type: 'reset',
|
| 164 |
+
data: seed !== undefined ? { seed } : {},
|
| 165 |
+
});
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Switch robot
|
| 170 |
+
*/
|
| 171 |
+
switchRobot(robot: RobotType, scene?: string | null): void {
|
| 172 |
+
this.send({
|
| 173 |
+
type: 'switch_robot',
|
| 174 |
+
data: { robot, scene },
|
| 175 |
+
});
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/**
|
| 179 |
+
* Send camera control
|
| 180 |
+
*/
|
| 181 |
+
sendCameraControl(data: CameraData): void {
|
| 182 |
+
this.send({
|
| 183 |
+
type: 'camera',
|
| 184 |
+
data,
|
| 185 |
+
});
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/**
|
| 189 |
+
* Toggle camera follow mode
|
| 190 |
+
*/
|
| 191 |
+
setCameraFollow(follow: boolean): void {
|
| 192 |
+
this.send({
|
| 193 |
+
type: 'camera_follow',
|
| 194 |
+
data: { follow },
|
| 195 |
+
});
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/**
|
| 199 |
+
* Start homing sequence (UR5)
|
| 200 |
+
*/
|
| 201 |
+
startHome(): void {
|
| 202 |
+
this.send({ type: 'home' });
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/**
|
| 206 |
+
* Stop homing sequence (UR5)
|
| 207 |
+
*/
|
| 208 |
+
stopHome(): void {
|
| 209 |
+
this.send({ type: 'stop_home' });
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/**
|
| 213 |
+
* Set arm target position (UR5 IK mode)
|
| 214 |
+
*/
|
| 215 |
+
setArmTarget(x: number, y: number, z: number): void {
|
| 216 |
+
this.send({
|
| 217 |
+
type: 'arm_target',
|
| 218 |
+
data: { x, y, z },
|
| 219 |
+
});
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/**
|
| 223 |
+
* Set arm orientation (UR5 IK mode)
|
| 224 |
+
*/
|
| 225 |
+
setArmOrientation(roll: number, pitch: number, yaw: number): void {
|
| 226 |
+
this.send({
|
| 227 |
+
type: 'arm_orientation',
|
| 228 |
+
data: { roll, pitch, yaw },
|
| 229 |
+
});
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Toggle orientation control (UR5)
|
| 234 |
+
*/
|
| 235 |
+
setUseOrientation(enabled: boolean): void {
|
| 236 |
+
this.send({
|
| 237 |
+
type: 'use_orientation',
|
| 238 |
+
data: { enabled },
|
| 239 |
+
});
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
/**
|
| 243 |
+
* Set joint positions (UR5 joint mode)
|
| 244 |
+
*/
|
| 245 |
+
setJointPositions(positions: number[]): void {
|
| 246 |
+
this.send({
|
| 247 |
+
type: 'joint_positions',
|
| 248 |
+
data: { positions },
|
| 249 |
+
});
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/**
|
| 253 |
+
* Set control mode (UR5)
|
| 254 |
+
*/
|
| 255 |
+
setControlMode(mode: 'ik' | 'joint'): void {
|
| 256 |
+
this.send({
|
| 257 |
+
type: 'control_mode',
|
| 258 |
+
data: { mode },
|
| 259 |
+
});
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/**
|
| 263 |
+
* Control gripper (UR5)
|
| 264 |
+
*/
|
| 265 |
+
setGripper(action: 'open' | 'close', value?: number): void {
|
| 266 |
+
this.send({
|
| 267 |
+
type: 'gripper',
|
| 268 |
+
data: { action, value },
|
| 269 |
+
});
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/**
|
| 273 |
+
* Start jogging (UR5)
|
| 274 |
+
*/
|
| 275 |
+
startJog(jogType: 'joint' | 'cartesian_translation' | 'cartesian_rotation', params: JogParams): void {
|
| 276 |
+
this.send({
|
| 277 |
+
type: 'start_jog',
|
| 278 |
+
data: { jog_type: jogType, params },
|
| 279 |
+
});
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
/**
|
| 283 |
+
* Stop jogging (UR5)
|
| 284 |
+
*/
|
| 285 |
+
stopJog(): void {
|
| 286 |
+
this.send({ type: 'stop_jog' });
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
/**
|
| 290 |
+
* Set Nova API mode (UR5)
|
| 291 |
+
*/
|
| 292 |
+
setNovaMode(enabled: boolean): void {
|
| 293 |
+
this.send({
|
| 294 |
+
type: 'set_nova_mode',
|
| 295 |
+
data: { enabled },
|
| 296 |
+
});
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
/**
|
| 300 |
+
* Send episode control (trainer)
|
| 301 |
+
*/
|
| 302 |
+
sendEpisodeControl(action: 'terminate' | 'truncate'): void {
|
| 303 |
+
this.send({
|
| 304 |
+
type: 'episode_control',
|
| 305 |
+
data: { action },
|
| 306 |
+
});
|
| 307 |
+
}
|
| 308 |
+
}
|
frontend/src/index.html
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<title>Nova Sim - Wandelbots Robot Simulator</title>
|
| 5 |
+
<meta charset="utf-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<link rel="stylesheet" href="./styles.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="video-container" id="viewport">
|
| 11 |
+
<img src="" draggable="false">
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<!-- State info panel - top right -->
|
| 15 |
+
<div class="state-panel" id="state_panel">
|
| 16 |
+
<!-- Connection status badge (inside panel) -->
|
| 17 |
+
<div class="connection-status-inline connecting" id="conn_status">
|
| 18 |
+
<span class="status-text" id="conn_status_text">Connecting...</span>
|
| 19 |
+
<span class="status-loader" id="conn_loader" aria-hidden="true"></span>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div id="locomotion_state">
|
| 23 |
+
<strong>Robot State</strong><br>
|
| 24 |
+
Position: <span id="loco_pos">0.00, 0.00, 0.00</span><br>
|
| 25 |
+
Orientation: <span id="loco_ori">0.00, 0.00, 0.00</span><br>
|
| 26 |
+
Steps: <span id="step_val">0</span><br>
|
| 27 |
+
Teleop: <span id="loco_teleop_display" style="display: inline-block; word-wrap: break-word; max-width: 100%;">-</span>
|
| 28 |
+
</div>
|
| 29 |
+
|
| 30 |
+
<div id="arm_state" style="display: none;">
|
| 31 |
+
<strong>Arm State</strong><br>
|
| 32 |
+
EE Pos: <span id="ee_pos">0.00, 0.00, 0.00</span><br>
|
| 33 |
+
EE Ori: <span id="ee_ori">0.00, 0.00, 0.00</span><br>
|
| 34 |
+
<span id="gripper_state_display">Gripper: <span id="gripper_val">50%</span><br></span>
|
| 35 |
+
Reward: <span id="arm_reward">-</span><br>
|
| 36 |
+
Mode: <span id="control_mode_display">IK</span> | Steps: <span id="arm_step_val">0</span><br>
|
| 37 |
+
Teleop: <span id="arm_teleop_display" style="display: inline-block; word-wrap: break-word; max-width: 100%;">-</span><br>
|
| 38 |
+
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(188, 190, 236, 0.2);">
|
| 39 |
+
<div id="nova_connection_badge" class="status-card" style="display: none; margin-bottom: 8px;">
|
| 40 |
+
<strong id="nova_badge_title" style="font-size: 0.85em; color: var(--wb-success); display: flex; align-items: center; gap: 6px;">
|
| 41 |
+
Nova Connected
|
| 42 |
+
</strong>
|
| 43 |
+
<div style="font-size: 0.75em; color: rgba(255, 255, 255, 0.85);">
|
| 44 |
+
<span id="nova_mode_text">Digital Twin Mode</span> - Read Only
|
| 45 |
+
</div>
|
| 46 |
+
<button id="nova_toggle_button" class="rl-btn" style="padding: 6px 12px; font-size: 0.75em;">Turn Nova Off</button>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
<div id="arm_teleop_vx" style="font-size: 0.7em;"></div>
|
| 50 |
+
<div id="arm_teleop_vy" style="font-size: 0.7em;"></div>
|
| 51 |
+
<div id="arm_teleop_vz" style="font-size: 0.7em;"></div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div id="trainer_status_card" class="status-card disconnected">
|
| 55 |
+
<strong>Trainer Connection</strong>
|
| 56 |
+
<div class="trainer-status disconnected" id="trainer_status_indicator">
|
| 57 |
+
<span class="trainer-status-dot" id="trainer_status_dot" aria-hidden="true"></span>
|
| 58 |
+
<span class="status-card-text" id="trainer_status_text">Trainer: Disconnected</span>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="overlay-tiles" id="overlay_tiles"></div>
|
| 64 |
+
|
| 65 |
+
<div class="overlay" id="control_panel">
|
| 66 |
+
<div class="panel-header" id="panel_header">
|
| 67 |
+
<h2 id="robot_title">Unitree G1 Humanoid</h2>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="panel-content" id="panel_content">
|
| 71 |
+
<div class="robot-selector">
|
| 72 |
+
<select id="robot_select">
|
| 73 |
+
<option value="">Loading robots...</option>
|
| 74 |
+
</select>
|
| 75 |
+
<div class="control-panel-info" id="arm_hints" style="display: none;">
|
| 76 |
+
<strong>Keyboard Controls</strong>
|
| 77 |
+
<ul>
|
| 78 |
+
<li>
|
| 79 |
+
<span class="hint-key">
|
| 80 |
+
<kbd>W/A/S/D</kbd>
|
| 81 |
+
<span>XY jog</span>
|
| 82 |
+
</span>
|
| 83 |
+
</li>
|
| 84 |
+
<li>
|
| 85 |
+
<span class="hint-key">
|
| 86 |
+
<kbd>R/F</kbd>
|
| 87 |
+
<span>Z nudge</span>
|
| 88 |
+
</span>
|
| 89 |
+
</li>
|
| 90 |
+
<li>
|
| 91 |
+
<span class="hint-key">
|
| 92 |
+
<kbd>Enter</kbd>
|
| 93 |
+
<span>Move to Home</span>
|
| 94 |
+
</span>
|
| 95 |
+
</li>
|
| 96 |
+
</ul>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="control-panel-info" id="loco_hints" style="display: none;">
|
| 99 |
+
<strong>Keyboard Controls</strong>
|
| 100 |
+
<ul>
|
| 101 |
+
<li>
|
| 102 |
+
<span class="hint-key">
|
| 103 |
+
<kbd>W/S</kbd>
|
| 104 |
+
<span>Forward/Back</span>
|
| 105 |
+
</span>
|
| 106 |
+
</li>
|
| 107 |
+
<li>
|
| 108 |
+
<span class="hint-key">
|
| 109 |
+
<kbd>A/D</kbd>
|
| 110 |
+
<span>Turn Left/Right</span>
|
| 111 |
+
</span>
|
| 112 |
+
</li>
|
| 113 |
+
<li>
|
| 114 |
+
<span class="hint-key">
|
| 115 |
+
<kbd>Q/E</kbd>
|
| 116 |
+
<span>Strafe Left/Right</span>
|
| 117 |
+
</span>
|
| 118 |
+
</li>
|
| 119 |
+
</ul>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="robot-info" id="robot_info">
|
| 122 |
+
29 DOF humanoid with RL walking policy
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
|
| 126 |
+
<!-- Locomotion controls (G1, Spot) -->
|
| 127 |
+
<div class="locomotion-controls" id="locomotion_controls">
|
| 128 |
+
<div class="control-group">
|
| 129 |
+
<label>Walking Controls (WASD or buttons)</label>
|
| 130 |
+
<div class="rl-buttons">
|
| 131 |
+
<div class="rl-row">
|
| 132 |
+
<button class="rl-btn">W Forward</button>
|
| 133 |
+
</div>
|
| 134 |
+
<div class="rl-row">
|
| 135 |
+
<button class="rl-btn">A Turn L</button>
|
| 136 |
+
<button class="rl-btn">S Back</button>
|
| 137 |
+
<button class="rl-btn">D Turn R</button>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="rl-row">
|
| 140 |
+
<button class="rl-btn">Q Strafe L</button>
|
| 141 |
+
<button class="rl-btn stop">Stop</button>
|
| 142 |
+
<button class="rl-btn">E Strafe R</button>
|
| 143 |
+
</div>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<!-- Arm controls (UR5) -->
|
| 149 |
+
<div class="arm-controls" id="arm_controls">
|
| 150 |
+
<div class="control-group">
|
| 151 |
+
<label>Control Mode</label>
|
| 152 |
+
<div class="mode-toggle">
|
| 153 |
+
<button id="mode_ik" class="active">IK (XYZ Target)</button>
|
| 154 |
+
<button id="mode_joint">Direct Joints</button>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<!-- IK Controls -->
|
| 159 |
+
<div class="ik-controls active" id="ik_controls">
|
| 160 |
+
<!-- Nova State/IK indicator boxes -->
|
| 161 |
+
<div id="nova_state_controller" style="display: none; margin-bottom: 10px; padding: 8px 10px; background: rgba(139, 127, 239, 0.15); border-radius: 6px; border: 1px solid rgba(139, 127, 239, 0.3); font-size: 0.75em;">
|
| 162 |
+
<strong>Nova State Streaming</strong><br>
|
| 163 |
+
<span style="opacity: 0.8;">Receiving real robot state</span>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<div id="nova_ik_controller" style="display: none; margin-bottom: 10px; padding: 8px 10px; background: rgba(139, 127, 239, 0.15); border-radius: 6px; border: 1px solid rgba(139, 127, 239, 0.3); font-size: 0.75em;">
|
| 167 |
+
<strong>Nova IK Controller</strong><br>
|
| 168 |
+
<span style="opacity: 0.8;">Using cloud IK</span>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<div class="control-group">
|
| 172 |
+
<label>Translation (XYZ)</label>
|
| 173 |
+
<div class="jog-controls">
|
| 174 |
+
<div class="jog-row">
|
| 175 |
+
<label style="color: #ff6b6b; font-weight: bold;">X</label>
|
| 176 |
+
<button class="jog-btn">−</button>
|
| 177 |
+
<span class="val-display" id="pos_x_val">0.00</span>
|
| 178 |
+
<button class="jog-btn">+</button>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="jog-row">
|
| 181 |
+
<label style="color: #51cf66; font-weight: bold;">Y</label>
|
| 182 |
+
<button class="jog-btn">−</button>
|
| 183 |
+
<span class="val-display" id="pos_y_val">0.00</span>
|
| 184 |
+
<button class="jog-btn">+</button>
|
| 185 |
+
</div>
|
| 186 |
+
<div class="jog-row">
|
| 187 |
+
<label style="color: #339af0; font-weight: bold;">Z</label>
|
| 188 |
+
<button class="jog-btn">−</button>
|
| 189 |
+
<span class="val-display" id="pos_z_val">0.00</span>
|
| 190 |
+
<button class="jog-btn">+</button>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
+
<div class="control-group" id="arm_orientation">
|
| 196 |
+
<label>Rotation (RPY)</label>
|
| 197 |
+
<div class="jog-controls">
|
| 198 |
+
<div class="jog-row">
|
| 199 |
+
<label style="color: #ff6b6b; font-weight: bold;">Rx</label>
|
| 200 |
+
<button class="jog-btn">−</button>
|
| 201 |
+
<span class="val-display" id="rot_x_val">0.00</span>
|
| 202 |
+
<button class="jog-btn">+</button>
|
| 203 |
+
</div>
|
| 204 |
+
<div class="jog-row">
|
| 205 |
+
<label style="color: #51cf66; font-weight: bold;">Ry</label>
|
| 206 |
+
<button class="jog-btn">−</button>
|
| 207 |
+
<span class="val-display" id="rot_y_val">0.00</span>
|
| 208 |
+
<button class="jog-btn">+</button>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="jog-row">
|
| 211 |
+
<label style="color: #339af0; font-weight: bold;">Rz</label>
|
| 212 |
+
<button class="jog-btn">−</button>
|
| 213 |
+
<span class="val-display" id="rot_z_val">0.00</span>
|
| 214 |
+
<button class="jog-btn">+</button>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
<div style="margin-top: 8px;">
|
| 218 |
+
<label style="font-size: 0.85em;">Velocity: <span id="rot_vel_val">0.3</span> rad/s</label>
|
| 219 |
+
<input type="range" id="rot_velocity" min="0.1" max="1.0" step="0.1" value="0.3" style="width: 100%;">
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<!-- Joint Controls -->
|
| 225 |
+
<div class="joint-controls" id="joint_controls">
|
| 226 |
+
<div class="control-group">
|
| 227 |
+
<label>Joint Positions</label>
|
| 228 |
+
<div class="jog-controls">
|
| 229 |
+
<div class="jog-row">
|
| 230 |
+
<label>J1</label>
|
| 231 |
+
<button class="jog-btn">−</button>
|
| 232 |
+
<span class="val-display" id="joint_0_val">-1.57</span>
|
| 233 |
+
<button class="jog-btn">+</button>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="jog-row">
|
| 236 |
+
<label>J2</label>
|
| 237 |
+
<button class="jog-btn">−</button>
|
| 238 |
+
<span class="val-display" id="joint_1_val">-1.57</span>
|
| 239 |
+
<button class="jog-btn">+</button>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="jog-row">
|
| 242 |
+
<label>J3</label>
|
| 243 |
+
<button class="jog-btn">−</button>
|
| 244 |
+
<span class="val-display" id="joint_2_val">1.57</span>
|
| 245 |
+
<button class="jog-btn">+</button>
|
| 246 |
+
</div>
|
| 247 |
+
<div class="jog-row">
|
| 248 |
+
<label>J4</label>
|
| 249 |
+
<button class="jog-btn">−</button>
|
| 250 |
+
<span class="val-display" id="joint_3_val">-1.57</span>
|
| 251 |
+
<button class="jog-btn">+</button>
|
| 252 |
+
</div>
|
| 253 |
+
<div class="jog-row">
|
| 254 |
+
<label>J5</label>
|
| 255 |
+
<button class="jog-btn">−</button>
|
| 256 |
+
<span class="val-display" id="joint_4_val">-1.57</span>
|
| 257 |
+
<button class="jog-btn">+</button>
|
| 258 |
+
</div>
|
| 259 |
+
<div class="jog-row">
|
| 260 |
+
<label>J6</label>
|
| 261 |
+
<button class="jog-btn">−</button>
|
| 262 |
+
<span class="val-display" id="joint_5_val">0.00</span>
|
| 263 |
+
<button class="jog-btn">+</button>
|
| 264 |
+
</div>
|
| 265 |
+
</div>
|
| 266 |
+
<div style="margin-top: 8px;">
|
| 267 |
+
<label style="font-size: 0.85em;">Velocity: <span id="joint_vel_val">0.5</span> rad/s</label>
|
| 268 |
+
<input type="range" id="joint_velocity" min="0.1" max="2.0" step="0.1" value="0.5" style="width: 100%;">
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
|
| 273 |
+
<div class="control-group" id="gripper_controls">
|
| 274 |
+
<label>Gripper</label>
|
| 275 |
+
<div class="gripper-btns">
|
| 276 |
+
<button class="rl-btn">Open</button>
|
| 277 |
+
<button class="rl-btn">Close</button>
|
| 278 |
+
</div>
|
| 279 |
+
</div>
|
| 280 |
+
|
| 281 |
+
</div>
|
| 282 |
+
|
| 283 |
+
<button class="danger" style="width: 100%; margin-top: 15px;">Reset Environment</button>
|
| 284 |
+
<button id="homeBtn" style="width: 100%; margin-top: 10px; display: none;">🏠 Move to Home</button>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
|
| 288 |
+
<div class="rl-notifications" id="rl_notifications"></div>
|
| 289 |
+
|
| 290 |
+
<div class="hint" id="hint_box" style="display: none;">
|
| 291 |
+
Drag: Rotate Camera<br>
|
| 292 |
+
Scroll: Zoom
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<!-- Hidden elements needed by JS but not visible -->
|
| 296 |
+
<span id="scene_label" style="display: none;"></span>
|
| 297 |
+
<span id="gripper_display" style="display: none;"></span>
|
| 298 |
+
<span id="pos_x_display" style="display: none;"></span>
|
| 299 |
+
<span id="pos_y_display" style="display: none;"></span>
|
| 300 |
+
<span id="pos_z_display" style="display: none;"></span>
|
| 301 |
+
<span id="rot_x_display" style="display: none;"></span>
|
| 302 |
+
<span id="rot_y_display" style="display: none;"></span>
|
| 303 |
+
<span id="rot_z_display" style="display: none;"></span>
|
| 304 |
+
<span id="joint_0_display" style="display: none;"></span>
|
| 305 |
+
<span id="joint_1_display" style="display: none;"></span>
|
| 306 |
+
<span id="joint_2_display" style="display: none;"></span>
|
| 307 |
+
<span id="joint_3_display" style="display: none;"></span>
|
| 308 |
+
<span id="joint_4_display" style="display: none;"></span>
|
| 309 |
+
<span id="joint_5_display" style="display: none;"></span>
|
| 310 |
+
<span id="gripper_joint_val" style="display: none;"></span>
|
| 311 |
+
<span id="gripper_joint_display" style="display: none;"></span>
|
| 312 |
+
<span id="trans_velocity" style="display: none;"></span>
|
| 313 |
+
<span id="trans_vel_val" style="display: none;"></span>
|
| 314 |
+
|
| 315 |
+
<script type="module" src="./main.ts"></script>
|
| 316 |
+
</body>
|
| 317 |
+
</html>
|
frontend/src/main.js
ADDED
|
@@ -0,0 +1,1282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const API_PREFIX = '/nova-sim/api/v1';
|
| 2 |
+
const WS_URL = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
| 3 |
+
window.location.host + API_PREFIX + '/ws';
|
| 4 |
+
let ws = null;
|
| 5 |
+
let reconnectTimer = null;
|
| 6 |
+
const connStatus = document.getElementById('conn_status');
|
| 7 |
+
const connStatusText = document.getElementById('conn_status_text');
|
| 8 |
+
const robotSelect = document.getElementById('robot_select');
|
| 9 |
+
const sceneLabel = document.getElementById('scene_label');
|
| 10 |
+
const overlayTiles = document.getElementById('overlay_tiles');
|
| 11 |
+
const trainerStatusCard = document.getElementById('trainer_status_card');
|
| 12 |
+
const trainerStatusIndicator = document.getElementById('trainer_status_indicator');
|
| 13 |
+
const trainerStatusText = document.getElementById('trainer_status_text');
|
| 14 |
+
const viewportImage = document.querySelector('.video-container img');
|
| 15 |
+
const robotTitle = document.getElementById('robot_title');
|
| 16 |
+
const robotInfo = document.getElementById('robot_info');
|
| 17 |
+
const metadataUrl = API_PREFIX + '/metadata';
|
| 18 |
+
const envUrl = API_PREFIX + '/env';
|
| 19 |
+
let metadataCache = null;
|
| 20 |
+
let envCache = null;
|
| 21 |
+
let pendingRobotSelection = null;
|
| 22 |
+
let currentRobot = null;
|
| 23 |
+
let currentScene = null;
|
| 24 |
+
let novaStateStreaming = false; // Track if Nova state streaming is active
|
| 25 |
+
let currentHomePose = null; // Store home pose from env data
|
| 26 |
+
let currentJointPositions = null; // Store latest joint positions from state stream
|
| 27 |
+
function updateConnectionLabel(text) {
|
| 28 |
+
if (connStatusText) {
|
| 29 |
+
connStatusText.innerText = text;
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
function enterConnectingState() {
|
| 33 |
+
updateConnectionLabel('Connecting...');
|
| 34 |
+
if (connStatus) {
|
| 35 |
+
connStatus.classList.add('connecting');
|
| 36 |
+
connStatus.classList.remove('disconnected');
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
function markConnectedState() {
|
| 40 |
+
updateConnectionLabel('Connected');
|
| 41 |
+
if (connStatus) {
|
| 42 |
+
connStatus.classList.remove('connecting', 'disconnected');
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
function refreshVideoStreams() {
|
| 46 |
+
const timestamp = Date.now();
|
| 47 |
+
if (viewportImage) {
|
| 48 |
+
viewportImage.src = `${API_PREFIX}/video_feed?ts=${timestamp}`;
|
| 49 |
+
}
|
| 50 |
+
if (overlayTiles) {
|
| 51 |
+
overlayTiles.querySelectorAll('img[data-feed]').forEach((img) => {
|
| 52 |
+
const feedName = img.dataset.feed || 'main';
|
| 53 |
+
img.src = `${API_PREFIX}/camera/${feedName}/video_feed?ts=${timestamp}`;
|
| 54 |
+
});
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
const novaToggleButton = document.getElementById('nova_toggle_button');
|
| 58 |
+
let novaEnabledState = false;
|
| 59 |
+
let novaManualToggle = false;
|
| 60 |
+
let novaAutoEnableRequested = false;
|
| 61 |
+
let novaPreconfigured = false;
|
| 62 |
+
function setNovaToggleState(available, enabled) {
|
| 63 |
+
if (!novaToggleButton) {
|
| 64 |
+
return;
|
| 65 |
+
}
|
| 66 |
+
novaEnabledState = !!enabled;
|
| 67 |
+
if (novaEnabledState) {
|
| 68 |
+
novaAutoEnableRequested = false;
|
| 69 |
+
}
|
| 70 |
+
if (!available) {
|
| 71 |
+
novaAutoEnableRequested = false;
|
| 72 |
+
}
|
| 73 |
+
novaToggleButton.style.display = available ? 'inline-flex' : 'none';
|
| 74 |
+
novaToggleButton.disabled = !available;
|
| 75 |
+
novaToggleButton.innerText = novaEnabledState ? 'Turn Nova Off' : 'Turn Nova On';
|
| 76 |
+
}
|
| 77 |
+
if (novaToggleButton) {
|
| 78 |
+
novaToggleButton.addEventListener('click', () => {
|
| 79 |
+
novaManualToggle = true;
|
| 80 |
+
send('set_nova_mode', { enabled: !novaEnabledState });
|
| 81 |
+
});
|
| 82 |
+
}
|
| 83 |
+
const NOVA_TRANSLATION_VELOCITY = 50.0;
|
| 84 |
+
const NOVA_ROTATION_VELOCITY = 0.3;
|
| 85 |
+
const NOVA_JOINT_VELOCITY = 0.5;
|
| 86 |
+
let novaVelocitiesConfigured = false;
|
| 87 |
+
function updateTrainerStatus(connected) {
|
| 88 |
+
if (!trainerStatusCard || !trainerStatusText) {
|
| 89 |
+
return;
|
| 90 |
+
}
|
| 91 |
+
trainerStatusText.innerText = connected ? 'Trainer: Connected' : 'Trainer: Disconnected';
|
| 92 |
+
trainerStatusCard.classList.toggle('connected', connected);
|
| 93 |
+
trainerStatusCard.classList.toggle('disconnected', !connected);
|
| 94 |
+
if (trainerStatusIndicator) {
|
| 95 |
+
trainerStatusIndicator.classList.toggle('connected', connected);
|
| 96 |
+
trainerStatusIndicator.classList.toggle('disconnected', !connected);
|
| 97 |
+
}
|
| 98 |
+
if (connected) {
|
| 99 |
+
configureNovaVelocities();
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
function configureNovaVelocities() {
|
| 103 |
+
if (novaVelocitiesConfigured) {
|
| 104 |
+
return;
|
| 105 |
+
}
|
| 106 |
+
const transSlider = document.getElementById('trans_velocity');
|
| 107 |
+
const rotSlider = document.getElementById('rot_velocity');
|
| 108 |
+
const jointSlider = document.getElementById('joint_velocity');
|
| 109 |
+
if (transSlider) {
|
| 110 |
+
transSlider.value = NOVA_TRANSLATION_VELOCITY;
|
| 111 |
+
updateTransVelocity();
|
| 112 |
+
}
|
| 113 |
+
if (rotSlider) {
|
| 114 |
+
rotSlider.value = NOVA_ROTATION_VELOCITY;
|
| 115 |
+
updateRotVelocity();
|
| 116 |
+
}
|
| 117 |
+
if (jointSlider) {
|
| 118 |
+
jointSlider.value = NOVA_JOINT_VELOCITY;
|
| 119 |
+
updateJointVelocity();
|
| 120 |
+
}
|
| 121 |
+
novaVelocitiesConfigured = true;
|
| 122 |
+
}
|
| 123 |
+
function refreshOverlayTiles() {
|
| 124 |
+
if (!metadataCache) {
|
| 125 |
+
return;
|
| 126 |
+
}
|
| 127 |
+
setupOverlayTiles();
|
| 128 |
+
}
|
| 129 |
+
const robotInfoText = {
|
| 130 |
+
'g1': '29 DOF humanoid with RL walking policy',
|
| 131 |
+
'spot': '12 DOF quadruped with trot gait controller',
|
| 132 |
+
'ur5': '6 DOF robot arm with Robotiq gripper',
|
| 133 |
+
'ur5_t_push': 'UR5e T-push task with stick tool'
|
| 134 |
+
};
|
| 135 |
+
const robotTitles = {
|
| 136 |
+
'g1': 'Unitree G1 Humanoid',
|
| 137 |
+
'spot': 'Boston Dynamics Spot',
|
| 138 |
+
'ur5': 'Universal Robots UR5e',
|
| 139 |
+
'ur5_t_push': 'UR5e T-Push Scene'
|
| 140 |
+
};
|
| 141 |
+
const locomotionControls = document.getElementById('locomotion_controls');
|
| 142 |
+
const armControls = document.getElementById('arm_controls');
|
| 143 |
+
const notificationList = document.getElementById('rl_notifications_list');
|
| 144 |
+
const teleopStatus = document.getElementById('teleop_status');
|
| 145 |
+
const armRobotTypes = new Set(['ur5', 'ur5_t_push']);
|
| 146 |
+
const armTeleopKeys = new Set(['KeyW', 'KeyA', 'KeyS', 'KeyD', 'KeyR', 'KeyF']);
|
| 147 |
+
const locomotionKeys = new Set([
|
| 148 |
+
'KeyW', 'KeyA', 'KeyS', 'KeyD', 'KeyQ', 'KeyE',
|
| 149 |
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'
|
| 150 |
+
]);
|
| 151 |
+
function maybeAutoEnableNova() {
|
| 152 |
+
if (!novaPreconfigured || novaManualToggle) {
|
| 153 |
+
return;
|
| 154 |
+
}
|
| 155 |
+
const activeSelection = getActiveSelection();
|
| 156 |
+
if (!armRobotTypes.has(activeSelection.robot)) {
|
| 157 |
+
return;
|
| 158 |
+
}
|
| 159 |
+
if (novaEnabledState) {
|
| 160 |
+
novaAutoEnableRequested = false;
|
| 161 |
+
return;
|
| 162 |
+
}
|
| 163 |
+
if (novaAutoEnableRequested) {
|
| 164 |
+
return;
|
| 165 |
+
}
|
| 166 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 167 |
+
send('set_nova_mode', { enabled: true });
|
| 168 |
+
novaAutoEnableRequested = true;
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
let teleopTranslationStep = 0.005; // meters per keyboard nudge
|
| 172 |
+
let teleopVerticalStep = 0.01;
|
| 173 |
+
const TELEOP_REPEAT_INTERVAL_MS = 80;
|
| 174 |
+
const NOTIFICATION_DURATION_MS = 5000;
|
| 175 |
+
let teleopRepeatTimer = null;
|
| 176 |
+
let lastTeleopCommand = { dx: 0, dy: 0, dz: 0 };
|
| 177 |
+
const armJogKeyMap = {
|
| 178 |
+
KeyW: { jogType: 'cartesian_translation', axis: 'x', direction: '+' },
|
| 179 |
+
KeyS: { jogType: 'cartesian_translation', axis: 'x', direction: '-' },
|
| 180 |
+
KeyA: { jogType: 'cartesian_translation', axis: 'y', direction: '+' },
|
| 181 |
+
KeyD: { jogType: 'cartesian_translation', axis: 'y', direction: '-' },
|
| 182 |
+
KeyR: { jogType: 'cartesian_translation', axis: 'z', direction: '+' },
|
| 183 |
+
KeyF: { jogType: 'cartesian_translation', axis: 'z', direction: '-' },
|
| 184 |
+
};
|
| 185 |
+
function humanizeScene(scene) {
|
| 186 |
+
if (!scene) {
|
| 187 |
+
return 'Default';
|
| 188 |
+
}
|
| 189 |
+
const cleaned = scene.replace(/^scene_/, '').replace(/_/g, ' ');
|
| 190 |
+
return cleaned.replace(/\\b\\w/g, (char) => char.toUpperCase());
|
| 191 |
+
}
|
| 192 |
+
function buildSelectionValue(robot, scene) {
|
| 193 |
+
return scene ? `${robot}::${scene}` : robot;
|
| 194 |
+
}
|
| 195 |
+
function parseSelection(value) {
|
| 196 |
+
if (!value) {
|
| 197 |
+
return { robot: 'g1', scene: null };
|
| 198 |
+
}
|
| 199 |
+
const [robotPart, scenePart] = value.split('::');
|
| 200 |
+
return { robot: robotPart, scene: scenePart || null };
|
| 201 |
+
}
|
| 202 |
+
function createRobotSceneOption(robot, scene, label) {
|
| 203 |
+
const option = document.createElement("option");
|
| 204 |
+
option.value = buildSelectionValue(robot, scene);
|
| 205 |
+
const sceneText = scene ? ` · ${humanizeScene(scene)}` : '';
|
| 206 |
+
option.textContent = `${label || robot}${sceneText}`;
|
| 207 |
+
return option;
|
| 208 |
+
}
|
| 209 |
+
function getDefaultSelection(metadata) {
|
| 210 |
+
if (!metadata) {
|
| 211 |
+
return '';
|
| 212 |
+
}
|
| 213 |
+
const robots = Object.keys(metadata.robots);
|
| 214 |
+
if (!robots.length) {
|
| 215 |
+
return '';
|
| 216 |
+
}
|
| 217 |
+
const preferred = metadata.robots['ur5_t_push'] ? 'ur5_t_push' : robots[0];
|
| 218 |
+
const preferredRobot = metadata.robots[preferred];
|
| 219 |
+
const preferredScenes = preferredRobot && preferredRobot.scenes;
|
| 220 |
+
const defaultScene = (metadata.default_scene && metadata.default_scene[preferred])
|
| 221 |
+
|| (preferredScenes && preferredScenes[0]) || '';
|
| 222 |
+
return buildSelectionValue(preferred, defaultScene);
|
| 223 |
+
}
|
| 224 |
+
function populateRobotOptions(metadata) {
|
| 225 |
+
if (!metadata) {
|
| 226 |
+
return null;
|
| 227 |
+
}
|
| 228 |
+
robotSelect.innerHTML = "";
|
| 229 |
+
Object.entries(metadata.robots).forEach(([robot, meta]) => {
|
| 230 |
+
const scenes = meta.scenes || [];
|
| 231 |
+
if (scenes.length <= 1) {
|
| 232 |
+
const scene = scenes[0] || "";
|
| 233 |
+
robotSelect.appendChild(createRobotSceneOption(robot, scene, meta.label));
|
| 234 |
+
}
|
| 235 |
+
else {
|
| 236 |
+
const group = document.createElement("optgroup");
|
| 237 |
+
group.label = meta.label || robot;
|
| 238 |
+
scenes.forEach((scene) => {
|
| 239 |
+
group.appendChild(createRobotSceneOption(robot, scene, meta.label));
|
| 240 |
+
});
|
| 241 |
+
robotSelect.appendChild(group);
|
| 242 |
+
}
|
| 243 |
+
});
|
| 244 |
+
const defaultValue = getDefaultSelection(metadata);
|
| 245 |
+
if (defaultValue) {
|
| 246 |
+
robotSelect.value = defaultValue;
|
| 247 |
+
const parsed = parseSelection(defaultValue);
|
| 248 |
+
return parsed;
|
| 249 |
+
}
|
| 250 |
+
return null;
|
| 251 |
+
}
|
| 252 |
+
async function setupOverlayTiles() {
|
| 253 |
+
if (!overlayTiles) {
|
| 254 |
+
return;
|
| 255 |
+
}
|
| 256 |
+
if (!envCache) {
|
| 257 |
+
overlayTiles.innerHTML = "";
|
| 258 |
+
overlayTiles.dataset.overlayKey = "";
|
| 259 |
+
overlayTiles.style.display = 'none';
|
| 260 |
+
return;
|
| 261 |
+
}
|
| 262 |
+
// Get overlay camera feeds from envCache (excludes main camera)
|
| 263 |
+
const allFeeds = envCache.camera_feeds || [];
|
| 264 |
+
const overlayFeeds = allFeeds.filter(feed => feed.name !== 'main');
|
| 265 |
+
if (!overlayFeeds.length) {
|
| 266 |
+
overlayTiles.innerHTML = "";
|
| 267 |
+
overlayTiles.dataset.overlayKey = "";
|
| 268 |
+
overlayTiles.style.display = 'none';
|
| 269 |
+
return;
|
| 270 |
+
}
|
| 271 |
+
const feedNames = overlayFeeds.map((feed) => feed.name || "aux");
|
| 272 |
+
const key = `${envCache.robot || ''}|${envCache.scene || ''}|${feedNames.join(',')}`;
|
| 273 |
+
if (overlayTiles.dataset.overlayKey === key) {
|
| 274 |
+
overlayTiles.style.display = 'flex';
|
| 275 |
+
return;
|
| 276 |
+
}
|
| 277 |
+
overlayTiles.dataset.overlayKey = key;
|
| 278 |
+
overlayTiles.innerHTML = "";
|
| 279 |
+
overlayTiles.style.display = 'flex';
|
| 280 |
+
overlayFeeds.forEach((feed) => {
|
| 281 |
+
const tile = document.createElement("div");
|
| 282 |
+
tile.className = "overlay-tile";
|
| 283 |
+
const img = document.createElement("img");
|
| 284 |
+
const feedName = feed.name || "aux";
|
| 285 |
+
img.dataset.feed = feedName;
|
| 286 |
+
img.src = feed.url + `?ts=${Date.now()}`;
|
| 287 |
+
tile.appendChild(img);
|
| 288 |
+
const label = document.createElement("div");
|
| 289 |
+
label.className = "overlay-label";
|
| 290 |
+
label.innerText = feed.label || feed.name;
|
| 291 |
+
tile.appendChild(label);
|
| 292 |
+
overlayTiles.appendChild(tile);
|
| 293 |
+
});
|
| 294 |
+
}
|
| 295 |
+
async function loadMetadata() {
|
| 296 |
+
try {
|
| 297 |
+
const resp = await fetch(metadataUrl);
|
| 298 |
+
if (!resp.ok) {
|
| 299 |
+
console.warn("Failed to load metadata");
|
| 300 |
+
return;
|
| 301 |
+
}
|
| 302 |
+
metadataCache = await resp.json();
|
| 303 |
+
const selection = populateRobotOptions(metadataCache);
|
| 304 |
+
if (selection) {
|
| 305 |
+
updateRobotUI(selection.robot, selection.scene);
|
| 306 |
+
refreshOverlayTiles();
|
| 307 |
+
}
|
| 308 |
+
const novaInfo = metadataCache && metadataCache.nova_api;
|
| 309 |
+
novaPreconfigured = Boolean(novaInfo && novaInfo.preconfigured);
|
| 310 |
+
setNovaToggleState(!!(novaInfo && novaInfo.preconfigured), !!(novaInfo && novaInfo.preconfigured));
|
| 311 |
+
maybeAutoEnableNova();
|
| 312 |
+
if (ws && ws.readyState === WebSocket.OPEN && pendingRobotSelection) {
|
| 313 |
+
send('switch_robot', {
|
| 314 |
+
robot: pendingRobotSelection.robot,
|
| 315 |
+
scene: pendingRobotSelection.scene
|
| 316 |
+
});
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
catch (error) {
|
| 320 |
+
console.warn("Metadata fetch error:", error);
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
async function loadEnvData() {
|
| 324 |
+
try {
|
| 325 |
+
console.log('Loading env data from:', envUrl);
|
| 326 |
+
const resp = await fetch(envUrl);
|
| 327 |
+
if (!resp.ok) {
|
| 328 |
+
console.warn("Failed to load env data, status:", resp.status);
|
| 329 |
+
return;
|
| 330 |
+
}
|
| 331 |
+
envCache = await resp.json();
|
| 332 |
+
console.log('Env data loaded:', envCache);
|
| 333 |
+
// Store home_pose if available
|
| 334 |
+
if (envCache && envCache.home_pose) {
|
| 335 |
+
currentHomePose = envCache.home_pose;
|
| 336 |
+
console.log('Home pose set to:', currentHomePose);
|
| 337 |
+
}
|
| 338 |
+
else {
|
| 339 |
+
console.warn('No home_pose in env data:', envCache);
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
catch (error) {
|
| 343 |
+
console.error("Env fetch error:", error);
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
function connect() {
|
| 347 |
+
ws = new WebSocket(WS_URL);
|
| 348 |
+
loadMetadata();
|
| 349 |
+
loadEnvData();
|
| 350 |
+
ws.onopen = () => {
|
| 351 |
+
markConnectedState();
|
| 352 |
+
refreshVideoStreams();
|
| 353 |
+
refreshOverlayTiles();
|
| 354 |
+
novaAutoEnableRequested = false;
|
| 355 |
+
maybeAutoEnableNova();
|
| 356 |
+
if (reconnectTimer) {
|
| 357 |
+
clearInterval(reconnectTimer);
|
| 358 |
+
reconnectTimer = null;
|
| 359 |
+
}
|
| 360 |
+
if (pendingRobotSelection) {
|
| 361 |
+
send('switch_robot', {
|
| 362 |
+
robot: pendingRobotSelection.robot,
|
| 363 |
+
scene: pendingRobotSelection.scene
|
| 364 |
+
});
|
| 365 |
+
}
|
| 366 |
+
};
|
| 367 |
+
ws.onclose = () => {
|
| 368 |
+
enterConnectingState();
|
| 369 |
+
// Auto-reconnect
|
| 370 |
+
if (!reconnectTimer) {
|
| 371 |
+
reconnectTimer = setInterval(() => {
|
| 372 |
+
if (ws.readyState === WebSocket.CLOSED) {
|
| 373 |
+
connect();
|
| 374 |
+
}
|
| 375 |
+
}, 2000);
|
| 376 |
+
}
|
| 377 |
+
};
|
| 378 |
+
ws.onerror = (err) => {
|
| 379 |
+
console.error('WebSocket error:', err);
|
| 380 |
+
};
|
| 381 |
+
ws.onmessage = (event) => {
|
| 382 |
+
try {
|
| 383 |
+
const msg = JSON.parse(event.data);
|
| 384 |
+
if (msg.type === 'state') {
|
| 385 |
+
const data = msg.data;
|
| 386 |
+
// Check if robot or scene changed in state stream
|
| 387 |
+
if (data.robot && data.robot !== currentRobot) {
|
| 388 |
+
console.log(`Robot changed from ${currentRobot} to ${data.robot}`);
|
| 389 |
+
currentRobot = data.robot;
|
| 390 |
+
updateRobotUI(data.robot, data.scene);
|
| 391 |
+
// Fetch new environment info only when robot actually changes
|
| 392 |
+
fetch('/nova-sim/api/v1/env')
|
| 393 |
+
.then(r => r.json())
|
| 394 |
+
.then(envData => {
|
| 395 |
+
console.log('Robot changed, fetched env data:', envData);
|
| 396 |
+
envData.has_gripper = envData.envData.has_gripper || false;
|
| 397 |
+
envData.control_mode = envData.envData.control_mode || 'ik';
|
| 398 |
+
if (envData.home_pose) {
|
| 399 |
+
currentHomePose = envData.home_pose;
|
| 400 |
+
console.log('Updated home pose from robot change:', currentHomePose);
|
| 401 |
+
}
|
| 402 |
+
else {
|
| 403 |
+
console.warn('No home_pose in robot change env data');
|
| 404 |
+
}
|
| 405 |
+
});
|
| 406 |
+
}
|
| 407 |
+
if (data.scene && data.scene !== currentScene) {
|
| 408 |
+
console.log(`Scene changed from ${currentScene} to ${data.scene}`);
|
| 409 |
+
currentScene = data.scene;
|
| 410 |
+
}
|
| 411 |
+
if (typeof data.trainer_connected === 'boolean') {
|
| 412 |
+
updateTrainerStatus(data.trainer_connected);
|
| 413 |
+
}
|
| 414 |
+
if (currentRobot === 'ur5' || currentRobot === 'ur5_t_push') {
|
| 415 |
+
// UR5 state - Access observation data first
|
| 416 |
+
const obs = data.observation || {};
|
| 417 |
+
// Update end effector position
|
| 418 |
+
if (obs.end_effector) {
|
| 419 |
+
const ee = obs.end_effector;
|
| 420 |
+
document.getElementById('ee_pos').innerText =
|
| 421 |
+
ee.x.toFixed(2) + ', ' + ee.y.toFixed(2) + ', ' + ee.z.toFixed(2);
|
| 422 |
+
}
|
| 423 |
+
// EE Orientation - convert quaternion to euler for display
|
| 424 |
+
if (obs.ee_orientation) {
|
| 425 |
+
const q = obs.ee_orientation;
|
| 426 |
+
const euler = quatToEuler(q.w, q.x, q.y, q.z);
|
| 427 |
+
document.getElementById('ee_ori').innerText =
|
| 428 |
+
euler[0].toFixed(2) + ', ' + euler[1].toFixed(2) + ', ' + euler[2].toFixed(2);
|
| 429 |
+
}
|
| 430 |
+
// Show/hide gripper UI based on envData.has_gripper
|
| 431 |
+
const gripperStateDisplay = document.getElementById('gripper_state_display');
|
| 432 |
+
const gripperControls = document.getElementById('gripper_controls');
|
| 433 |
+
if (data.envData.has_gripper) {
|
| 434 |
+
gripperStateDisplay.style.display = '';
|
| 435 |
+
gripperControls.style.display = '';
|
| 436 |
+
// Gripper: 0=open, 255=closed (Robotiq 2F-85)
|
| 437 |
+
if (obs.gripper !== undefined) {
|
| 438 |
+
document.getElementById('gripper_val').innerText =
|
| 439 |
+
((255 - obs.gripper) / 255 * 100).toFixed(0) + '% open';
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
else {
|
| 443 |
+
gripperStateDisplay.style.display = 'none';
|
| 444 |
+
gripperControls.style.display = 'none';
|
| 445 |
+
}
|
| 446 |
+
document.getElementById('arm_step_val').innerText = data.steps;
|
| 447 |
+
const rewardEl = document.getElementById('arm_reward');
|
| 448 |
+
if (data.reward === null || data.reward === undefined) {
|
| 449 |
+
rewardEl.innerText = '-';
|
| 450 |
+
}
|
| 451 |
+
else {
|
| 452 |
+
rewardEl.innerText = data.reward.toFixed(3);
|
| 453 |
+
}
|
| 454 |
+
// Update joint position display (actual positions)
|
| 455 |
+
if (obs.joint_positions) {
|
| 456 |
+
const jp = obs.joint_positions;
|
| 457 |
+
currentJointPositions = jp; // Store for homing
|
| 458 |
+
const jointPosDisplay = document.getElementById('joint_pos_display');
|
| 459 |
+
if (jointPosDisplay) {
|
| 460 |
+
jointPosDisplay.innerText = jp.map(j => j.toFixed(2)).join(', ');
|
| 461 |
+
}
|
| 462 |
+
// Update jog button displays with actual joint positions
|
| 463 |
+
for (let i = 0; i < 6; i++) {
|
| 464 |
+
const el = document.getElementById('joint_' + i + '_val');
|
| 465 |
+
if (el) {
|
| 466 |
+
el.innerText = jp[i].toFixed(2);
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
// Debug: log first joint update
|
| 470 |
+
if (!window.debugJointLogCount)
|
| 471 |
+
window.debugJointLogCount = 0;
|
| 472 |
+
if (window.debugJointLogCount < 2) {
|
| 473 |
+
console.log('Joint positions update:', jp.map(j => j.toFixed(2)));
|
| 474 |
+
console.log('Joint 0 element found:', !!document.getElementById('joint_0_val'));
|
| 475 |
+
window.debugJointLogCount++;
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
// Update cartesian position displays with actual EE position
|
| 479 |
+
if (obs.end_effector) {
|
| 480 |
+
const ee = obs.end_effector;
|
| 481 |
+
const posXEl = document.getElementById('pos_x_val');
|
| 482 |
+
const posYEl = document.getElementById('pos_y_val');
|
| 483 |
+
const posZEl = document.getElementById('pos_z_val');
|
| 484 |
+
if (posXEl)
|
| 485 |
+
posXEl.innerText = ee.x.toFixed(3);
|
| 486 |
+
if (posYEl)
|
| 487 |
+
posYEl.innerText = ee.y.toFixed(3);
|
| 488 |
+
if (posZEl)
|
| 489 |
+
posZEl.innerText = ee.z.toFixed(3);
|
| 490 |
+
// Debug: log first few updates
|
| 491 |
+
if (!window.debugLogCount)
|
| 492 |
+
window.debugLogCount = 0;
|
| 493 |
+
if (window.debugLogCount < 3) {
|
| 494 |
+
console.log('EE Position update:', ee.x.toFixed(3), ee.y.toFixed(3), ee.z.toFixed(3));
|
| 495 |
+
console.log('Elements found:', !!posXEl, !!posYEl, !!posZEl);
|
| 496 |
+
window.debugLogCount++;
|
| 497 |
+
}
|
| 498 |
+
}
|
| 499 |
+
// Update rotation displays with actual EE orientation
|
| 500 |
+
if (obs.ee_orientation) {
|
| 501 |
+
const q = obs.ee_orientation;
|
| 502 |
+
const euler = quatToEuler(q.w, q.x, q.y, q.z);
|
| 503 |
+
document.getElementById('rot_x_val').innerText = euler[0].toFixed(2);
|
| 504 |
+
document.getElementById('rot_y_val').innerText = euler[1].toFixed(2);
|
| 505 |
+
document.getElementById('rot_z_val').innerText = euler[2].toFixed(2);
|
| 506 |
+
}
|
| 507 |
+
// Update control mode display
|
| 508 |
+
if (data.envData.control_mode) {
|
| 509 |
+
document.getElementById('envData.control_mode_display').innerText =
|
| 510 |
+
data.envData.control_mode === 'ik' ? 'IK' : 'Joint';
|
| 511 |
+
// Sync UI if mode changed externally
|
| 512 |
+
if (currentControlMode !== data.envData.control_mode) {
|
| 513 |
+
setControlMode(data.envData.control_mode);
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
// Update Nova API controller status
|
| 517 |
+
if (data.nova_api) {
|
| 518 |
+
const stateController = document.getElementById('nova_state_controller');
|
| 519 |
+
const ikController = document.getElementById('nova_ik_controller');
|
| 520 |
+
const badge = document.getElementById('nova_connection_badge');
|
| 521 |
+
const badgeTitle = document.getElementById('nova_badge_title');
|
| 522 |
+
const modeText = document.getElementById('nova_mode_text');
|
| 523 |
+
setNovaToggleState(data.nova_api.available, data.nova_api.enabled);
|
| 524 |
+
// Track Nova state streaming status
|
| 525 |
+
novaStateStreaming = data.nova_api.state_streaming || false;
|
| 526 |
+
if (data.nova_api.available) {
|
| 527 |
+
badge.style.display = 'block';
|
| 528 |
+
}
|
| 529 |
+
else {
|
| 530 |
+
badge.style.display = 'none';
|
| 531 |
+
}
|
| 532 |
+
badge.classList.toggle('connected', data.nova_api.enabled);
|
| 533 |
+
badge.classList.toggle('disconnected', !data.nova_api.enabled);
|
| 534 |
+
const connectedColor = data.nova_api.connected ? 'var(--wb-success)' : 'var(--wb-highlight)';
|
| 535 |
+
if (stateController) {
|
| 536 |
+
stateController.innerText = data.nova_api.state_streaming ? 'Nova API' : 'Internal';
|
| 537 |
+
stateController.style.color = connectedColor;
|
| 538 |
+
}
|
| 539 |
+
if (ikController) {
|
| 540 |
+
ikController.innerText = data.nova_api.ik ? 'Nova API' : 'Internal';
|
| 541 |
+
ikController.style.color = data.nova_api.ik ? 'var(--wb-success)' : 'var(--wb-highlight)';
|
| 542 |
+
}
|
| 543 |
+
if (badgeTitle) {
|
| 544 |
+
if (data.nova_api.enabled) {
|
| 545 |
+
badgeTitle.innerText = data.nova_api.connected ? '🌐 Nova Connected' : '🌐 Nova Powering Up';
|
| 546 |
+
}
|
| 547 |
+
else {
|
| 548 |
+
badgeTitle.innerText = '🌐 Nova Disabled';
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
if (modeText) {
|
| 552 |
+
if (data.nova_api.connected) {
|
| 553 |
+
if (data.nova_api.state_streaming && data.nova_api.ik) {
|
| 554 |
+
modeText.innerText = 'Hybrid Mode';
|
| 555 |
+
}
|
| 556 |
+
else if (data.nova_api.state_streaming) {
|
| 557 |
+
modeText.innerText = 'Digital Twin Mode';
|
| 558 |
+
}
|
| 559 |
+
else if (data.nova_api.ik) {
|
| 560 |
+
modeText.innerText = 'Nova IK Mode';
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
else if (data.nova_api.enabled) {
|
| 564 |
+
modeText.innerText = 'Awaiting connection';
|
| 565 |
+
}
|
| 566 |
+
else {
|
| 567 |
+
modeText.innerText = 'Internal control';
|
| 568 |
+
}
|
| 569 |
+
}
|
| 570 |
+
}
|
| 571 |
+
// Update teleop command display - only show non-zero values
|
| 572 |
+
const armTeleop = data.teleop_action;
|
| 573 |
+
const armTeleopDisplayEl = document.getElementById('arm_teleop_display');
|
| 574 |
+
if (armTeleop && armTeleopDisplayEl) {
|
| 575 |
+
const parts = [];
|
| 576 |
+
// Check all possible teleop fields
|
| 577 |
+
const fields = {
|
| 578 |
+
'vx': armTeleop.vx,
|
| 579 |
+
'vy': armTeleop.vy,
|
| 580 |
+
'vz': armTeleop.vz,
|
| 581 |
+
'vyaw': armTeleop.vyaw,
|
| 582 |
+
'vrx': armTeleop.vrx,
|
| 583 |
+
'vry': armTeleop.vry,
|
| 584 |
+
'vrz': armTeleop.vrz,
|
| 585 |
+
'j1': armTeleop.j1,
|
| 586 |
+
'j2': armTeleop.j2,
|
| 587 |
+
'j3': armTeleop.j3,
|
| 588 |
+
'j4': armTeleop.j4,
|
| 589 |
+
'j5': armTeleop.j5,
|
| 590 |
+
'j6': armTeleop.j6,
|
| 591 |
+
'g': armTeleop.gripper
|
| 592 |
+
};
|
| 593 |
+
for (const [key, value] of Object.entries(fields)) {
|
| 594 |
+
const numVal = Number(value ?? 0);
|
| 595 |
+
if (numVal !== 0) {
|
| 596 |
+
if (key === 'g') {
|
| 597 |
+
parts.push(`${key}=${numVal.toFixed(0)}`);
|
| 598 |
+
}
|
| 599 |
+
else {
|
| 600 |
+
parts.push(`${key}=${numVal.toFixed(2)}`);
|
| 601 |
+
}
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
armTeleopDisplayEl.innerText = parts.length > 0 ? parts.join(', ') : '-';
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
else {
|
| 608 |
+
// Locomotion state
|
| 609 |
+
const locoObs = data.observation || {};
|
| 610 |
+
// Update position display
|
| 611 |
+
const locoPos = document.getElementById('loco_pos');
|
| 612 |
+
if (locoObs.position) {
|
| 613 |
+
const p = locoObs.position;
|
| 614 |
+
locoPos.innerText = `${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)}`;
|
| 615 |
+
}
|
| 616 |
+
// Update orientation display (convert quaternion to euler)
|
| 617 |
+
const locoOri = document.getElementById('loco_ori');
|
| 618 |
+
if (locoObs.orientation) {
|
| 619 |
+
const q = locoObs.orientation;
|
| 620 |
+
const euler = quatToEuler(q.w, q.x, q.y, q.z);
|
| 621 |
+
locoOri.innerText = `${euler[0].toFixed(2)}, ${euler[1].toFixed(2)}, ${euler[2].toFixed(2)}`;
|
| 622 |
+
}
|
| 623 |
+
// Update teleop command display - only show non-zero values
|
| 624 |
+
const locoTeleop = data.teleop_action || {};
|
| 625 |
+
const locoTeleopDisplayEl = document.getElementById('loco_teleop_display');
|
| 626 |
+
if (locoTeleopDisplayEl) {
|
| 627 |
+
const parts = [];
|
| 628 |
+
const fields = {
|
| 629 |
+
'vx': locoTeleop.vx,
|
| 630 |
+
'vy': locoTeleop.vy,
|
| 631 |
+
'vyaw': locoTeleop.vyaw
|
| 632 |
+
};
|
| 633 |
+
for (const [key, value] of Object.entries(fields)) {
|
| 634 |
+
const numVal = Number(value ?? 0);
|
| 635 |
+
if (numVal !== 0) {
|
| 636 |
+
parts.push(`${key}=${numVal.toFixed(2)}`);
|
| 637 |
+
}
|
| 638 |
+
}
|
| 639 |
+
locoTeleopDisplayEl.innerText = parts.length > 0 ? parts.join(', ') : '-';
|
| 640 |
+
}
|
| 641 |
+
stepVal.innerText = data.steps;
|
| 642 |
+
}
|
| 643 |
+
}
|
| 644 |
+
else if (msg.type === 'trainer_status') {
|
| 645 |
+
const payload = msg.data || {};
|
| 646 |
+
if (typeof payload.connected === 'boolean') {
|
| 647 |
+
updateTrainerStatus(payload.connected);
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
else if (msg.type === 'trainer_notification') {
|
| 651 |
+
showTrainerNotification(msg.data);
|
| 652 |
+
}
|
| 653 |
+
}
|
| 654 |
+
catch (e) {
|
| 655 |
+
console.error('Error parsing message:', e);
|
| 656 |
+
}
|
| 657 |
+
};
|
| 658 |
+
}
|
| 659 |
+
function send(type, data = {}) {
|
| 660 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 661 |
+
const msg = { type, data };
|
| 662 |
+
console.log('Sending WebSocket message:', msg);
|
| 663 |
+
ws.send(JSON.stringify(msg));
|
| 664 |
+
}
|
| 665 |
+
else {
|
| 666 |
+
console.warn('Cannot send message, WebSocket not ready:', {
|
| 667 |
+
type,
|
| 668 |
+
hasWs: !!ws,
|
| 669 |
+
wsState: ws ? ws.readyState : 'no ws'
|
| 670 |
+
});
|
| 671 |
+
}
|
| 672 |
+
}
|
| 673 |
+
function requestEpisodeControl(action) {
|
| 674 |
+
if (!action) {
|
| 675 |
+
return;
|
| 676 |
+
}
|
| 677 |
+
send('episode_control', { action });
|
| 678 |
+
if (teleopStatus) {
|
| 679 |
+
teleopStatus.innerText = `UI requested episode ${action}`;
|
| 680 |
+
}
|
| 681 |
+
}
|
| 682 |
+
function showTrainerNotification(payload) {
|
| 683 |
+
if (!notificationList || !payload) {
|
| 684 |
+
return;
|
| 685 |
+
}
|
| 686 |
+
const entry = document.createElement('div');
|
| 687 |
+
entry.className = 'notification';
|
| 688 |
+
entry.style.opacity = '0.9';
|
| 689 |
+
const when = payload.timestamp ? new Date(payload.timestamp * 1000).toLocaleTimeString() : new Date().toLocaleTimeString();
|
| 690 |
+
const status = payload.status ? payload.status.toUpperCase() : 'INFO';
|
| 691 |
+
const message = payload.message || payload.text || payload.detail || 'Notification';
|
| 692 |
+
entry.textContent = `[${when}] ${status}: ${message}`;
|
| 693 |
+
notificationList.insertBefore(entry, notificationList.firstChild);
|
| 694 |
+
setTimeout(() => {
|
| 695 |
+
if (entry.parentNode) {
|
| 696 |
+
entry.parentNode.removeChild(entry);
|
| 697 |
+
}
|
| 698 |
+
}, NOTIFICATION_DURATION_MS);
|
| 699 |
+
while (notificationList.childElementCount > 5) {
|
| 700 |
+
notificationList.removeChild(notificationList.lastChild);
|
| 701 |
+
}
|
| 702 |
+
}
|
| 703 |
+
// Convert quaternion to euler angles (XYZ convention)
|
| 704 |
+
function quatToEuler(w, x, y, z) {
|
| 705 |
+
// Roll (x-axis rotation)
|
| 706 |
+
const sinr_cosp = 2 * (w * x + y * z);
|
| 707 |
+
const cosr_cosp = 1 - 2 * (x * x + y * y);
|
| 708 |
+
const roll = Math.atan2(sinr_cosp, cosr_cosp);
|
| 709 |
+
// Pitch (y-axis rotation)
|
| 710 |
+
const sinp = 2 * (w * y - z * x);
|
| 711 |
+
let pitch;
|
| 712 |
+
if (Math.abs(sinp) >= 1) {
|
| 713 |
+
pitch = Math.sign(sinp) * Math.PI / 2;
|
| 714 |
+
}
|
| 715 |
+
else {
|
| 716 |
+
pitch = Math.asin(sinp);
|
| 717 |
+
}
|
| 718 |
+
// Yaw (z-axis rotation)
|
| 719 |
+
const siny_cosp = 2 * (w * z + x * y);
|
| 720 |
+
const cosy_cosp = 1 - 2 * (y * y + z * z);
|
| 721 |
+
const yaw = Math.atan2(siny_cosp, cosy_cosp);
|
| 722 |
+
return [roll, pitch, yaw];
|
| 723 |
+
}
|
| 724 |
+
function updateHintBox(robotType) {
|
| 725 |
+
const hintBox = document.getElementById('hint_box');
|
| 726 |
+
if (!hintBox)
|
| 727 |
+
return;
|
| 728 |
+
if (robotType === 'arm') {
|
| 729 |
+
hintBox.innerHTML = `
|
| 730 |
+
Drag: Rotate Camera<br>
|
| 731 |
+
Scroll: Zoom<br>
|
| 732 |
+
<strong>Keyboard:</strong><br>
|
| 733 |
+
W/A/S/D: XY jog<br>
|
| 734 |
+
R/F: Z nudge<br>
|
| 735 |
+
Enter: Move to Home
|
| 736 |
+
`;
|
| 737 |
+
}
|
| 738 |
+
else {
|
| 739 |
+
hintBox.innerHTML = `
|
| 740 |
+
Drag: Rotate Camera<br>
|
| 741 |
+
Scroll: Zoom<br>
|
| 742 |
+
<strong>Keyboard:</strong><br>
|
| 743 |
+
W/S: Forward/Back<br>
|
| 744 |
+
A/D: Turn<br>
|
| 745 |
+
Q/E: Strafe
|
| 746 |
+
`;
|
| 747 |
+
}
|
| 748 |
+
}
|
| 749 |
+
function updateRobotUI(robot, scene = null) {
|
| 750 |
+
currentRobot = robot;
|
| 751 |
+
currentScene = scene;
|
| 752 |
+
robotTitle.innerText = robotTitles[robot] || robot;
|
| 753 |
+
robotInfo.innerText = robotInfoText[robot] || '';
|
| 754 |
+
if (sceneLabel) {
|
| 755 |
+
sceneLabel.innerText = humanizeScene(scene);
|
| 756 |
+
}
|
| 757 |
+
// Toggle controls based on robot type
|
| 758 |
+
if (robot === 'ur5' || robot === 'ur5_t_push') {
|
| 759 |
+
locomotionControls.classList.add('hidden');
|
| 760 |
+
armControls.classList.add('active');
|
| 761 |
+
document.getElementById('locomotion_state').style.display = 'none';
|
| 762 |
+
document.getElementById('arm_state').style.display = 'block';
|
| 763 |
+
document.getElementById('arm_hints').style.display = 'block';
|
| 764 |
+
document.getElementById('loco_hints').style.display = 'none';
|
| 765 |
+
// Update hint box for ARM robots
|
| 766 |
+
updateHintBox('arm');
|
| 767 |
+
// Load env data and show home button only if home_pose is available
|
| 768 |
+
loadEnvData().then(() => {
|
| 769 |
+
setupOverlayTiles();
|
| 770 |
+
// Show home button only if home_pose was loaded
|
| 771 |
+
const homeBtn = document.getElementById('homeBtn');
|
| 772 |
+
if (homeBtn) {
|
| 773 |
+
if (currentHomePose && currentHomePose.length > 0) {
|
| 774 |
+
homeBtn.style.display = 'block';
|
| 775 |
+
}
|
| 776 |
+
else {
|
| 777 |
+
homeBtn.style.display = 'none';
|
| 778 |
+
console.warn('Home button hidden: no home_pose available');
|
| 779 |
+
}
|
| 780 |
+
}
|
| 781 |
+
});
|
| 782 |
+
}
|
| 783 |
+
else {
|
| 784 |
+
locomotionControls.classList.remove('hidden');
|
| 785 |
+
armControls.classList.remove('active');
|
| 786 |
+
document.getElementById('locomotion_state').style.display = 'block';
|
| 787 |
+
document.getElementById('arm_state').style.display = 'none';
|
| 788 |
+
document.getElementById('arm_hints').style.display = 'none';
|
| 789 |
+
document.getElementById('loco_hints').style.display = 'block';
|
| 790 |
+
// Hide home button for non-UR5 robots
|
| 791 |
+
const homeBtn = document.getElementById('homeBtn');
|
| 792 |
+
if (homeBtn)
|
| 793 |
+
homeBtn.style.display = 'none';
|
| 794 |
+
// Update hint box for locomotion robots
|
| 795 |
+
updateHintBox('locomotion');
|
| 796 |
+
}
|
| 797 |
+
maybeAutoEnableNova();
|
| 798 |
+
}
|
| 799 |
+
let panelCollapsed = false;
|
| 800 |
+
function togglePanel() {
|
| 801 |
+
panelCollapsed = !panelCollapsed;
|
| 802 |
+
const content = document.getElementById('panel_content');
|
| 803 |
+
const header = document.getElementById('panel_header');
|
| 804 |
+
const panel = document.getElementById('control_panel');
|
| 805 |
+
if (panelCollapsed) {
|
| 806 |
+
content.classList.add('collapsed');
|
| 807 |
+
panel.classList.add('collapsed');
|
| 808 |
+
header.classList.add('collapsed');
|
| 809 |
+
}
|
| 810 |
+
else {
|
| 811 |
+
content.classList.remove('collapsed');
|
| 812 |
+
panel.classList.remove('collapsed');
|
| 813 |
+
header.classList.remove('collapsed');
|
| 814 |
+
}
|
| 815 |
+
}
|
| 816 |
+
function switchRobot() {
|
| 817 |
+
const selectionValue = robotSelect.value;
|
| 818 |
+
if (!selectionValue) {
|
| 819 |
+
return;
|
| 820 |
+
}
|
| 821 |
+
const parsed = parseSelection(selectionValue);
|
| 822 |
+
pendingRobotSelection = {
|
| 823 |
+
value: selectionValue,
|
| 824 |
+
robot: parsed.robot,
|
| 825 |
+
scene: parsed.scene
|
| 826 |
+
};
|
| 827 |
+
send('switch_robot', { robot: parsed.robot, scene: parsed.scene });
|
| 828 |
+
updateRobotUI(parsed.robot, parsed.scene);
|
| 829 |
+
}
|
| 830 |
+
const viewport = document.getElementById('viewport');
|
| 831 |
+
const stepVal = document.getElementById('step_val');
|
| 832 |
+
const camDist = document.getElementById('cam_dist');
|
| 833 |
+
const camDistVal = document.getElementById('cam_dist_val');
|
| 834 |
+
const armTeleopVx = document.getElementById('arm_teleop_vx');
|
| 835 |
+
const armTeleopVy = document.getElementById('arm_teleop_vy');
|
| 836 |
+
const armTeleopVz = document.getElementById('arm_teleop_vz');
|
| 837 |
+
let keysPressed = new Set();
|
| 838 |
+
function resetEnv() {
|
| 839 |
+
send('reset');
|
| 840 |
+
}
|
| 841 |
+
// Homing functionality for UR5
|
| 842 |
+
let homingInterval = null;
|
| 843 |
+
let homingJoints = []; // Joints being homed
|
| 844 |
+
function startHoming() {
|
| 845 |
+
console.log('startHoming called:', {
|
| 846 |
+
currentRobot,
|
| 847 |
+
hasHomePose: !!currentHomePose,
|
| 848 |
+
novaStateStreaming,
|
| 849 |
+
wsState: ws ? ws.readyState : 'no ws'
|
| 850 |
+
});
|
| 851 |
+
if (currentRobot !== 'ur5' && currentRobot !== 'ur5_t_push') {
|
| 852 |
+
console.log('Not a UR5 robot, skipping homing');
|
| 853 |
+
return;
|
| 854 |
+
}
|
| 855 |
+
// Check if we have necessary data
|
| 856 |
+
if (!currentHomePose) {
|
| 857 |
+
console.warn('Cannot home: home_pose not available');
|
| 858 |
+
return;
|
| 859 |
+
}
|
| 860 |
+
if (novaStateStreaming) {
|
| 861 |
+
// Nova mode: use joint jogging to move to home
|
| 862 |
+
console.log('Starting Nova jogging homing...');
|
| 863 |
+
startNovaJoggingHome();
|
| 864 |
+
}
|
| 865 |
+
else {
|
| 866 |
+
// Local mode: send home messages continuously while button is pressed
|
| 867 |
+
console.log('Starting local homing...');
|
| 868 |
+
send('home');
|
| 869 |
+
// Send home message every 100ms while button is pressed
|
| 870 |
+
if (homingInterval)
|
| 871 |
+
clearInterval(homingInterval);
|
| 872 |
+
homingInterval = setInterval(() => {
|
| 873 |
+
send('home');
|
| 874 |
+
}, 100);
|
| 875 |
+
}
|
| 876 |
+
}
|
| 877 |
+
function stopHoming() {
|
| 878 |
+
if (novaStateStreaming) {
|
| 879 |
+
// Stop all jogging
|
| 880 |
+
for (const joint of homingJoints) {
|
| 881 |
+
send('stop_jog');
|
| 882 |
+
}
|
| 883 |
+
homingJoints = [];
|
| 884 |
+
if (homingInterval) {
|
| 885 |
+
clearInterval(homingInterval);
|
| 886 |
+
homingInterval = null;
|
| 887 |
+
}
|
| 888 |
+
}
|
| 889 |
+
else {
|
| 890 |
+
// Stop the home message interval
|
| 891 |
+
if (homingInterval) {
|
| 892 |
+
clearInterval(homingInterval);
|
| 893 |
+
homingInterval = null;
|
| 894 |
+
}
|
| 895 |
+
// Send stop_home message to backend
|
| 896 |
+
send('stop_home');
|
| 897 |
+
console.log('Sent stop_home message');
|
| 898 |
+
}
|
| 899 |
+
}
|
| 900 |
+
function startNovaJoggingHome() {
|
| 901 |
+
if (!currentHomePose || !currentJointPositions) {
|
| 902 |
+
console.warn('Cannot jog home: missing home_pose or joint_positions');
|
| 903 |
+
return;
|
| 904 |
+
}
|
| 905 |
+
const tolerance = 0.01; // 0.01 rad ~= 0.57 degrees
|
| 906 |
+
// Check each joint and start jogging for those not at home
|
| 907 |
+
homingJoints = [];
|
| 908 |
+
for (let i = 0; i < Math.min(currentHomePose.length, currentJointPositions.length); i++) {
|
| 909 |
+
const target = currentHomePose[i];
|
| 910 |
+
const current = currentJointPositions[i];
|
| 911 |
+
const error = target - current;
|
| 912 |
+
if (Math.abs(error) > tolerance) {
|
| 913 |
+
const joint = i + 1; // Nova uses 1-based indexing
|
| 914 |
+
const direction = error > 0 ? '+' : '-';
|
| 915 |
+
const velocity = 0.5; // rad/s
|
| 916 |
+
console.log(`Jogging joint ${joint} ${direction} (error: ${error.toFixed(3)} rad)`);
|
| 917 |
+
send('start_jog', {
|
| 918 |
+
jog_type: 'joint',
|
| 919 |
+
params: {
|
| 920 |
+
joint: joint,
|
| 921 |
+
direction: direction,
|
| 922 |
+
velocity: velocity
|
| 923 |
+
}
|
| 924 |
+
});
|
| 925 |
+
homingJoints.push(joint);
|
| 926 |
+
}
|
| 927 |
+
}
|
| 928 |
+
if (homingJoints.length === 0) {
|
| 929 |
+
console.log('Already at home position');
|
| 930 |
+
return;
|
| 931 |
+
}
|
| 932 |
+
// Monitor progress and stop when home is reached
|
| 933 |
+
if (homingInterval)
|
| 934 |
+
clearInterval(homingInterval);
|
| 935 |
+
homingInterval = setInterval(() => {
|
| 936 |
+
if (!currentHomePose || !currentJointPositions) {
|
| 937 |
+
stopHoming();
|
| 938 |
+
return;
|
| 939 |
+
}
|
| 940 |
+
// Check if we're at home
|
| 941 |
+
let allAtHome = true;
|
| 942 |
+
for (let i = 0; i < Math.min(currentHomePose.length, currentJointPositions.length); i++) {
|
| 943 |
+
const target = currentHomePose[i];
|
| 944 |
+
const current = currentJointPositions[i];
|
| 945 |
+
const error = Math.abs(target - current);
|
| 946 |
+
if (error > tolerance) {
|
| 947 |
+
allAtHome = false;
|
| 948 |
+
break;
|
| 949 |
+
}
|
| 950 |
+
}
|
| 951 |
+
if (allAtHome) {
|
| 952 |
+
console.log('Home position reached');
|
| 953 |
+
stopHoming();
|
| 954 |
+
}
|
| 955 |
+
}, 100); // Check every 100ms
|
| 956 |
+
}
|
| 957 |
+
function setCmd(vx, vy, vyaw) {
|
| 958 |
+
send('command', { vx, vy, vyaw });
|
| 959 |
+
}
|
| 960 |
+
function setCameraFollow() {
|
| 961 |
+
const follow = document.getElementById('cam_follow').checked;
|
| 962 |
+
send('camera_follow', { follow });
|
| 963 |
+
}
|
| 964 |
+
// UR5 controls
|
| 965 |
+
let currentControlMode = 'ik';
|
| 966 |
+
function setControlMode(mode) {
|
| 967 |
+
currentControlMode = mode;
|
| 968 |
+
send('envData.control_mode', { mode });
|
| 969 |
+
// Update UI
|
| 970 |
+
document.getElementById('mode_ik').classList.toggle('active', mode === 'ik');
|
| 971 |
+
document.getElementById('mode_joint').classList.toggle('active', mode === 'joint');
|
| 972 |
+
document.getElementById('ik_controls').classList.toggle('active', mode === 'ik');
|
| 973 |
+
document.getElementById('joint_controls').classList.toggle('active', mode === 'joint');
|
| 974 |
+
}
|
| 975 |
+
// Jogging velocities
|
| 976 |
+
let transVelocity = 50.0; // mm/s
|
| 977 |
+
let rotVelocity = 0.3; // rad/s
|
| 978 |
+
let jointVelocity = 0.5; // rad/s
|
| 979 |
+
function updateTransVelocity() {
|
| 980 |
+
transVelocity = parseFloat(document.getElementById('trans_velocity').value);
|
| 981 |
+
document.getElementById('trans_vel_val').innerText = transVelocity.toFixed(0);
|
| 982 |
+
teleopTranslationStep = transVelocity / 1000;
|
| 983 |
+
}
|
| 984 |
+
function updateRotVelocity() {
|
| 985 |
+
rotVelocity = parseFloat(document.getElementById('rot_velocity').value);
|
| 986 |
+
document.getElementById('rot_vel_val').innerText = rotVelocity.toFixed(1);
|
| 987 |
+
}
|
| 988 |
+
function updateJointVelocity() {
|
| 989 |
+
jointVelocity = parseFloat(document.getElementById('joint_velocity').value);
|
| 990 |
+
document.getElementById('joint_vel_val').innerText = jointVelocity.toFixed(1);
|
| 991 |
+
}
|
| 992 |
+
function startJog(jogType, axisOrJoint, direction) {
|
| 993 |
+
if (jogType === 'cartesian_translation') {
|
| 994 |
+
send('start_jog', {
|
| 995 |
+
jog_type: 'cartesian_translation',
|
| 996 |
+
params: {
|
| 997 |
+
axis: axisOrJoint,
|
| 998 |
+
direction: direction,
|
| 999 |
+
velocity: transVelocity,
|
| 1000 |
+
tcp_id: 'Flange',
|
| 1001 |
+
coord_system_id: 'world'
|
| 1002 |
+
}
|
| 1003 |
+
});
|
| 1004 |
+
}
|
| 1005 |
+
else if (jogType === 'cartesian_rotation') {
|
| 1006 |
+
send('start_jog', {
|
| 1007 |
+
jog_type: 'cartesian_rotation',
|
| 1008 |
+
params: {
|
| 1009 |
+
axis: axisOrJoint,
|
| 1010 |
+
direction: direction,
|
| 1011 |
+
velocity: rotVelocity,
|
| 1012 |
+
tcp_id: 'Flange',
|
| 1013 |
+
coord_system_id: 'world'
|
| 1014 |
+
}
|
| 1015 |
+
});
|
| 1016 |
+
}
|
| 1017 |
+
else if (jogType === 'joint') {
|
| 1018 |
+
// Nova jogger uses 1-based joint indexing
|
| 1019 |
+
send('start_jog', {
|
| 1020 |
+
jog_type: 'joint',
|
| 1021 |
+
params: {
|
| 1022 |
+
joint: axisOrJoint, // 1-6
|
| 1023 |
+
direction: direction,
|
| 1024 |
+
velocity: jointVelocity
|
| 1025 |
+
}
|
| 1026 |
+
});
|
| 1027 |
+
}
|
| 1028 |
+
}
|
| 1029 |
+
function stopJog() {
|
| 1030 |
+
send('stop_jog', {});
|
| 1031 |
+
}
|
| 1032 |
+
function setGripper(action) {
|
| 1033 |
+
send('gripper', { action });
|
| 1034 |
+
}
|
| 1035 |
+
function updateCmdFromKeys() {
|
| 1036 |
+
let vx = 0, vy = 0, vyaw = 0;
|
| 1037 |
+
if (keysPressed.has('KeyW') || keysPressed.has('ArrowUp'))
|
| 1038 |
+
vx = 0.8;
|
| 1039 |
+
if (keysPressed.has('KeyS') || keysPressed.has('ArrowDown'))
|
| 1040 |
+
vx = -0.5;
|
| 1041 |
+
if (keysPressed.has('KeyA'))
|
| 1042 |
+
vyaw = 1.2;
|
| 1043 |
+
if (keysPressed.has('KeyD'))
|
| 1044 |
+
vyaw = -1.2;
|
| 1045 |
+
if (keysPressed.has('KeyQ'))
|
| 1046 |
+
vy = 0.4;
|
| 1047 |
+
if (keysPressed.has('KeyE'))
|
| 1048 |
+
vy = -0.4;
|
| 1049 |
+
if (keysPressed.has('ArrowLeft'))
|
| 1050 |
+
vyaw = 1.2;
|
| 1051 |
+
if (keysPressed.has('ArrowRight'))
|
| 1052 |
+
vyaw = -1.2;
|
| 1053 |
+
setCmd(vx, vy, vyaw);
|
| 1054 |
+
}
|
| 1055 |
+
function getActiveSelection() {
|
| 1056 |
+
return parseSelection(robotSelect.value);
|
| 1057 |
+
}
|
| 1058 |
+
function isArmRobot() {
|
| 1059 |
+
const active = getActiveSelection();
|
| 1060 |
+
return armRobotTypes.has(active.robot);
|
| 1061 |
+
}
|
| 1062 |
+
function shouldCaptureKey(code) {
|
| 1063 |
+
const active = getActiveSelection();
|
| 1064 |
+
return armRobotTypes.has(active.robot) ? armTeleopKeys.has(code) : locomotionKeys.has(code);
|
| 1065 |
+
}
|
| 1066 |
+
function hasActiveTeleopKeys() {
|
| 1067 |
+
const keySet = isArmRobot() ? armTeleopKeys : locomotionKeys;
|
| 1068 |
+
for (const key of keySet) {
|
| 1069 |
+
if (keysPressed.has(key)) {
|
| 1070 |
+
return true;
|
| 1071 |
+
}
|
| 1072 |
+
}
|
| 1073 |
+
return false;
|
| 1074 |
+
}
|
| 1075 |
+
function startTeleopRepeat() {
|
| 1076 |
+
if (teleopRepeatTimer) {
|
| 1077 |
+
return;
|
| 1078 |
+
}
|
| 1079 |
+
teleopRepeatTimer = setInterval(() => updateArmTeleopFromKeys(true), TELEOP_REPEAT_INTERVAL_MS);
|
| 1080 |
+
}
|
| 1081 |
+
function stopTeleopRepeat() {
|
| 1082 |
+
if (!teleopRepeatTimer) {
|
| 1083 |
+
return;
|
| 1084 |
+
}
|
| 1085 |
+
clearInterval(teleopRepeatTimer);
|
| 1086 |
+
teleopRepeatTimer = null;
|
| 1087 |
+
}
|
| 1088 |
+
function updateArmTeleopFromKeys(force = false) {
|
| 1089 |
+
let dx = 0, dy = 0, dz = 0;
|
| 1090 |
+
if (keysPressed.has('KeyW'))
|
| 1091 |
+
dx += teleopTranslationStep;
|
| 1092 |
+
if (keysPressed.has('KeyS'))
|
| 1093 |
+
dx -= teleopTranslationStep;
|
| 1094 |
+
if (keysPressed.has('KeyA'))
|
| 1095 |
+
dy += teleopTranslationStep;
|
| 1096 |
+
if (keysPressed.has('KeyD'))
|
| 1097 |
+
dy -= teleopTranslationStep;
|
| 1098 |
+
if (keysPressed.has('KeyR'))
|
| 1099 |
+
dz += teleopVerticalStep;
|
| 1100 |
+
if (keysPressed.has('KeyF'))
|
| 1101 |
+
dz -= teleopVerticalStep;
|
| 1102 |
+
const unchanged = Math.abs(dx - lastTeleopCommand.dx) < 1e-6 &&
|
| 1103 |
+
Math.abs(dy - lastTeleopCommand.dy) < 1e-6 &&
|
| 1104 |
+
Math.abs(dz - lastTeleopCommand.dz) < 1e-6;
|
| 1105 |
+
if (unchanged && !force) {
|
| 1106 |
+
return;
|
| 1107 |
+
}
|
| 1108 |
+
lastTeleopCommand = { dx, dy, dz };
|
| 1109 |
+
send('teleop_action', { dx, dy, dz });
|
| 1110 |
+
if (teleopStatus) {
|
| 1111 |
+
teleopStatus.innerText = `UI teleop → dx: ${dx.toFixed(3)} m, dy: ${dy.toFixed(3)} m, dz: ${dz.toFixed(3)} m`;
|
| 1112 |
+
}
|
| 1113 |
+
}
|
| 1114 |
+
function startArmJogForKey(code) {
|
| 1115 |
+
const entry = armJogKeyMap[code];
|
| 1116 |
+
if (!entry) {
|
| 1117 |
+
return;
|
| 1118 |
+
}
|
| 1119 |
+
startJog(entry.jogType, entry.axis, entry.direction);
|
| 1120 |
+
if (teleopStatus) {
|
| 1121 |
+
teleopStatus.innerText = `Nova jog → ${entry.axis.toUpperCase()} ${entry.direction}`;
|
| 1122 |
+
}
|
| 1123 |
+
}
|
| 1124 |
+
function handleArmKeyDown(code) {
|
| 1125 |
+
startArmJogForKey(code);
|
| 1126 |
+
}
|
| 1127 |
+
function handleArmKeyUp(code) {
|
| 1128 |
+
if (!armJogKeyMap[code]) {
|
| 1129 |
+
return;
|
| 1130 |
+
}
|
| 1131 |
+
stopJog();
|
| 1132 |
+
const remaining = [...keysPressed].filter((key) => armJogKeyMap[key]);
|
| 1133 |
+
if (remaining.length > 0) {
|
| 1134 |
+
startArmJogForKey(remaining[remaining.length - 1]);
|
| 1135 |
+
}
|
| 1136 |
+
else if (teleopStatus) {
|
| 1137 |
+
teleopStatus.innerText = 'UI teleop idle';
|
| 1138 |
+
}
|
| 1139 |
+
}
|
| 1140 |
+
function handleKeyStateChange() {
|
| 1141 |
+
if (isArmRobot()) {
|
| 1142 |
+
return;
|
| 1143 |
+
}
|
| 1144 |
+
const active = hasActiveTeleopKeys();
|
| 1145 |
+
if (active) {
|
| 1146 |
+
startTeleopRepeat();
|
| 1147 |
+
}
|
| 1148 |
+
else {
|
| 1149 |
+
stopTeleopRepeat();
|
| 1150 |
+
}
|
| 1151 |
+
updateCmdFromKeys();
|
| 1152 |
+
if (teleopStatus && !active) {
|
| 1153 |
+
teleopStatus.innerText = 'UI teleop idle';
|
| 1154 |
+
}
|
| 1155 |
+
}
|
| 1156 |
+
window.addEventListener('keydown', (e) => {
|
| 1157 |
+
if (e.code === 'Enter') {
|
| 1158 |
+
console.log('ENTER KEY PRESSED', { isArm: isArmRobot() });
|
| 1159 |
+
e.preventDefault();
|
| 1160 |
+
// For ARM robots, trigger homing; for locomotion robots, terminate episode
|
| 1161 |
+
if (isArmRobot()) {
|
| 1162 |
+
console.log('Calling startHoming from Enter key');
|
| 1163 |
+
startHoming();
|
| 1164 |
+
}
|
| 1165 |
+
else {
|
| 1166 |
+
requestEpisodeControl('terminate');
|
| 1167 |
+
}
|
| 1168 |
+
return;
|
| 1169 |
+
}
|
| 1170 |
+
if (shouldCaptureKey(e.code)) {
|
| 1171 |
+
e.preventDefault();
|
| 1172 |
+
const alreadyPressed = keysPressed.has(e.code);
|
| 1173 |
+
keysPressed.add(e.code);
|
| 1174 |
+
if (isArmRobot()) {
|
| 1175 |
+
if (!alreadyPressed) {
|
| 1176 |
+
handleArmKeyDown(e.code);
|
| 1177 |
+
}
|
| 1178 |
+
}
|
| 1179 |
+
else {
|
| 1180 |
+
handleKeyStateChange();
|
| 1181 |
+
}
|
| 1182 |
+
}
|
| 1183 |
+
});
|
| 1184 |
+
window.addEventListener('keyup', (e) => {
|
| 1185 |
+
if (e.code === 'Enter') {
|
| 1186 |
+
// For ARM robots, stop homing on Enter key release
|
| 1187 |
+
if (isArmRobot()) {
|
| 1188 |
+
stopHoming();
|
| 1189 |
+
}
|
| 1190 |
+
return;
|
| 1191 |
+
}
|
| 1192 |
+
if (keysPressed.delete(e.code)) {
|
| 1193 |
+
if (isArmRobot()) {
|
| 1194 |
+
handleArmKeyUp(e.code);
|
| 1195 |
+
}
|
| 1196 |
+
else {
|
| 1197 |
+
handleKeyStateChange();
|
| 1198 |
+
}
|
| 1199 |
+
}
|
| 1200 |
+
});
|
| 1201 |
+
camDist.oninput = () => {
|
| 1202 |
+
camDistVal.innerText = parseFloat(camDist.value).toFixed(1);
|
| 1203 |
+
send('camera', { action: 'set_distance', distance: parseFloat(camDist.value) });
|
| 1204 |
+
};
|
| 1205 |
+
let isDragging = false;
|
| 1206 |
+
let lastX, lastY;
|
| 1207 |
+
viewport.oncontextmenu = (e) => e.preventDefault();
|
| 1208 |
+
// Mouse controls
|
| 1209 |
+
viewport.onmousedown = (e) => {
|
| 1210 |
+
isDragging = true;
|
| 1211 |
+
lastX = e.clientX;
|
| 1212 |
+
lastY = e.clientY;
|
| 1213 |
+
};
|
| 1214 |
+
window.addEventListener('mouseup', () => {
|
| 1215 |
+
isDragging = false;
|
| 1216 |
+
});
|
| 1217 |
+
window.onmousemove = (e) => {
|
| 1218 |
+
if (isDragging) {
|
| 1219 |
+
const dx = e.clientX - lastX;
|
| 1220 |
+
const dy = e.clientY - lastY;
|
| 1221 |
+
lastX = e.clientX;
|
| 1222 |
+
lastY = e.clientY;
|
| 1223 |
+
send('camera', { action: 'rotate', dx, dy });
|
| 1224 |
+
}
|
| 1225 |
+
};
|
| 1226 |
+
viewport.onwheel = (e) => {
|
| 1227 |
+
e.preventDefault();
|
| 1228 |
+
send('camera', { action: 'zoom', dz: e.deltaY });
|
| 1229 |
+
};
|
| 1230 |
+
// Touch controls for camera rotation and pinch-to-zoom
|
| 1231 |
+
let touchStartX, touchStartY;
|
| 1232 |
+
let lastPinchDist = null;
|
| 1233 |
+
function getTouchDistance(touches) {
|
| 1234 |
+
const dx = touches[0].clientX - touches[1].clientX;
|
| 1235 |
+
const dy = touches[0].clientY - touches[1].clientY;
|
| 1236 |
+
return Math.sqrt(dx * dx + dy * dy);
|
| 1237 |
+
}
|
| 1238 |
+
viewport.addEventListener('touchstart', (e) => {
|
| 1239 |
+
if (e.touches.length === 1) {
|
| 1240 |
+
// Single touch - rotation
|
| 1241 |
+
touchStartX = e.touches[0].clientX;
|
| 1242 |
+
touchStartY = e.touches[0].clientY;
|
| 1243 |
+
lastPinchDist = null;
|
| 1244 |
+
}
|
| 1245 |
+
else if (e.touches.length === 2) {
|
| 1246 |
+
// Two touches - pinch zoom
|
| 1247 |
+
lastPinchDist = getTouchDistance(e.touches);
|
| 1248 |
+
}
|
| 1249 |
+
}, { passive: true });
|
| 1250 |
+
viewport.addEventListener('touchmove', (e) => {
|
| 1251 |
+
e.preventDefault();
|
| 1252 |
+
if (e.touches.length === 1 && lastPinchDist === null) {
|
| 1253 |
+
// Single touch drag - rotate camera
|
| 1254 |
+
const dx = e.touches[0].clientX - touchStartX;
|
| 1255 |
+
const dy = e.touches[0].clientY - touchStartY;
|
| 1256 |
+
touchStartX = e.touches[0].clientX;
|
| 1257 |
+
touchStartY = e.touches[0].clientY;
|
| 1258 |
+
send('camera', { action: 'rotate', dx, dy });
|
| 1259 |
+
}
|
| 1260 |
+
else if (e.touches.length === 2 && lastPinchDist !== null) {
|
| 1261 |
+
// Pinch zoom
|
| 1262 |
+
const dist = getTouchDistance(e.touches);
|
| 1263 |
+
const delta = lastPinchDist - dist;
|
| 1264 |
+
lastPinchDist = dist;
|
| 1265 |
+
// Scale delta for smoother zoom
|
| 1266 |
+
send('camera', { action: 'zoom', dz: delta * 3 });
|
| 1267 |
+
}
|
| 1268 |
+
}, { passive: false });
|
| 1269 |
+
viewport.addEventListener('touchend', (e) => {
|
| 1270 |
+
if (e.touches.length < 2) {
|
| 1271 |
+
lastPinchDist = null;
|
| 1272 |
+
}
|
| 1273 |
+
if (e.touches.length === 1) {
|
| 1274 |
+
// Reset for single finger after pinch
|
| 1275 |
+
touchStartX = e.touches[0].clientX;
|
| 1276 |
+
touchStartY = e.touches[0].clientY;
|
| 1277 |
+
}
|
| 1278 |
+
}, { passive: true });
|
| 1279 |
+
// Connect on load
|
| 1280 |
+
enterConnectingState();
|
| 1281 |
+
connect();
|
| 1282 |
+
export {};
|
frontend/src/main.ts
ADDED
|
@@ -0,0 +1,1533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NovaSimClient } from './api/client';
|
| 2 |
+
import type { StateData, RobotType } from './types/protocol';
|
| 3 |
+
import { isUR5Observation, isArmRobot } from './types/protocol';
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
// Type augmentation for window object
|
| 7 |
+
declare global {
|
| 8 |
+
interface Window {
|
| 9 |
+
debugJointLogCount?: number;
|
| 10 |
+
debugLogCount?: number;
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
const API_PREFIX = '/nova-sim/api/v1';
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
const WS_URL = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
|
| 19 |
+
window.location.host + API_PREFIX + '/ws';
|
| 20 |
+
|
| 21 |
+
let ws = null;
|
| 22 |
+
let reconnectTimer = null;
|
| 23 |
+
const connStatus = document.getElementById('conn_status');
|
| 24 |
+
const connStatusText = document.getElementById('conn_status_text');
|
| 25 |
+
const robotSelect = document.getElementById('robot_select') as HTMLSelectElement;
|
| 26 |
+
const sceneLabel = document.getElementById('scene_label');
|
| 27 |
+
const overlayTiles = document.getElementById('overlay_tiles');
|
| 28 |
+
const trainerStatusCard = document.getElementById('trainer_status_card');
|
| 29 |
+
const trainerStatusIndicator = document.getElementById('trainer_status_indicator');
|
| 30 |
+
const trainerStatusText = document.getElementById('trainer_status_text');
|
| 31 |
+
const viewportImage = document.querySelector('.video-container img');
|
| 32 |
+
const robotTitle = document.getElementById('robot_title');
|
| 33 |
+
const robotInfo = document.getElementById('robot_info');
|
| 34 |
+
const metadataUrl = API_PREFIX + '/metadata';
|
| 35 |
+
const envUrl = API_PREFIX + '/env';
|
| 36 |
+
let metadataCache = null;
|
| 37 |
+
let envCache = null;
|
| 38 |
+
let pendingRobotSelection = null;
|
| 39 |
+
let currentRobot = null;
|
| 40 |
+
let currentScene = null;
|
| 41 |
+
let novaStateStreaming = false; // Track if Nova state streaming is active
|
| 42 |
+
let currentHomePose = null; // Store home pose from env data
|
| 43 |
+
let currentJointPositions = null; // Store latest joint positions from state stream
|
| 44 |
+
function updateConnectionLabel(text) {
|
| 45 |
+
if (connStatusText) {
|
| 46 |
+
connStatusText.innerText = text;
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function enterConnectingState() {
|
| 51 |
+
updateConnectionLabel('Connecting...');
|
| 52 |
+
if (connStatus) {
|
| 53 |
+
connStatus.classList.add('connecting');
|
| 54 |
+
connStatus.classList.remove('disconnected');
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function markConnectedState() {
|
| 59 |
+
updateConnectionLabel('Connected');
|
| 60 |
+
if (connStatus) {
|
| 61 |
+
connStatus.classList.remove('connecting', 'disconnected');
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function refreshVideoStreams() {
|
| 66 |
+
const timestamp = Date.now();
|
| 67 |
+
if (viewportImage) {
|
| 68 |
+
viewportImage.src = `${API_PREFIX}/video_feed?ts=${timestamp}`;
|
| 69 |
+
}
|
| 70 |
+
if (overlayTiles) {
|
| 71 |
+
overlayTiles.querySelectorAll('img[data-feed]').forEach((img) => {
|
| 72 |
+
const feedName = img.dataset.feed || 'main';
|
| 73 |
+
img.src = `${API_PREFIX}/camera/${feedName}/video_feed?ts=${timestamp}`;
|
| 74 |
+
});
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
const novaToggleButton = document.getElementById('nova_toggle_button');
|
| 78 |
+
let novaEnabledState = false;
|
| 79 |
+
let novaManualToggle = false;
|
| 80 |
+
let novaAutoEnableRequested = false;
|
| 81 |
+
let novaPreconfigured = false;
|
| 82 |
+
function setNovaToggleState(available, enabled) {
|
| 83 |
+
if (!novaToggleButton) {
|
| 84 |
+
return;
|
| 85 |
+
}
|
| 86 |
+
novaEnabledState = !!enabled;
|
| 87 |
+
if (novaEnabledState) {
|
| 88 |
+
novaAutoEnableRequested = false;
|
| 89 |
+
}
|
| 90 |
+
if (!available) {
|
| 91 |
+
novaAutoEnableRequested = false;
|
| 92 |
+
}
|
| 93 |
+
novaToggleButton.style.display = available ? 'inline-flex' : 'none';
|
| 94 |
+
novaToggleButton.disabled = !available;
|
| 95 |
+
novaToggleButton.innerText = novaEnabledState ? 'Turn Nova Off' : 'Turn Nova On';
|
| 96 |
+
}
|
| 97 |
+
if (novaToggleButton) {
|
| 98 |
+
novaToggleButton.addEventListener('click', () => {
|
| 99 |
+
novaManualToggle = true;
|
| 100 |
+
send('set_nova_mode', {enabled: !novaEnabledState});
|
| 101 |
+
});
|
| 102 |
+
}
|
| 103 |
+
const NOVA_TRANSLATION_VELOCITY = 50.0;
|
| 104 |
+
const NOVA_ROTATION_VELOCITY = 0.3;
|
| 105 |
+
const NOVA_JOINT_VELOCITY = 0.5;
|
| 106 |
+
let novaVelocitiesConfigured = false;
|
| 107 |
+
|
| 108 |
+
function updateTrainerStatus(connected) {
|
| 109 |
+
if (!trainerStatusCard || !trainerStatusText) {
|
| 110 |
+
return;
|
| 111 |
+
}
|
| 112 |
+
trainerStatusText.innerText = connected ? 'Trainer: Connected' : 'Trainer: Disconnected';
|
| 113 |
+
trainerStatusCard.classList.toggle('connected', connected);
|
| 114 |
+
trainerStatusCard.classList.toggle('disconnected', !connected);
|
| 115 |
+
if (trainerStatusIndicator) {
|
| 116 |
+
trainerStatusIndicator.classList.toggle('connected', connected);
|
| 117 |
+
trainerStatusIndicator.classList.toggle('disconnected', !connected);
|
| 118 |
+
}
|
| 119 |
+
if (connected) {
|
| 120 |
+
configureNovaVelocities();
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
function configureNovaVelocities() {
|
| 125 |
+
if (novaVelocitiesConfigured) {
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
const transSlider = document.getElementById('trans_velocity');
|
| 129 |
+
const rotSlider = document.getElementById('rot_velocity');
|
| 130 |
+
const jointSlider = document.getElementById('joint_velocity');
|
| 131 |
+
|
| 132 |
+
if (transSlider) {
|
| 133 |
+
transSlider.value = NOVA_TRANSLATION_VELOCITY;
|
| 134 |
+
updateTransVelocity();
|
| 135 |
+
}
|
| 136 |
+
if (rotSlider) {
|
| 137 |
+
rotSlider.value = NOVA_ROTATION_VELOCITY;
|
| 138 |
+
updateRotVelocity();
|
| 139 |
+
}
|
| 140 |
+
if (jointSlider) {
|
| 141 |
+
jointSlider.value = NOVA_JOINT_VELOCITY;
|
| 142 |
+
updateJointVelocity();
|
| 143 |
+
}
|
| 144 |
+
novaVelocitiesConfigured = true;
|
| 145 |
+
}
|
| 146 |
+
function refreshOverlayTiles() {
|
| 147 |
+
if (!metadataCache) {
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
setupOverlayTiles();
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
const robotInfoText = {
|
| 154 |
+
'g1': '29 DOF humanoid with RL walking policy',
|
| 155 |
+
'spot': '12 DOF quadruped with trot gait controller',
|
| 156 |
+
'ur5': '6 DOF robot arm with Robotiq gripper',
|
| 157 |
+
'ur5_t_push': 'UR5e T-push task with stick tool'
|
| 158 |
+
};
|
| 159 |
+
|
| 160 |
+
const robotTitles = {
|
| 161 |
+
'g1': 'Unitree G1 Humanoid',
|
| 162 |
+
'spot': 'Boston Dynamics Spot',
|
| 163 |
+
'ur5': 'Universal Robots UR5e',
|
| 164 |
+
'ur5_t_push': 'UR5e T-Push Scene'
|
| 165 |
+
};
|
| 166 |
+
|
| 167 |
+
const locomotionControls = document.getElementById('locomotion_controls');
|
| 168 |
+
const armControls = document.getElementById('arm_controls');
|
| 169 |
+
const notificationList = document.getElementById('rl_notifications_list');
|
| 170 |
+
const armRobotTypes = new Set(['ur5', 'ur5_t_push']);
|
| 171 |
+
const armTeleopKeys = new Set(['KeyW', 'KeyA', 'KeyS', 'KeyD', 'KeyR', 'KeyF']);
|
| 172 |
+
const locomotionKeys = new Set([
|
| 173 |
+
'KeyW', 'KeyA', 'KeyS', 'KeyD', 'KeyQ', 'KeyE',
|
| 174 |
+
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'
|
| 175 |
+
]);
|
| 176 |
+
function maybeAutoEnableNova() {
|
| 177 |
+
if (!novaPreconfigured || novaManualToggle) {
|
| 178 |
+
return;
|
| 179 |
+
}
|
| 180 |
+
const activeSelection = getActiveSelection();
|
| 181 |
+
if (!armRobotTypes.has(activeSelection.robot)) {
|
| 182 |
+
return;
|
| 183 |
+
}
|
| 184 |
+
if (novaEnabledState) {
|
| 185 |
+
novaAutoEnableRequested = false;
|
| 186 |
+
return;
|
| 187 |
+
}
|
| 188 |
+
if (novaAutoEnableRequested) {
|
| 189 |
+
return;
|
| 190 |
+
}
|
| 191 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 192 |
+
send('set_nova_mode', {enabled: true});
|
| 193 |
+
novaAutoEnableRequested = true;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
let teleopTranslationStep = 0.005; // meters per keyboard nudge
|
| 197 |
+
let teleopVerticalStep = 0.01;
|
| 198 |
+
const TELEOP_REPEAT_INTERVAL_MS = 80;
|
| 199 |
+
const NOTIFICATION_DURATION_MS = 5000;
|
| 200 |
+
let teleopRepeatTimer = null;
|
| 201 |
+
let lastTeleopCommand = {dx: 0, dy: 0, dz: 0};
|
| 202 |
+
const armJogKeyMap = {
|
| 203 |
+
KeyW: { jogType: 'cartesian_translation', axis: 'x', direction: '+' },
|
| 204 |
+
KeyS: { jogType: 'cartesian_translation', axis: 'x', direction: '-' },
|
| 205 |
+
KeyA: { jogType: 'cartesian_translation', axis: 'y', direction: '+' },
|
| 206 |
+
KeyD: { jogType: 'cartesian_translation', axis: 'y', direction: '-' },
|
| 207 |
+
KeyR: { jogType: 'cartesian_translation', axis: 'z', direction: '+' },
|
| 208 |
+
KeyF: { jogType: 'cartesian_translation', axis: 'z', direction: '-' },
|
| 209 |
+
};
|
| 210 |
+
|
| 211 |
+
function humanizeScene(scene) {
|
| 212 |
+
if (!scene) {
|
| 213 |
+
return 'Default';
|
| 214 |
+
}
|
| 215 |
+
const cleaned = scene.replace(/^scene_/, '').replace(/_/g, ' ');
|
| 216 |
+
return cleaned.replace(/\\b\\w/g, (char) => char.toUpperCase());
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
function buildSelectionValue(robot, scene) {
|
| 220 |
+
return scene ? `${robot}::${scene}` : robot;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function parseSelection(value) {
|
| 224 |
+
if (!value) {
|
| 225 |
+
return {robot: 'g1', scene: null};
|
| 226 |
+
}
|
| 227 |
+
const [robotPart, scenePart] = value.split('::');
|
| 228 |
+
return {robot: robotPart, scene: scenePart || null};
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
function createRobotSceneOption(robot, scene, label) {
|
| 232 |
+
const option = document.createElement("option");
|
| 233 |
+
option.value = buildSelectionValue(robot, scene);
|
| 234 |
+
const sceneText = scene ? ` · ${humanizeScene(scene)}` : '';
|
| 235 |
+
option.textContent = `${label || robot}${sceneText}`;
|
| 236 |
+
return option;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
function getDefaultSelection(metadata) {
|
| 240 |
+
if (!metadata) {
|
| 241 |
+
return '';
|
| 242 |
+
}
|
| 243 |
+
const robots = Object.keys(metadata.robots);
|
| 244 |
+
if (!robots.length) {
|
| 245 |
+
return '';
|
| 246 |
+
}
|
| 247 |
+
const preferred = metadata.robots['ur5_t_push'] ? 'ur5_t_push' : robots[0];
|
| 248 |
+
const preferredRobot = metadata.robots[preferred];
|
| 249 |
+
const preferredScenes = preferredRobot && preferredRobot.scenes;
|
| 250 |
+
const defaultScene = (metadata.default_scene && metadata.default_scene[preferred])
|
| 251 |
+
|| (preferredScenes && preferredScenes[0]) || '';
|
| 252 |
+
return buildSelectionValue(preferred, defaultScene);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
function populateRobotOptions(metadata) {
|
| 256 |
+
if (!metadata || !metadata.robots) {
|
| 257 |
+
console.warn('populateRobotOptions: no metadata or robots', metadata);
|
| 258 |
+
return null;
|
| 259 |
+
}
|
| 260 |
+
if (!robotSelect) {
|
| 261 |
+
console.error('populateRobotOptions: robotSelect element not found');
|
| 262 |
+
return null;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
console.log('Populating robot options...', metadata);
|
| 266 |
+
robotSelect.innerHTML = "";
|
| 267 |
+
|
| 268 |
+
Object.entries(metadata.robots).forEach(([robot, meta]: [string, any]) => {
|
| 269 |
+
const scenes = meta.scenes || [];
|
| 270 |
+
if (scenes.length <= 1) {
|
| 271 |
+
const scene = scenes[0] || "";
|
| 272 |
+
const option = createRobotSceneOption(robot, scene, meta.label);
|
| 273 |
+
robotSelect.appendChild(option);
|
| 274 |
+
} else {
|
| 275 |
+
const group = document.createElement("optgroup");
|
| 276 |
+
group.label = meta.label || robot;
|
| 277 |
+
scenes.forEach((scene: string) => {
|
| 278 |
+
group.appendChild(createRobotSceneOption(robot, scene, meta.label));
|
| 279 |
+
});
|
| 280 |
+
robotSelect.appendChild(group);
|
| 281 |
+
}
|
| 282 |
+
});
|
| 283 |
+
|
| 284 |
+
const defaultValue = getDefaultSelection(metadata);
|
| 285 |
+
if (defaultValue) {
|
| 286 |
+
robotSelect.value = defaultValue;
|
| 287 |
+
const parsed = parseSelection(defaultValue);
|
| 288 |
+
console.log('Default selection:', parsed);
|
| 289 |
+
return parsed;
|
| 290 |
+
}
|
| 291 |
+
return null;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
async function setupOverlayTiles() {
|
| 295 |
+
if (!overlayTiles) {
|
| 296 |
+
return;
|
| 297 |
+
}
|
| 298 |
+
if (!envCache) {
|
| 299 |
+
overlayTiles.innerHTML = "";
|
| 300 |
+
overlayTiles.dataset.overlayKey = "";
|
| 301 |
+
overlayTiles.style.display = 'none';
|
| 302 |
+
return;
|
| 303 |
+
}
|
| 304 |
+
// Get overlay camera feeds from envCache (excludes main camera)
|
| 305 |
+
const allFeeds = envCache.camera_feeds || [];
|
| 306 |
+
const overlayFeeds = allFeeds.filter(feed => feed.name !== 'main');
|
| 307 |
+
|
| 308 |
+
if (!overlayFeeds.length) {
|
| 309 |
+
overlayTiles.innerHTML = "";
|
| 310 |
+
overlayTiles.dataset.overlayKey = "";
|
| 311 |
+
overlayTiles.style.display = 'none';
|
| 312 |
+
return;
|
| 313 |
+
}
|
| 314 |
+
const feedNames = overlayFeeds.map((feed) => feed.name || "aux");
|
| 315 |
+
const key = `${envCache.robot || ''}|${envCache.scene || ''}|${feedNames.join(',')}`;
|
| 316 |
+
if (overlayTiles.dataset.overlayKey === key) {
|
| 317 |
+
overlayTiles.style.display = 'flex';
|
| 318 |
+
return;
|
| 319 |
+
}
|
| 320 |
+
overlayTiles.dataset.overlayKey = key;
|
| 321 |
+
overlayTiles.innerHTML = "";
|
| 322 |
+
overlayTiles.style.display = 'flex';
|
| 323 |
+
overlayFeeds.forEach((feed) => {
|
| 324 |
+
const tile = document.createElement("div");
|
| 325 |
+
tile.className = "overlay-tile";
|
| 326 |
+
const img = document.createElement("img");
|
| 327 |
+
const feedName = feed.name || "aux";
|
| 328 |
+
img.dataset.feed = feedName;
|
| 329 |
+
img.src = feed.url + `?ts=${Date.now()}`;
|
| 330 |
+
tile.appendChild(img);
|
| 331 |
+
const label = document.createElement("div");
|
| 332 |
+
label.className = "overlay-label";
|
| 333 |
+
label.innerText = feed.label || feed.name;
|
| 334 |
+
tile.appendChild(label);
|
| 335 |
+
overlayTiles.appendChild(tile);
|
| 336 |
+
});
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
async function loadMetadata() {
|
| 340 |
+
try {
|
| 341 |
+
const resp = await fetch(metadataUrl);
|
| 342 |
+
if (!resp.ok) {
|
| 343 |
+
console.warn("Failed to load metadata");
|
| 344 |
+
return;
|
| 345 |
+
}
|
| 346 |
+
metadataCache = await resp.json();
|
| 347 |
+
const selection = populateRobotOptions(metadataCache);
|
| 348 |
+
if (selection) {
|
| 349 |
+
updateRobotUI(selection.robot, selection.scene);
|
| 350 |
+
refreshOverlayTiles();
|
| 351 |
+
}
|
| 352 |
+
const novaInfo = metadataCache && metadataCache.nova_api;
|
| 353 |
+
novaPreconfigured = Boolean(novaInfo && novaInfo.preconfigured);
|
| 354 |
+
setNovaToggleState(!!(novaInfo && novaInfo.preconfigured), !!(novaInfo && novaInfo.preconfigured));
|
| 355 |
+
maybeAutoEnableNova();
|
| 356 |
+
if (ws && ws.readyState === WebSocket.OPEN && pendingRobotSelection) {
|
| 357 |
+
send('switch_robot', {
|
| 358 |
+
robot: pendingRobotSelection.robot,
|
| 359 |
+
scene: pendingRobotSelection.scene
|
| 360 |
+
});
|
| 361 |
+
}
|
| 362 |
+
} catch (error) {
|
| 363 |
+
console.warn("Metadata fetch error:", error);
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
async function loadEnvData() {
|
| 368 |
+
try {
|
| 369 |
+
console.log('Loading env data from:', envUrl);
|
| 370 |
+
const resp = await fetch(envUrl);
|
| 371 |
+
if (!resp.ok) {
|
| 372 |
+
console.warn("Failed to load env data, status:", resp.status);
|
| 373 |
+
return;
|
| 374 |
+
}
|
| 375 |
+
envCache = await resp.json();
|
| 376 |
+
console.log('Env data loaded:', envCache);
|
| 377 |
+
// Store home_pose if available
|
| 378 |
+
if (envCache && envCache.home_pose) {
|
| 379 |
+
currentHomePose = envCache.home_pose;
|
| 380 |
+
console.log('Home pose set to:', currentHomePose);
|
| 381 |
+
} else {
|
| 382 |
+
console.warn('No home_pose in env data:', envCache);
|
| 383 |
+
}
|
| 384 |
+
} catch (error) {
|
| 385 |
+
console.error("Env fetch error:", error);
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
function connect() {
|
| 390 |
+
ws = new WebSocket(WS_URL);
|
| 391 |
+
|
| 392 |
+
loadMetadata();
|
| 393 |
+
loadEnvData();
|
| 394 |
+
|
| 395 |
+
ws.onopen = () => {
|
| 396 |
+
markConnectedState();
|
| 397 |
+
refreshVideoStreams();
|
| 398 |
+
refreshOverlayTiles();
|
| 399 |
+
novaAutoEnableRequested = false;
|
| 400 |
+
maybeAutoEnableNova();
|
| 401 |
+
if (reconnectTimer) {
|
| 402 |
+
clearInterval(reconnectTimer);
|
| 403 |
+
reconnectTimer = null;
|
| 404 |
+
}
|
| 405 |
+
if (pendingRobotSelection) {
|
| 406 |
+
send('switch_robot', {
|
| 407 |
+
robot: pendingRobotSelection.robot,
|
| 408 |
+
scene: pendingRobotSelection.scene
|
| 409 |
+
});
|
| 410 |
+
}
|
| 411 |
+
};
|
| 412 |
+
|
| 413 |
+
ws.onclose = () => {
|
| 414 |
+
enterConnectingState();
|
| 415 |
+
// Auto-reconnect
|
| 416 |
+
if (!reconnectTimer) {
|
| 417 |
+
reconnectTimer = setInterval(() => {
|
| 418 |
+
if (ws.readyState === WebSocket.CLOSED) {
|
| 419 |
+
connect();
|
| 420 |
+
}
|
| 421 |
+
}, 2000);
|
| 422 |
+
}
|
| 423 |
+
};
|
| 424 |
+
|
| 425 |
+
ws.onerror = (err) => {
|
| 426 |
+
console.error('WebSocket error:', err);
|
| 427 |
+
};
|
| 428 |
+
|
| 429 |
+
ws.onmessage = (event) => {
|
| 430 |
+
try {
|
| 431 |
+
const msg = JSON.parse(event.data);
|
| 432 |
+
if (msg.type === 'state') {
|
| 433 |
+
const data = msg.data;
|
| 434 |
+
|
| 435 |
+
// Check if robot or scene changed in state stream
|
| 436 |
+
if (data.robot && data.robot !== currentRobot) {
|
| 437 |
+
console.log(`Robot changed from ${currentRobot} to ${data.robot}`);
|
| 438 |
+
currentRobot = data.robot;
|
| 439 |
+
updateRobotUI(data.robot, data.scene);
|
| 440 |
+
// Update robot selector dropdown
|
| 441 |
+
if (robotSelect) {
|
| 442 |
+
robotSelect.value = buildSelectionValue(data.robot, data.scene);
|
| 443 |
+
}
|
| 444 |
+
// Fetch new environment info only when robot actually changes
|
| 445 |
+
fetch('/nova-sim/api/v1/env')
|
| 446 |
+
.then(r => r.json())
|
| 447 |
+
.then(envData => {
|
| 448 |
+
console.log('Robot changed, fetched env data:', envData);
|
| 449 |
+
envData.has_gripper = envData.envData.has_gripper || false;
|
| 450 |
+
envData.control_mode = envData.envData.control_mode || 'ik';
|
| 451 |
+
if (envData.home_pose) {
|
| 452 |
+
currentHomePose = envData.home_pose;
|
| 453 |
+
console.log('Updated home pose from robot change:', currentHomePose);
|
| 454 |
+
} else {
|
| 455 |
+
console.warn('No home_pose in robot change env data');
|
| 456 |
+
}
|
| 457 |
+
});
|
| 458 |
+
}
|
| 459 |
+
if (data.scene && data.scene !== currentScene) {
|
| 460 |
+
console.log(`Scene changed from ${currentScene} to ${data.scene}`);
|
| 461 |
+
currentScene = data.scene;
|
| 462 |
+
// Update robot selector dropdown to reflect scene change
|
| 463 |
+
if (robotSelect && currentRobot) {
|
| 464 |
+
robotSelect.value = buildSelectionValue(currentRobot, data.scene);
|
| 465 |
+
}
|
| 466 |
+
// Update UI to reflect scene change
|
| 467 |
+
updateRobotUI(currentRobot, data.scene);
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
if (typeof data.trainer_connected === 'boolean') {
|
| 471 |
+
updateTrainerStatus(data.trainer_connected);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
if (currentRobot === 'ur5' || currentRobot === 'ur5_t_push') {
|
| 475 |
+
// UR5 state - Access observation data first
|
| 476 |
+
const obs = data.observation || {};
|
| 477 |
+
|
| 478 |
+
// Update end effector position
|
| 479 |
+
if (obs.end_effector) {
|
| 480 |
+
const ee = obs.end_effector;
|
| 481 |
+
const eePosEl = document.getElementById('ee_pos');
|
| 482 |
+
if (eePosEl) {
|
| 483 |
+
eePosEl.innerText = ee.x.toFixed(2) + ', ' + ee.y.toFixed(2) + ', ' + ee.z.toFixed(2);
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
// EE Orientation - convert quaternion to euler for display
|
| 488 |
+
if (obs.ee_orientation) {
|
| 489 |
+
const q = obs.ee_orientation;
|
| 490 |
+
const euler = quatToEuler(q.w, q.x, q.y, q.z);
|
| 491 |
+
const eeOriEl = document.getElementById('ee_ori');
|
| 492 |
+
if (eeOriEl) {
|
| 493 |
+
eeOriEl.innerText = euler[0].toFixed(2) + ', ' + euler[1].toFixed(2) + ', ' + euler[2].toFixed(2);
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// Show/hide gripper UI based on envCache.has_gripper
|
| 498 |
+
const gripperStateDisplay = document.getElementById('gripper_state_display');
|
| 499 |
+
const gripperControls = document.getElementById('gripper_controls');
|
| 500 |
+
if (envCache && envCache.has_gripper) {
|
| 501 |
+
if (gripperStateDisplay) gripperStateDisplay.style.display = '';
|
| 502 |
+
if (gripperControls) gripperControls.style.display = '';
|
| 503 |
+
// Gripper: 0=open, 255=closed (Robotiq 2F-85)
|
| 504 |
+
if (obs.gripper !== undefined) {
|
| 505 |
+
const gripperVal = document.getElementById('gripper_val');
|
| 506 |
+
if (gripperVal) {
|
| 507 |
+
gripperVal.innerText = ((255 - obs.gripper) / 255 * 100).toFixed(0) + '% open';
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
} else {
|
| 511 |
+
if (gripperStateDisplay) gripperStateDisplay.style.display = 'none';
|
| 512 |
+
if (gripperControls) gripperControls.style.display = 'none';
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
const armStepEl = document.getElementById('arm_step_val');
|
| 516 |
+
if (armStepEl && data.steps !== undefined) {
|
| 517 |
+
armStepEl.innerText = String(data.steps);
|
| 518 |
+
}
|
| 519 |
+
const rewardEl = document.getElementById('arm_reward');
|
| 520 |
+
if (rewardEl) {
|
| 521 |
+
if (data.reward === null || data.reward === undefined) {
|
| 522 |
+
rewardEl.innerText = '-';
|
| 523 |
+
} else {
|
| 524 |
+
rewardEl.innerText = data.reward.toFixed(3);
|
| 525 |
+
}
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// Update joint position display (actual positions)
|
| 529 |
+
if (obs.joint_positions) {
|
| 530 |
+
const jp = obs.joint_positions;
|
| 531 |
+
currentJointPositions = jp; // Store for homing
|
| 532 |
+
|
| 533 |
+
// Update jog button displays with actual joint positions
|
| 534 |
+
for (let i = 0; i < 6; i++) {
|
| 535 |
+
const el = document.getElementById('joint_' + i + '_val');
|
| 536 |
+
if (el) {
|
| 537 |
+
el.innerText = jp[i].toFixed(2);
|
| 538 |
+
}
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
// Debug: log first joint update
|
| 542 |
+
if (!window.debugJointLogCount) window.debugJointLogCount = 0;
|
| 543 |
+
if (window.debugJointLogCount < 2) {
|
| 544 |
+
console.log('Joint positions update:', jp.map(j => j.toFixed(2)));
|
| 545 |
+
console.log('Joint 0 element found:', !!document.getElementById('joint_0_val'));
|
| 546 |
+
window.debugJointLogCount++;
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
// Update cartesian position displays with actual EE position
|
| 551 |
+
if (obs.end_effector) {
|
| 552 |
+
const ee = obs.end_effector;
|
| 553 |
+
const posXEl = document.getElementById('pos_x_val');
|
| 554 |
+
const posYEl = document.getElementById('pos_y_val');
|
| 555 |
+
const posZEl = document.getElementById('pos_z_val');
|
| 556 |
+
|
| 557 |
+
if (posXEl) posXEl.innerText = ee.x.toFixed(3);
|
| 558 |
+
if (posYEl) posYEl.innerText = ee.y.toFixed(3);
|
| 559 |
+
if (posZEl) posZEl.innerText = ee.z.toFixed(3);
|
| 560 |
+
|
| 561 |
+
// Debug: log first few updates
|
| 562 |
+
if (!window.debugLogCount) window.debugLogCount = 0;
|
| 563 |
+
if (window.debugLogCount < 3) {
|
| 564 |
+
console.log('EE Position update:', ee.x.toFixed(3), ee.y.toFixed(3), ee.z.toFixed(3));
|
| 565 |
+
console.log('Elements found:', !!posXEl, !!posYEl, !!posZEl);
|
| 566 |
+
window.debugLogCount++;
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
// Update rotation displays with actual EE orientation
|
| 571 |
+
if (obs.ee_orientation) {
|
| 572 |
+
const q = obs.ee_orientation;
|
| 573 |
+
const euler = quatToEuler(q.w, q.x, q.y, q.z);
|
| 574 |
+
const rotXEl = document.getElementById('rot_x_val');
|
| 575 |
+
const rotYEl = document.getElementById('rot_y_val');
|
| 576 |
+
const rotZEl = document.getElementById('rot_z_val');
|
| 577 |
+
if (rotXEl) rotXEl.innerText = euler[0].toFixed(2);
|
| 578 |
+
if (rotYEl) rotYEl.innerText = euler[1].toFixed(2);
|
| 579 |
+
if (rotZEl) rotZEl.innerText = euler[2].toFixed(2);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
// Update control mode display
|
| 583 |
+
if (data.control_mode) {
|
| 584 |
+
const controlModeDisplay = document.getElementById('control_mode_display');
|
| 585 |
+
if (controlModeDisplay) {
|
| 586 |
+
controlModeDisplay.innerText = data.control_mode === 'ik' ? 'IK' : 'Joint';
|
| 587 |
+
}
|
| 588 |
+
// Sync UI if mode changed externally (but not if we just changed it ourselves)
|
| 589 |
+
// Wait at least 500ms after our own change before syncing from server
|
| 590 |
+
const timeSinceOurChange = Date.now() - controlModeChangeTime;
|
| 591 |
+
if (currentControlMode !== data.control_mode && timeSinceOurChange > 500) {
|
| 592 |
+
setControlMode(data.control_mode);
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
// Update Nova API controller status
|
| 597 |
+
if (data.nova_api) {
|
| 598 |
+
const stateController = document.getElementById('nova_state_controller');
|
| 599 |
+
const ikController = document.getElementById('nova_ik_controller');
|
| 600 |
+
const badge = document.getElementById('nova_connection_badge');
|
| 601 |
+
const badgeTitle = document.getElementById('nova_badge_title');
|
| 602 |
+
const modeText = document.getElementById('nova_mode_text');
|
| 603 |
+
setNovaToggleState(data.nova_api.available, data.nova_api.enabled);
|
| 604 |
+
|
| 605 |
+
// Track Nova state streaming status
|
| 606 |
+
novaStateStreaming = data.nova_api.state_streaming || false;
|
| 607 |
+
|
| 608 |
+
if (badge) {
|
| 609 |
+
if (data.nova_api.available) {
|
| 610 |
+
badge.style.display = 'block';
|
| 611 |
+
} else {
|
| 612 |
+
badge.style.display = 'none';
|
| 613 |
+
}
|
| 614 |
+
badge.classList.toggle('connected', data.nova_api.enabled);
|
| 615 |
+
badge.classList.toggle('disconnected', !data.nova_api.enabled);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
const connectedColor = data.nova_api.connected ? 'var(--wb-success)' : 'var(--wb-highlight)';
|
| 619 |
+
if (stateController) {
|
| 620 |
+
stateController.innerText = data.nova_api.state_streaming ? 'Nova API' : 'Internal';
|
| 621 |
+
stateController.style.color = connectedColor;
|
| 622 |
+
}
|
| 623 |
+
if (ikController) {
|
| 624 |
+
ikController.innerText = data.nova_api.ik ? 'Nova API' : 'Internal';
|
| 625 |
+
ikController.style.color = data.nova_api.ik ? 'var(--wb-success)' : 'var(--wb-highlight)';
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
if (badgeTitle) {
|
| 629 |
+
if (data.nova_api.enabled) {
|
| 630 |
+
badgeTitle.innerText = data.nova_api.connected ? '🌐 Nova Connected' : '🌐 Nova Powering Up';
|
| 631 |
+
} else {
|
| 632 |
+
badgeTitle.innerText = '🌐 Nova Disabled';
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
if (modeText) {
|
| 637 |
+
if (data.nova_api.connected) {
|
| 638 |
+
if (data.nova_api.state_streaming && data.nova_api.ik) {
|
| 639 |
+
modeText.innerText = 'Hybrid Mode';
|
| 640 |
+
} else if (data.nova_api.state_streaming) {
|
| 641 |
+
modeText.innerText = 'Digital Twin Mode';
|
| 642 |
+
} else if (data.nova_api.ik) {
|
| 643 |
+
modeText.innerText = 'Nova IK Mode';
|
| 644 |
+
}
|
| 645 |
+
} else if (data.nova_api.enabled) {
|
| 646 |
+
modeText.innerText = 'Awaiting connection';
|
| 647 |
+
} else {
|
| 648 |
+
modeText.innerText = 'Internal control';
|
| 649 |
+
}
|
| 650 |
+
}
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
// Update teleop command display - only show non-zero values
|
| 654 |
+
const armTeleop = data.teleop_action;
|
| 655 |
+
const armTeleopDisplayEl = document.getElementById('arm_teleop_display');
|
| 656 |
+
if (armTeleop && armTeleopDisplayEl) {
|
| 657 |
+
const parts = [];
|
| 658 |
+
// Check all possible teleop fields
|
| 659 |
+
const fields = {
|
| 660 |
+
'vx': armTeleop.vx,
|
| 661 |
+
'vy': armTeleop.vy,
|
| 662 |
+
'vz': armTeleop.vz,
|
| 663 |
+
'vyaw': armTeleop.vyaw,
|
| 664 |
+
'vrx': armTeleop.vrx,
|
| 665 |
+
'vry': armTeleop.vry,
|
| 666 |
+
'vrz': armTeleop.vrz,
|
| 667 |
+
'j1': armTeleop.j1,
|
| 668 |
+
'j2': armTeleop.j2,
|
| 669 |
+
'j3': armTeleop.j3,
|
| 670 |
+
'j4': armTeleop.j4,
|
| 671 |
+
'j5': armTeleop.j5,
|
| 672 |
+
'j6': armTeleop.j6,
|
| 673 |
+
'g': armTeleop.gripper
|
| 674 |
+
};
|
| 675 |
+
|
| 676 |
+
for (const [key, value] of Object.entries(fields)) {
|
| 677 |
+
const numVal = Number(value ?? 0);
|
| 678 |
+
if (numVal !== 0) {
|
| 679 |
+
if (key === 'g') {
|
| 680 |
+
parts.push(`${key}=${numVal.toFixed(0)}`);
|
| 681 |
+
} else {
|
| 682 |
+
parts.push(`${key}=${numVal.toFixed(2)}`);
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
armTeleopDisplayEl.innerText = parts.length > 0 ? parts.join(', ') : '-';
|
| 688 |
+
}
|
| 689 |
+
} else {
|
| 690 |
+
// Locomotion state
|
| 691 |
+
const locoObs = data.observation || {};
|
| 692 |
+
|
| 693 |
+
// Update position display
|
| 694 |
+
const locoPos = document.getElementById('loco_pos');
|
| 695 |
+
if (locoPos && locoObs.position) {
|
| 696 |
+
const p = locoObs.position;
|
| 697 |
+
locoPos.innerText = `${p.x.toFixed(2)}, ${p.y.toFixed(2)}, ${p.z.toFixed(2)}`;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
// Update orientation display (convert quaternion to euler)
|
| 701 |
+
const locoOri = document.getElementById('loco_ori');
|
| 702 |
+
if (locoOri && locoObs.orientation) {
|
| 703 |
+
const q = locoObs.orientation;
|
| 704 |
+
const euler = quatToEuler(q.w, q.x, q.y, q.z);
|
| 705 |
+
locoOri.innerText = `${euler[0].toFixed(2)}, ${euler[1].toFixed(2)}, ${euler[2].toFixed(2)}`;
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
// Update teleop command display - only show non-zero values
|
| 709 |
+
const locoTeleop = data.teleop_action || {};
|
| 710 |
+
const locoTeleopDisplayEl = document.getElementById('loco_teleop_display');
|
| 711 |
+
if (locoTeleopDisplayEl) {
|
| 712 |
+
const parts = [];
|
| 713 |
+
const fields = {
|
| 714 |
+
'vx': locoTeleop.vx,
|
| 715 |
+
'vy': locoTeleop.vy,
|
| 716 |
+
'vyaw': locoTeleop.vyaw
|
| 717 |
+
};
|
| 718 |
+
|
| 719 |
+
for (const [key, value] of Object.entries(fields)) {
|
| 720 |
+
const numVal = Number(value ?? 0);
|
| 721 |
+
if (numVal !== 0) {
|
| 722 |
+
parts.push(`${key}=${numVal.toFixed(2)}`);
|
| 723 |
+
}
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
locoTeleopDisplayEl.innerText = parts.length > 0 ? parts.join(', ') : '-';
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
const stepVal = document.getElementById('step_val');
|
| 730 |
+
if (stepVal && data.steps !== undefined) {
|
| 731 |
+
stepVal.innerText = String(data.steps);
|
| 732 |
+
}
|
| 733 |
+
}
|
| 734 |
+
} else if (msg.type === 'trainer_status') {
|
| 735 |
+
const payload = msg.data || {};
|
| 736 |
+
if (typeof payload.connected === 'boolean') {
|
| 737 |
+
updateTrainerStatus(payload.connected);
|
| 738 |
+
}
|
| 739 |
+
} else if (msg.type === 'trainer_notification') {
|
| 740 |
+
showTrainerNotification(msg.data);
|
| 741 |
+
}
|
| 742 |
+
} catch (e) {
|
| 743 |
+
console.error('Error parsing message:', e);
|
| 744 |
+
}
|
| 745 |
+
};
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
function send(type, data = {}) {
|
| 749 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 750 |
+
const msg = {type, data};
|
| 751 |
+
ws.send(JSON.stringify(msg));
|
| 752 |
+
} else {
|
| 753 |
+
console.warn('Cannot send message, WebSocket not ready:', {
|
| 754 |
+
type,
|
| 755 |
+
hasWs: !!ws,
|
| 756 |
+
wsState: ws ? ws.readyState : 'no ws'
|
| 757 |
+
});
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
function requestEpisodeControl(action) {
|
| 762 |
+
if (!action) {
|
| 763 |
+
return;
|
| 764 |
+
}
|
| 765 |
+
send('episode_control', {action});
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
function showTrainerNotification(payload) {
|
| 769 |
+
if (!notificationList || !payload) {
|
| 770 |
+
return;
|
| 771 |
+
}
|
| 772 |
+
const entry = document.createElement('div');
|
| 773 |
+
entry.className = 'notification';
|
| 774 |
+
entry.style.opacity = '0.9';
|
| 775 |
+
const when = payload.timestamp ? new Date(payload.timestamp * 1000).toLocaleTimeString() : new Date().toLocaleTimeString();
|
| 776 |
+
const status = payload.status ? payload.status.toUpperCase() : 'INFO';
|
| 777 |
+
const message = payload.message || payload.text || payload.detail || 'Notification';
|
| 778 |
+
entry.textContent = `[${when}] ${status}: ${message}`;
|
| 779 |
+
notificationList.insertBefore(entry, notificationList.firstChild);
|
| 780 |
+
setTimeout(() => {
|
| 781 |
+
if (entry.parentNode) {
|
| 782 |
+
entry.parentNode.removeChild(entry);
|
| 783 |
+
}
|
| 784 |
+
}, NOTIFICATION_DURATION_MS);
|
| 785 |
+
while (notificationList.childElementCount > 5) {
|
| 786 |
+
notificationList.removeChild(notificationList.lastChild);
|
| 787 |
+
}
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
// Convert quaternion to euler angles (XYZ convention)
|
| 791 |
+
function quatToEuler(w, x, y, z) {
|
| 792 |
+
// Roll (x-axis rotation)
|
| 793 |
+
const sinr_cosp = 2 * (w * x + y * z);
|
| 794 |
+
const cosr_cosp = 1 - 2 * (x * x + y * y);
|
| 795 |
+
const roll = Math.atan2(sinr_cosp, cosr_cosp);
|
| 796 |
+
|
| 797 |
+
// Pitch (y-axis rotation)
|
| 798 |
+
const sinp = 2 * (w * y - z * x);
|
| 799 |
+
let pitch;
|
| 800 |
+
if (Math.abs(sinp) >= 1) {
|
| 801 |
+
pitch = Math.sign(sinp) * Math.PI / 2;
|
| 802 |
+
} else {
|
| 803 |
+
pitch = Math.asin(sinp);
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
// Yaw (z-axis rotation)
|
| 807 |
+
const siny_cosp = 2 * (w * z + x * y);
|
| 808 |
+
const cosy_cosp = 1 - 2 * (y * y + z * z);
|
| 809 |
+
const yaw = Math.atan2(siny_cosp, cosy_cosp);
|
| 810 |
+
|
| 811 |
+
return [roll, pitch, yaw];
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
function updateHintBox(robotType) {
|
| 815 |
+
const hintBox = document.getElementById('hint_box');
|
| 816 |
+
if (!hintBox) return;
|
| 817 |
+
|
| 818 |
+
if (robotType === 'arm') {
|
| 819 |
+
hintBox.innerHTML = `
|
| 820 |
+
Drag: Rotate Camera<br>
|
| 821 |
+
Scroll: Zoom<br>
|
| 822 |
+
<strong>Keyboard:</strong><br>
|
| 823 |
+
W/A/S/D: XY jog<br>
|
| 824 |
+
R/F: Z nudge<br>
|
| 825 |
+
Enter: Move to Home
|
| 826 |
+
`;
|
| 827 |
+
} else {
|
| 828 |
+
hintBox.innerHTML = `
|
| 829 |
+
Drag: Rotate Camera<br>
|
| 830 |
+
Scroll: Zoom<br>
|
| 831 |
+
<strong>Keyboard:</strong><br>
|
| 832 |
+
W/S: Forward/Back<br>
|
| 833 |
+
A/D: Turn<br>
|
| 834 |
+
Q/E: Strafe
|
| 835 |
+
`;
|
| 836 |
+
}
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
function updateRobotUI(robot, scene = null) {
|
| 840 |
+
currentRobot = robot;
|
| 841 |
+
currentScene = scene;
|
| 842 |
+
robotTitle.innerText = robotTitles[robot] || robot;
|
| 843 |
+
robotInfo.innerText = robotInfoText[robot] || '';
|
| 844 |
+
if (sceneLabel) {
|
| 845 |
+
sceneLabel.innerText = humanizeScene(scene);
|
| 846 |
+
}
|
| 847 |
+
// Toggle controls based on robot type
|
| 848 |
+
if (robot === 'ur5' || robot === 'ur5_t_push') {
|
| 849 |
+
locomotionControls.classList.add('hidden');
|
| 850 |
+
armControls.classList.add('active');
|
| 851 |
+
document.getElementById('locomotion_state').style.display = 'none';
|
| 852 |
+
document.getElementById('arm_state').style.display = 'block';
|
| 853 |
+
document.getElementById('arm_hints').style.display = 'block';
|
| 854 |
+
document.getElementById('loco_hints').style.display = 'none';
|
| 855 |
+
// Update hint box for ARM robots
|
| 856 |
+
updateHintBox('arm');
|
| 857 |
+
// Load env data and show home button only if home_pose is available
|
| 858 |
+
loadEnvData().then(() => {
|
| 859 |
+
setupOverlayTiles();
|
| 860 |
+
// Show home button only if home_pose was loaded
|
| 861 |
+
const homeBtn = document.getElementById('homeBtn');
|
| 862 |
+
if (homeBtn) {
|
| 863 |
+
if (currentHomePose && currentHomePose.length > 0) {
|
| 864 |
+
homeBtn.style.display = 'block';
|
| 865 |
+
} else {
|
| 866 |
+
homeBtn.style.display = 'none';
|
| 867 |
+
console.warn('Home button hidden: no home_pose available');
|
| 868 |
+
}
|
| 869 |
+
}
|
| 870 |
+
});
|
| 871 |
+
} else {
|
| 872 |
+
locomotionControls.classList.remove('hidden');
|
| 873 |
+
armControls.classList.remove('active');
|
| 874 |
+
document.getElementById('locomotion_state').style.display = 'block';
|
| 875 |
+
document.getElementById('arm_state').style.display = 'none';
|
| 876 |
+
document.getElementById('arm_hints').style.display = 'none';
|
| 877 |
+
document.getElementById('loco_hints').style.display = 'block';
|
| 878 |
+
// Hide home button for non-UR5 robots
|
| 879 |
+
const homeBtn = document.getElementById('homeBtn');
|
| 880 |
+
if (homeBtn) homeBtn.style.display = 'none';
|
| 881 |
+
// Update hint box for locomotion robots
|
| 882 |
+
updateHintBox('locomotion');
|
| 883 |
+
}
|
| 884 |
+
maybeAutoEnableNova();
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
let panelCollapsed = false;
|
| 888 |
+
function togglePanel() {
|
| 889 |
+
panelCollapsed = !panelCollapsed;
|
| 890 |
+
const content = document.getElementById('panel_content');
|
| 891 |
+
const header = document.getElementById('panel_header');
|
| 892 |
+
const panel = document.getElementById('control_panel');
|
| 893 |
+
if (panelCollapsed) {
|
| 894 |
+
content.classList.add('collapsed');
|
| 895 |
+
panel.classList.add('collapsed');
|
| 896 |
+
header.classList.add('collapsed');
|
| 897 |
+
} else {
|
| 898 |
+
content.classList.remove('collapsed');
|
| 899 |
+
panel.classList.remove('collapsed');
|
| 900 |
+
header.classList.remove('collapsed');
|
| 901 |
+
}
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
function switchRobot() {
|
| 905 |
+
const selectionValue = robotSelect.value;
|
| 906 |
+
if (!selectionValue) {
|
| 907 |
+
return;
|
| 908 |
+
}
|
| 909 |
+
const parsed = parseSelection(selectionValue);
|
| 910 |
+
pendingRobotSelection = {
|
| 911 |
+
value: selectionValue,
|
| 912 |
+
robot: parsed.robot,
|
| 913 |
+
scene: parsed.scene
|
| 914 |
+
};
|
| 915 |
+
send('switch_robot', {robot: parsed.robot, scene: parsed.scene});
|
| 916 |
+
updateRobotUI(parsed.robot, parsed.scene);
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
const viewport = document.getElementById('viewport');
|
| 920 |
+
const camDist = document.getElementById('cam_dist');
|
| 921 |
+
const camDistVal = document.getElementById('cam_dist_val');
|
| 922 |
+
const armTeleopVx = document.getElementById('arm_teleop_vx');
|
| 923 |
+
const armTeleopVy = document.getElementById('arm_teleop_vy');
|
| 924 |
+
const armTeleopVz = document.getElementById('arm_teleop_vz');
|
| 925 |
+
|
| 926 |
+
let keysPressed = new Set();
|
| 927 |
+
|
| 928 |
+
function resetEnv() {
|
| 929 |
+
send('reset');
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
// Homing functionality for UR5
|
| 933 |
+
let homingInterval = null;
|
| 934 |
+
let homingJoints = []; // Joints being homed
|
| 935 |
+
|
| 936 |
+
function startHoming() {
|
| 937 |
+
console.log('startHoming called:', {
|
| 938 |
+
currentRobot,
|
| 939 |
+
hasHomePose: !!currentHomePose,
|
| 940 |
+
novaStateStreaming,
|
| 941 |
+
wsState: ws ? ws.readyState : 'no ws'
|
| 942 |
+
});
|
| 943 |
+
|
| 944 |
+
if (currentRobot !== 'ur5' && currentRobot !== 'ur5_t_push') {
|
| 945 |
+
console.log('Not a UR5 robot, skipping homing');
|
| 946 |
+
return;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
// Check if we have necessary data
|
| 950 |
+
if (!currentHomePose) {
|
| 951 |
+
console.warn('Cannot home: home_pose not available');
|
| 952 |
+
return;
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
if (novaStateStreaming) {
|
| 956 |
+
// Nova mode: use joint jogging to move to home
|
| 957 |
+
console.log('Starting Nova jogging homing...');
|
| 958 |
+
startNovaJoggingHome();
|
| 959 |
+
} else {
|
| 960 |
+
// Local mode: send home messages continuously while button is pressed
|
| 961 |
+
console.log('Starting local homing...');
|
| 962 |
+
send('home');
|
| 963 |
+
|
| 964 |
+
// Send home message every 100ms while button is pressed
|
| 965 |
+
if (homingInterval) clearInterval(homingInterval);
|
| 966 |
+
homingInterval = setInterval(() => {
|
| 967 |
+
send('home');
|
| 968 |
+
}, 100);
|
| 969 |
+
}
|
| 970 |
+
}
|
| 971 |
+
|
| 972 |
+
function stopHoming() {
|
| 973 |
+
if (novaStateStreaming) {
|
| 974 |
+
// Stop all motion by sending empty action
|
| 975 |
+
send('action', {});
|
| 976 |
+
homingJoints = [];
|
| 977 |
+
if (homingInterval) {
|
| 978 |
+
clearInterval(homingInterval);
|
| 979 |
+
homingInterval = null;
|
| 980 |
+
}
|
| 981 |
+
} else {
|
| 982 |
+
// Stop the home message interval
|
| 983 |
+
if (homingInterval) {
|
| 984 |
+
clearInterval(homingInterval);
|
| 985 |
+
homingInterval = null;
|
| 986 |
+
}
|
| 987 |
+
// Send stop_home message to backend
|
| 988 |
+
send('stop_home');
|
| 989 |
+
console.log('Sent stop_home message');
|
| 990 |
+
}
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
function startNovaJoggingHome() {
|
| 994 |
+
if (!currentHomePose || !currentJointPositions) {
|
| 995 |
+
console.warn('Cannot jog home: missing home_pose or joint_positions');
|
| 996 |
+
return;
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
const tolerance = 0.01; // 0.01 rad ~= 0.57 degrees
|
| 1000 |
+
|
| 1001 |
+
// Build action message with joint velocities for all joints that need to move
|
| 1002 |
+
const actionData: any = {};
|
| 1003 |
+
homingJoints = [];
|
| 1004 |
+
|
| 1005 |
+
for (let i = 0; i < Math.min(currentHomePose.length, currentJointPositions.length); i++) {
|
| 1006 |
+
const target = currentHomePose[i];
|
| 1007 |
+
const current = currentJointPositions[i];
|
| 1008 |
+
const error = target - current;
|
| 1009 |
+
|
| 1010 |
+
if (Math.abs(error) > tolerance) {
|
| 1011 |
+
const joint = i + 1; // 1-based for logging
|
| 1012 |
+
const velocity = error > 0 ? 0.5 : -0.5; // rad/s, direction encoded in sign
|
| 1013 |
+
|
| 1014 |
+
console.log(`Moving joint ${joint} (error: ${error.toFixed(3)} rad)`);
|
| 1015 |
+
|
| 1016 |
+
// Set joint velocity in action data (j1-j6)
|
| 1017 |
+
actionData[`j${joint}`] = velocity;
|
| 1018 |
+
homingJoints.push(joint);
|
| 1019 |
+
}
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
if (homingJoints.length === 0) {
|
| 1023 |
+
console.log('Already at home position');
|
| 1024 |
+
return;
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
// Send action message with joint velocities
|
| 1028 |
+
send('action', actionData);
|
| 1029 |
+
|
| 1030 |
+
// Monitor progress and stop when home is reached
|
| 1031 |
+
if (homingInterval) clearInterval(homingInterval);
|
| 1032 |
+
homingInterval = setInterval(() => {
|
| 1033 |
+
if (!currentHomePose || !currentJointPositions) {
|
| 1034 |
+
stopHoming();
|
| 1035 |
+
return;
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
// Check if we're at home
|
| 1039 |
+
let allAtHome = true;
|
| 1040 |
+
for (let i = 0; i < Math.min(currentHomePose.length, currentJointPositions.length); i++) {
|
| 1041 |
+
const target = currentHomePose[i];
|
| 1042 |
+
const current = currentJointPositions[i];
|
| 1043 |
+
const error = Math.abs(target - current);
|
| 1044 |
+
|
| 1045 |
+
if (error > tolerance) {
|
| 1046 |
+
allAtHome = false;
|
| 1047 |
+
break;
|
| 1048 |
+
}
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
if (allAtHome) {
|
| 1052 |
+
console.log('Home position reached');
|
| 1053 |
+
stopHoming();
|
| 1054 |
+
}
|
| 1055 |
+
}, 100); // Check every 100ms
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
function setCmd(vx, vy, vyaw) {
|
| 1059 |
+
send('command', {vx, vy, vyaw});
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
function setCameraFollow() {
|
| 1063 |
+
const follow = document.getElementById('cam_follow').checked;
|
| 1064 |
+
send('camera_follow', {follow});
|
| 1065 |
+
}
|
| 1066 |
+
|
| 1067 |
+
// UR5 controls
|
| 1068 |
+
let currentControlMode = 'ik';
|
| 1069 |
+
let controlModeChangeTime = 0;
|
| 1070 |
+
|
| 1071 |
+
function setControlMode(mode) {
|
| 1072 |
+
currentControlMode = mode;
|
| 1073 |
+
controlModeChangeTime = Date.now();
|
| 1074 |
+
send('control_mode', {mode});
|
| 1075 |
+
|
| 1076 |
+
// Update UI
|
| 1077 |
+
const modeIkBtn = document.getElementById('mode_ik');
|
| 1078 |
+
const modeJointBtn = document.getElementById('mode_joint');
|
| 1079 |
+
const ikControls = document.getElementById('ik_controls');
|
| 1080 |
+
const jointControls = document.getElementById('joint_controls');
|
| 1081 |
+
|
| 1082 |
+
if (modeIkBtn) modeIkBtn.classList.toggle('active', mode === 'ik');
|
| 1083 |
+
if (modeJointBtn) modeJointBtn.classList.toggle('active', mode === 'joint');
|
| 1084 |
+
if (ikControls) ikControls.classList.toggle('active', mode === 'ik');
|
| 1085 |
+
if (jointControls) jointControls.classList.toggle('active', mode === 'joint');
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
// Jogging velocities
|
| 1089 |
+
let transVelocity = 50.0; // mm/s
|
| 1090 |
+
let rotVelocity = 0.3; // rad/s
|
| 1091 |
+
let jointVelocity = 0.5; // rad/s
|
| 1092 |
+
|
| 1093 |
+
function updateTransVelocity() {
|
| 1094 |
+
transVelocity = parseFloat(document.getElementById('trans_velocity').value);
|
| 1095 |
+
document.getElementById('trans_vel_val').innerText = transVelocity.toFixed(0);
|
| 1096 |
+
teleopTranslationStep = transVelocity / 1000;
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
function updateRotVelocity() {
|
| 1100 |
+
rotVelocity = parseFloat(document.getElementById('rot_velocity').value);
|
| 1101 |
+
document.getElementById('rot_vel_val').innerText = rotVelocity.toFixed(1);
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
function updateJointVelocity() {
|
| 1105 |
+
jointVelocity = parseFloat(document.getElementById('joint_velocity').value);
|
| 1106 |
+
document.getElementById('joint_vel_val').innerText = jointVelocity.toFixed(1);
|
| 1107 |
+
}
|
| 1108 |
+
|
| 1109 |
+
function startJog(jogType, axisOrJoint, direction) {
|
| 1110 |
+
const actionData: any = {};
|
| 1111 |
+
const vel = direction === '+' ? 1 : -1;
|
| 1112 |
+
|
| 1113 |
+
if (jogType === 'cartesian_translation') {
|
| 1114 |
+
// Convert mm/s to m/s and set appropriate axis
|
| 1115 |
+
const velocity = (transVelocity / 1000) * vel;
|
| 1116 |
+
if (axisOrJoint === 'x') actionData.vx = velocity;
|
| 1117 |
+
else if (axisOrJoint === 'y') actionData.vy = velocity;
|
| 1118 |
+
else if (axisOrJoint === 'z') actionData.vz = velocity;
|
| 1119 |
+
} else if (jogType === 'cartesian_rotation') {
|
| 1120 |
+
// Set rotation velocity in rad/s
|
| 1121 |
+
const velocity = rotVelocity * vel;
|
| 1122 |
+
if (axisOrJoint === 'x') actionData.vrx = velocity;
|
| 1123 |
+
else if (axisOrJoint === 'y') actionData.vry = velocity;
|
| 1124 |
+
else if (axisOrJoint === 'z') actionData.vrz = velocity;
|
| 1125 |
+
} else if (jogType === 'joint') {
|
| 1126 |
+
// Set joint velocity (axisOrJoint is 1-6)
|
| 1127 |
+
const velocity = jointVelocity * vel;
|
| 1128 |
+
actionData[`j${axisOrJoint}`] = velocity;
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
send('action', actionData);
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
function stopJog() {
|
| 1135 |
+
// Send zero velocities to stop all motion
|
| 1136 |
+
send('action', {});
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
function setGripper(action) {
|
| 1140 |
+
send('gripper', {action});
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
function updateCmdFromKeys() {
|
| 1144 |
+
let vx = 0, vy = 0, vyaw = 0;
|
| 1145 |
+
if (keysPressed.has('KeyW') || keysPressed.has('ArrowUp')) vx = 0.8;
|
| 1146 |
+
if (keysPressed.has('KeyS') || keysPressed.has('ArrowDown')) vx = -0.5;
|
| 1147 |
+
if (keysPressed.has('KeyA')) vyaw = 1.2;
|
| 1148 |
+
if (keysPressed.has('KeyD')) vyaw = -1.2;
|
| 1149 |
+
if (keysPressed.has('KeyQ')) vy = 0.4;
|
| 1150 |
+
if (keysPressed.has('KeyE')) vy = -0.4;
|
| 1151 |
+
if (keysPressed.has('ArrowLeft')) vyaw = 1.2;
|
| 1152 |
+
if (keysPressed.has('ArrowRight')) vyaw = -1.2;
|
| 1153 |
+
setCmd(vx, vy, vyaw);
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
function getActiveSelection() {
|
| 1157 |
+
return parseSelection(robotSelect.value);
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
function isArmRobot() {
|
| 1161 |
+
const active = getActiveSelection();
|
| 1162 |
+
return armRobotTypes.has(active.robot);
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
function shouldCaptureKey(code) {
|
| 1166 |
+
const active = getActiveSelection();
|
| 1167 |
+
return armRobotTypes.has(active.robot) ? armTeleopKeys.has(code) : locomotionKeys.has(code);
|
| 1168 |
+
}
|
| 1169 |
+
|
| 1170 |
+
function hasActiveTeleopKeys() {
|
| 1171 |
+
const keySet = isArmRobot() ? armTeleopKeys : locomotionKeys;
|
| 1172 |
+
for (const key of keySet) {
|
| 1173 |
+
if (keysPressed.has(key)) {
|
| 1174 |
+
return true;
|
| 1175 |
+
}
|
| 1176 |
+
}
|
| 1177 |
+
return false;
|
| 1178 |
+
}
|
| 1179 |
+
|
| 1180 |
+
function startTeleopRepeat() {
|
| 1181 |
+
if (teleopRepeatTimer) {
|
| 1182 |
+
return;
|
| 1183 |
+
}
|
| 1184 |
+
teleopRepeatTimer = setInterval(() => updateArmTeleopFromKeys(true), TELEOP_REPEAT_INTERVAL_MS);
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
function stopTeleopRepeat() {
|
| 1188 |
+
if (!teleopRepeatTimer) {
|
| 1189 |
+
return;
|
| 1190 |
+
}
|
| 1191 |
+
clearInterval(teleopRepeatTimer);
|
| 1192 |
+
teleopRepeatTimer = null;
|
| 1193 |
+
}
|
| 1194 |
+
|
| 1195 |
+
function updateArmTeleopFromKeys(force = false) {
|
| 1196 |
+
let dx = 0, dy = 0, dz = 0;
|
| 1197 |
+
if (keysPressed.has('KeyW')) dx += teleopTranslationStep;
|
| 1198 |
+
if (keysPressed.has('KeyS')) dx -= teleopTranslationStep;
|
| 1199 |
+
if (keysPressed.has('KeyA')) dy += teleopTranslationStep;
|
| 1200 |
+
if (keysPressed.has('KeyD')) dy -= teleopTranslationStep;
|
| 1201 |
+
if (keysPressed.has('KeyR')) dz += teleopVerticalStep;
|
| 1202 |
+
if (keysPressed.has('KeyF')) dz -= teleopVerticalStep;
|
| 1203 |
+
|
| 1204 |
+
const unchanged =
|
| 1205 |
+
Math.abs(dx - lastTeleopCommand.dx) < 1e-6 &&
|
| 1206 |
+
Math.abs(dy - lastTeleopCommand.dy) < 1e-6 &&
|
| 1207 |
+
Math.abs(dz - lastTeleopCommand.dz) < 1e-6;
|
| 1208 |
+
|
| 1209 |
+
if (unchanged && !force) {
|
| 1210 |
+
return;
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
lastTeleopCommand = {dx, dy, dz};
|
| 1214 |
+
send('teleop_action', {dx, dy, dz});
|
| 1215 |
+
}
|
| 1216 |
+
|
| 1217 |
+
function startArmJogForKey(code) {
|
| 1218 |
+
const entry = armJogKeyMap[code];
|
| 1219 |
+
if (!entry) {
|
| 1220 |
+
return;
|
| 1221 |
+
}
|
| 1222 |
+
startJog(entry.jogType, entry.axis, entry.direction);
|
| 1223 |
+
}
|
| 1224 |
+
|
| 1225 |
+
function handleArmKeyDown(code) {
|
| 1226 |
+
startArmJogForKey(code);
|
| 1227 |
+
}
|
| 1228 |
+
|
| 1229 |
+
function handleArmKeyUp(code) {
|
| 1230 |
+
if (!armJogKeyMap[code]) {
|
| 1231 |
+
return;
|
| 1232 |
+
}
|
| 1233 |
+
stopJog();
|
| 1234 |
+
const remaining = [...keysPressed].filter((key) => armJogKeyMap[key]);
|
| 1235 |
+
if (remaining.length > 0) {
|
| 1236 |
+
startArmJogForKey(remaining[remaining.length - 1]);
|
| 1237 |
+
}
|
| 1238 |
+
}
|
| 1239 |
+
|
| 1240 |
+
function handleKeyStateChange() {
|
| 1241 |
+
if (isArmRobot()) {
|
| 1242 |
+
return;
|
| 1243 |
+
}
|
| 1244 |
+
const active = hasActiveTeleopKeys();
|
| 1245 |
+
if (active) {
|
| 1246 |
+
startTeleopRepeat();
|
| 1247 |
+
} else {
|
| 1248 |
+
stopTeleopRepeat();
|
| 1249 |
+
}
|
| 1250 |
+
updateCmdFromKeys();
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
window.addEventListener('keydown', (e) => {
|
| 1254 |
+
if (e.code === 'Enter') {
|
| 1255 |
+
console.log('ENTER KEY PRESSED', { isArm: isArmRobot() });
|
| 1256 |
+
e.preventDefault();
|
| 1257 |
+
// For ARM robots, trigger homing; for locomotion robots, terminate episode
|
| 1258 |
+
if (isArmRobot()) {
|
| 1259 |
+
console.log('Calling startHoming from Enter key');
|
| 1260 |
+
startHoming();
|
| 1261 |
+
} else {
|
| 1262 |
+
requestEpisodeControl('terminate');
|
| 1263 |
+
}
|
| 1264 |
+
return;
|
| 1265 |
+
}
|
| 1266 |
+
if (shouldCaptureKey(e.code)) {
|
| 1267 |
+
e.preventDefault();
|
| 1268 |
+
const alreadyPressed = keysPressed.has(e.code);
|
| 1269 |
+
keysPressed.add(e.code);
|
| 1270 |
+
if (isArmRobot()) {
|
| 1271 |
+
if (!alreadyPressed) {
|
| 1272 |
+
handleArmKeyDown(e.code);
|
| 1273 |
+
}
|
| 1274 |
+
} else {
|
| 1275 |
+
handleKeyStateChange();
|
| 1276 |
+
}
|
| 1277 |
+
}
|
| 1278 |
+
});
|
| 1279 |
+
|
| 1280 |
+
window.addEventListener('keyup', (e) => {
|
| 1281 |
+
if (e.code === 'Enter') {
|
| 1282 |
+
// For ARM robots, stop homing on Enter key release
|
| 1283 |
+
if (isArmRobot()) {
|
| 1284 |
+
stopHoming();
|
| 1285 |
+
}
|
| 1286 |
+
return;
|
| 1287 |
+
}
|
| 1288 |
+
if (keysPressed.delete(e.code)) {
|
| 1289 |
+
if (isArmRobot()) {
|
| 1290 |
+
handleArmKeyUp(e.code);
|
| 1291 |
+
} else {
|
| 1292 |
+
handleKeyStateChange();
|
| 1293 |
+
}
|
| 1294 |
+
}
|
| 1295 |
+
});
|
| 1296 |
+
|
| 1297 |
+
camDist.oninput = () => {
|
| 1298 |
+
camDistVal.innerText = parseFloat(camDist.value).toFixed(1);
|
| 1299 |
+
send('camera', {action: 'set_distance', distance: parseFloat(camDist.value)});
|
| 1300 |
+
};
|
| 1301 |
+
|
| 1302 |
+
let isDragging = false;
|
| 1303 |
+
let lastX, lastY;
|
| 1304 |
+
|
| 1305 |
+
viewport.oncontextmenu = (e) => e.preventDefault();
|
| 1306 |
+
|
| 1307 |
+
// Mouse controls
|
| 1308 |
+
viewport.onmousedown = (e) => {
|
| 1309 |
+
isDragging = true;
|
| 1310 |
+
lastX = e.clientX;
|
| 1311 |
+
lastY = e.clientY;
|
| 1312 |
+
};
|
| 1313 |
+
|
| 1314 |
+
window.addEventListener('mouseup', () => {
|
| 1315 |
+
isDragging = false;
|
| 1316 |
+
});
|
| 1317 |
+
|
| 1318 |
+
window.onmousemove = (e) => {
|
| 1319 |
+
if (isDragging) {
|
| 1320 |
+
const dx = e.clientX - lastX;
|
| 1321 |
+
const dy = e.clientY - lastY;
|
| 1322 |
+
lastX = e.clientX;
|
| 1323 |
+
lastY = e.clientY;
|
| 1324 |
+
send('camera', {action: 'rotate', dx, dy});
|
| 1325 |
+
}
|
| 1326 |
+
};
|
| 1327 |
+
|
| 1328 |
+
viewport.onwheel = (e) => {
|
| 1329 |
+
e.preventDefault();
|
| 1330 |
+
send('camera', {action: 'zoom', dz: e.deltaY});
|
| 1331 |
+
};
|
| 1332 |
+
|
| 1333 |
+
// Touch controls for camera rotation and pinch-to-zoom
|
| 1334 |
+
let touchStartX, touchStartY;
|
| 1335 |
+
let lastPinchDist = null;
|
| 1336 |
+
|
| 1337 |
+
function getTouchDistance(touches) {
|
| 1338 |
+
const dx = touches[0].clientX - touches[1].clientX;
|
| 1339 |
+
const dy = touches[0].clientY - touches[1].clientY;
|
| 1340 |
+
return Math.sqrt(dx * dx + dy * dy);
|
| 1341 |
+
}
|
| 1342 |
+
|
| 1343 |
+
viewport.addEventListener('touchstart', (e) => {
|
| 1344 |
+
if (e.touches.length === 1) {
|
| 1345 |
+
// Single touch - rotation
|
| 1346 |
+
touchStartX = e.touches[0].clientX;
|
| 1347 |
+
touchStartY = e.touches[0].clientY;
|
| 1348 |
+
lastPinchDist = null;
|
| 1349 |
+
} else if (e.touches.length === 2) {
|
| 1350 |
+
// Two touches - pinch zoom
|
| 1351 |
+
lastPinchDist = getTouchDistance(e.touches);
|
| 1352 |
+
}
|
| 1353 |
+
}, {passive: true});
|
| 1354 |
+
|
| 1355 |
+
viewport.addEventListener('touchmove', (e) => {
|
| 1356 |
+
e.preventDefault();
|
| 1357 |
+
if (e.touches.length === 1 && lastPinchDist === null) {
|
| 1358 |
+
// Single touch drag - rotate camera
|
| 1359 |
+
const dx = e.touches[0].clientX - touchStartX;
|
| 1360 |
+
const dy = e.touches[0].clientY - touchStartY;
|
| 1361 |
+
touchStartX = e.touches[0].clientX;
|
| 1362 |
+
touchStartY = e.touches[0].clientY;
|
| 1363 |
+
send('camera', {action: 'rotate', dx, dy});
|
| 1364 |
+
} else if (e.touches.length === 2 && lastPinchDist !== null) {
|
| 1365 |
+
// Pinch zoom
|
| 1366 |
+
const dist = getTouchDistance(e.touches);
|
| 1367 |
+
const delta = lastPinchDist - dist;
|
| 1368 |
+
lastPinchDist = dist;
|
| 1369 |
+
// Scale delta for smoother zoom
|
| 1370 |
+
send('camera', {action: 'zoom', dz: delta * 3});
|
| 1371 |
+
}
|
| 1372 |
+
}, {passive: false});
|
| 1373 |
+
|
| 1374 |
+
viewport.addEventListener('touchend', (e) => {
|
| 1375 |
+
if (e.touches.length < 2) {
|
| 1376 |
+
lastPinchDist = null;
|
| 1377 |
+
}
|
| 1378 |
+
if (e.touches.length === 1) {
|
| 1379 |
+
// Reset for single finger after pinch
|
| 1380 |
+
touchStartX = e.touches[0].clientX;
|
| 1381 |
+
touchStartY = e.touches[0].clientY;
|
| 1382 |
+
}
|
| 1383 |
+
}, {passive: true});
|
| 1384 |
+
|
| 1385 |
+
// Add panel header click handler
|
| 1386 |
+
const panelHeader = document.getElementById('panel_header');
|
| 1387 |
+
if (panelHeader) {
|
| 1388 |
+
panelHeader.addEventListener('click', togglePanel);
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
// Add robot selector change handler
|
| 1392 |
+
if (robotSelect) {
|
| 1393 |
+
robotSelect.addEventListener('change', switchRobot);
|
| 1394 |
+
}
|
| 1395 |
+
|
| 1396 |
+
// Add control mode button handlers
|
| 1397 |
+
const modeIkBtn = document.getElementById('mode_ik');
|
| 1398 |
+
const modeJointBtn = document.getElementById('mode_joint');
|
| 1399 |
+
if (modeIkBtn) {
|
| 1400 |
+
modeIkBtn.addEventListener('click', () => setControlMode('ik'));
|
| 1401 |
+
}
|
| 1402 |
+
if (modeJointBtn) {
|
| 1403 |
+
modeJointBtn.addEventListener('click', () => setControlMode('joint'));
|
| 1404 |
+
}
|
| 1405 |
+
|
| 1406 |
+
// Add locomotion button handlers
|
| 1407 |
+
const locomotionButtons = document.querySelectorAll('.locomotion-controls .rl-btn');
|
| 1408 |
+
locomotionButtons.forEach(btn => {
|
| 1409 |
+
const text = btn.textContent;
|
| 1410 |
+
if (text.includes('W Forward')) {
|
| 1411 |
+
btn.addEventListener('mousedown', () => setCmd(0.8, 0, 0));
|
| 1412 |
+
btn.addEventListener('mouseup', () => setCmd(0, 0, 0));
|
| 1413 |
+
btn.addEventListener('touchstart', () => setCmd(0.8, 0, 0));
|
| 1414 |
+
btn.addEventListener('touchend', () => setCmd(0, 0, 0));
|
| 1415 |
+
} else if (text.includes('S Back')) {
|
| 1416 |
+
btn.addEventListener('mousedown', () => setCmd(-0.5, 0, 0));
|
| 1417 |
+
btn.addEventListener('mouseup', () => setCmd(0, 0, 0));
|
| 1418 |
+
btn.addEventListener('touchstart', () => setCmd(-0.5, 0, 0));
|
| 1419 |
+
btn.addEventListener('touchend', () => setCmd(0, 0, 0));
|
| 1420 |
+
} else if (text.includes('A Turn L')) {
|
| 1421 |
+
btn.addEventListener('mousedown', () => setCmd(0, 0, 1.2));
|
| 1422 |
+
btn.addEventListener('mouseup', () => setCmd(0, 0, 0));
|
| 1423 |
+
btn.addEventListener('touchstart', () => setCmd(0, 0, 1.2));
|
| 1424 |
+
btn.addEventListener('touchend', () => setCmd(0, 0, 0));
|
| 1425 |
+
} else if (text.includes('D Turn R')) {
|
| 1426 |
+
btn.addEventListener('mousedown', () => setCmd(0, 0, -1.2));
|
| 1427 |
+
btn.addEventListener('mouseup', () => setCmd(0, 0, 0));
|
| 1428 |
+
btn.addEventListener('touchstart', () => setCmd(0, 0, -1.2));
|
| 1429 |
+
btn.addEventListener('touchend', () => setCmd(0, 0, 0));
|
| 1430 |
+
} else if (text.includes('Q Strafe L')) {
|
| 1431 |
+
btn.addEventListener('mousedown', () => setCmd(0, 0.4, 0));
|
| 1432 |
+
btn.addEventListener('mouseup', () => setCmd(0, 0, 0));
|
| 1433 |
+
btn.addEventListener('touchstart', () => setCmd(0, 0.4, 0));
|
| 1434 |
+
btn.addEventListener('touchend', () => setCmd(0, 0, 0));
|
| 1435 |
+
} else if (text.includes('E Strafe R')) {
|
| 1436 |
+
btn.addEventListener('mousedown', () => setCmd(0, -0.4, 0));
|
| 1437 |
+
btn.addEventListener('mouseup', () => setCmd(0, 0, 0));
|
| 1438 |
+
btn.addEventListener('touchstart', () => setCmd(0, -0.4, 0));
|
| 1439 |
+
btn.addEventListener('touchend', () => setCmd(0, 0, 0));
|
| 1440 |
+
} else if (text.includes('Stop')) {
|
| 1441 |
+
btn.addEventListener('click', () => setCmd(0, 0, 0));
|
| 1442 |
+
}
|
| 1443 |
+
});
|
| 1444 |
+
|
| 1445 |
+
// Add arm jogging button handlers
|
| 1446 |
+
const jogButtons = document.querySelectorAll('.jog-btn');
|
| 1447 |
+
jogButtons.forEach(btn => {
|
| 1448 |
+
const row = btn.closest('.jog-row');
|
| 1449 |
+
if (!row) return;
|
| 1450 |
+
|
| 1451 |
+
const label = row.querySelector('label');
|
| 1452 |
+
if (!label) return;
|
| 1453 |
+
|
| 1454 |
+
const labelText = label.textContent.trim();
|
| 1455 |
+
const isPlus = btn.textContent === '+';
|
| 1456 |
+
const direction = isPlus ? '+' : '-';
|
| 1457 |
+
|
| 1458 |
+
// Determine jog type and axis from parent section
|
| 1459 |
+
const ikControls = btn.closest('#ik_controls');
|
| 1460 |
+
const jointControls = btn.closest('#joint_controls');
|
| 1461 |
+
|
| 1462 |
+
if (ikControls) {
|
| 1463 |
+
// IK mode jogging
|
| 1464 |
+
const translationSection = btn.closest('.control-group')?.querySelector('label')?.textContent?.includes('Translation');
|
| 1465 |
+
const rotationSection = btn.closest('.control-group')?.querySelector('label')?.textContent?.includes('Rotation');
|
| 1466 |
+
|
| 1467 |
+
if (translationSection) {
|
| 1468 |
+
const axis = labelText === 'X' ? 'x' : labelText === 'Y' ? 'y' : 'z';
|
| 1469 |
+
btn.addEventListener('mousedown', () => startJog('cartesian_translation', axis, direction));
|
| 1470 |
+
btn.addEventListener('mouseup', () => stopJog());
|
| 1471 |
+
btn.addEventListener('touchstart', (e) => { e.preventDefault(); startJog('cartesian_translation', axis, direction); });
|
| 1472 |
+
btn.addEventListener('touchend', () => stopJog());
|
| 1473 |
+
} else if (rotationSection) {
|
| 1474 |
+
const axis = labelText === 'Rx' ? 'x' : labelText === 'Ry' ? 'y' : 'z';
|
| 1475 |
+
btn.addEventListener('mousedown', () => startJog('cartesian_rotation', axis, direction));
|
| 1476 |
+
btn.addEventListener('mouseup', () => stopJog());
|
| 1477 |
+
btn.addEventListener('touchstart', (e) => { e.preventDefault(); startJog('cartesian_rotation', axis, direction); });
|
| 1478 |
+
btn.addEventListener('touchend', () => stopJog());
|
| 1479 |
+
}
|
| 1480 |
+
} else if (jointControls) {
|
| 1481 |
+
// Joint mode jogging
|
| 1482 |
+
const jointNum = labelText.replace('J', '');
|
| 1483 |
+
btn.addEventListener('mousedown', () => startJog('joint', parseInt(jointNum), direction));
|
| 1484 |
+
btn.addEventListener('mouseup', () => stopJog());
|
| 1485 |
+
btn.addEventListener('touchstart', (e) => { e.preventDefault(); startJog('joint', parseInt(jointNum), direction); });
|
| 1486 |
+
btn.addEventListener('touchend', () => stopJog());
|
| 1487 |
+
}
|
| 1488 |
+
});
|
| 1489 |
+
|
| 1490 |
+
// Add gripper button handlers
|
| 1491 |
+
const gripperButtons = document.querySelectorAll('#gripper_controls .rl-btn');
|
| 1492 |
+
gripperButtons.forEach(btn => {
|
| 1493 |
+
const text = btn.textContent;
|
| 1494 |
+
if (text.includes('Open')) {
|
| 1495 |
+
btn.addEventListener('click', () => setGripper('open'));
|
| 1496 |
+
} else if (text.includes('Close')) {
|
| 1497 |
+
btn.addEventListener('click', () => setGripper('close'));
|
| 1498 |
+
}
|
| 1499 |
+
});
|
| 1500 |
+
|
| 1501 |
+
// Add reset environment button handler
|
| 1502 |
+
const resetButtons = document.querySelectorAll('button.danger');
|
| 1503 |
+
resetButtons.forEach(btn => {
|
| 1504 |
+
if (btn.textContent.includes('Reset')) {
|
| 1505 |
+
btn.addEventListener('click', () => send('reset', {}));
|
| 1506 |
+
}
|
| 1507 |
+
});
|
| 1508 |
+
|
| 1509 |
+
// Add home button handler
|
| 1510 |
+
const homeBtn = document.getElementById('homeBtn');
|
| 1511 |
+
if (homeBtn) {
|
| 1512 |
+
homeBtn.addEventListener('mousedown', () => startHoming());
|
| 1513 |
+
homeBtn.addEventListener('mouseup', () => stopHoming());
|
| 1514 |
+
homeBtn.addEventListener('mouseleave', () => stopHoming());
|
| 1515 |
+
homeBtn.addEventListener('touchstart', () => startHoming());
|
| 1516 |
+
homeBtn.addEventListener('touchend', () => stopHoming());
|
| 1517 |
+
}
|
| 1518 |
+
|
| 1519 |
+
// Add velocity controls handlers
|
| 1520 |
+
const rotVelocitySlider = document.getElementById('rot_velocity');
|
| 1521 |
+
const jointVelocitySlider = document.getElementById('joint_velocity');
|
| 1522 |
+
|
| 1523 |
+
if (rotVelocitySlider) {
|
| 1524 |
+
rotVelocitySlider.addEventListener('input', updateRotVelocity);
|
| 1525 |
+
}
|
| 1526 |
+
|
| 1527 |
+
if (jointVelocitySlider) {
|
| 1528 |
+
jointVelocitySlider.addEventListener('input', updateJointVelocity);
|
| 1529 |
+
}
|
| 1530 |
+
|
| 1531 |
+
// Connect on load
|
| 1532 |
+
enterConnectingState();
|
| 1533 |
+
connect();
|
frontend/src/styles.css
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Wandelbots Corporate Design Colors:
|
| 2 |
+
Primary Dark: #01040f (Blue Charcoal)
|
| 3 |
+
Light/Secondary: #bcbeec (Spindle - lavender)
|
| 4 |
+
Accent: #211c44 (Port Gore - deep purple)
|
| 5 |
+
Logo Dark: #181838
|
| 6 |
+
*/
|
| 7 |
+
:root {
|
| 8 |
+
--wb-primary: #01040f;
|
| 9 |
+
--wb-secondary: #bcbeec;
|
| 10 |
+
--wb-accent: #211c44;
|
| 11 |
+
--wb-logo: #181838;
|
| 12 |
+
--wb-highlight: #8b7fef;
|
| 13 |
+
--wb-success: #7c6bef;
|
| 14 |
+
--wb-danger: #ef6b6b;
|
| 15 |
+
}
|
| 16 |
+
body, html {
|
| 17 |
+
margin: 0; padding: 0; width: 100%; height: 100%;
|
| 18 |
+
overflow: hidden; background: var(--wb-primary);
|
| 19 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 20 |
+
user-select: none;
|
| 21 |
+
}
|
| 22 |
+
.video-container {
|
| 23 |
+
position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
|
| 24 |
+
cursor: grab;
|
| 25 |
+
}
|
| 26 |
+
.video-container:active { cursor: grabbing; }
|
| 27 |
+
.video-container img { width: 100%; height: 100%; object-fit: cover; }
|
| 28 |
+
.overlay {
|
| 29 |
+
position: absolute; top: 20px; left: 20px;
|
| 30 |
+
background: rgba(33, 28, 68, 0.85);
|
| 31 |
+
backdrop-filter: blur(15px);
|
| 32 |
+
padding: 25px; border-radius: 12px;
|
| 33 |
+
box-shadow: 0 8px 32px rgba(1, 4, 15, 0.6);
|
| 34 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
|
| 35 |
+
z-index: 100; width: 320px;
|
| 36 |
+
max-height: calc(100vh - 40px);
|
| 37 |
+
overflow-y: auto;
|
| 38 |
+
box-sizing: border-box;
|
| 39 |
+
}
|
| 40 |
+
.overlay h2 { margin: 0 0 15px 0; font-size: 1.1em; font-weight: 600; letter-spacing: 0.5px; color: #fff; }
|
| 41 |
+
.control-group { margin-bottom: 15px; }
|
| 42 |
+
.control-group label { display: block; margin-bottom: 5px; font-size: 0.8em; opacity: 0.8; color: var(--wb-secondary); }
|
| 43 |
+
.slider-row { display: flex; align-items: center; gap: 12px; }
|
| 44 |
+
input[type=range] {
|
| 45 |
+
flex-grow: 1; cursor: pointer;
|
| 46 |
+
accent-color: var(--wb-highlight);
|
| 47 |
+
}
|
| 48 |
+
.val-display { width: 60px; font-family: monospace; font-size: 0.9em; text-align: right; color: #fff; }
|
| 49 |
+
button {
|
| 50 |
+
flex: 1; padding: 10px;
|
| 51 |
+
background: rgba(188, 190, 236, 0.1);
|
| 52 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.25);
|
| 53 |
+
border-radius: 6px; cursor: pointer; transition: all 0.2s;
|
| 54 |
+
font-size: 0.85em;
|
| 55 |
+
}
|
| 56 |
+
button:hover { background: rgba(188, 190, 236, 0.2); border-color: rgba(188, 190, 236, 0.4); }
|
| 57 |
+
button.danger { background: rgba(239, 107, 107, 0.4); border-color: rgba(239, 107, 107, 0.5); }
|
| 58 |
+
button.danger:hover { background: rgba(239, 107, 107, 0.6); }
|
| 59 |
+
.stats { margin-top: 15px; font-size: 0.8em; opacity: 0.9; line-height: 1.6; color: var(--wb-secondary); }
|
| 60 |
+
.hint { position: absolute; bottom: 20px; right: 20px; color: rgba(188, 190, 236, 0.5); font-size: 0.8em; pointer-events: none; text-align: right; }
|
| 61 |
+
.rl-notifications {
|
| 62 |
+
position: absolute;
|
| 63 |
+
right: 24px;
|
| 64 |
+
bottom: 24px;
|
| 65 |
+
display: flex;
|
| 66 |
+
flex-direction: column;
|
| 67 |
+
gap: 8px;
|
| 68 |
+
z-index: 250;
|
| 69 |
+
}
|
| 70 |
+
.rl-notifications .notification {
|
| 71 |
+
min-width: 200px;
|
| 72 |
+
padding: 10px 14px;
|
| 73 |
+
background: rgba(9, 8, 29, 0.95);
|
| 74 |
+
border-radius: 12px;
|
| 75 |
+
border: 1px solid rgba(188, 190, 236, 0.25);
|
| 76 |
+
font-size: 0.75em;
|
| 77 |
+
color: #fff;
|
| 78 |
+
box-shadow: 0 10px 24px rgba(1, 4, 15, 0.5);
|
| 79 |
+
animation: toast-pop 0.35s ease;
|
| 80 |
+
}
|
| 81 |
+
@keyframes toast-pop {
|
| 82 |
+
from { transform: translateY(10px); opacity: 0; }
|
| 83 |
+
to { transform: translateY(0); opacity: 1; }
|
| 84 |
+
}
|
| 85 |
+
.overlay-tiles {
|
| 86 |
+
position: absolute;
|
| 87 |
+
right: 20px;
|
| 88 |
+
bottom: 50px;
|
| 89 |
+
top: auto;
|
| 90 |
+
width: 240px;
|
| 91 |
+
display: none;
|
| 92 |
+
flex-direction: column;
|
| 93 |
+
gap: 10px;
|
| 94 |
+
z-index: 200;
|
| 95 |
+
pointer-events: none;
|
| 96 |
+
}
|
| 97 |
+
.overlay-tile {
|
| 98 |
+
width: 100%;
|
| 99 |
+
height: 140px;
|
| 100 |
+
border-radius: 8px;
|
| 101 |
+
overflow: hidden;
|
| 102 |
+
background: rgba(0, 0, 0, 0.35);
|
| 103 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 104 |
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
| 105 |
+
display: flex;
|
| 106 |
+
flex-direction: column;
|
| 107 |
+
}
|
| 108 |
+
.overlay-tile img {
|
| 109 |
+
width: 100%;
|
| 110 |
+
height: 100%;
|
| 111 |
+
object-fit: cover;
|
| 112 |
+
filter: saturate(1.1);
|
| 113 |
+
}
|
| 114 |
+
.overlay-label {
|
| 115 |
+
position: absolute;
|
| 116 |
+
bottom: 4px;
|
| 117 |
+
left: 6px;
|
| 118 |
+
right: 6px;
|
| 119 |
+
font-size: 0.65em;
|
| 120 |
+
text-transform: uppercase;
|
| 121 |
+
letter-spacing: 0.2em;
|
| 122 |
+
color: rgba(255, 255, 255, 0.9);
|
| 123 |
+
text-shadow: 0 0 6px rgba(0, 0, 0, 0.6);
|
| 124 |
+
}
|
| 125 |
+
.control-panel-info {
|
| 126 |
+
margin-top: 12px;
|
| 127 |
+
padding: 10px;
|
| 128 |
+
border: 1px solid rgba(188, 190, 236, 0.2);
|
| 129 |
+
border-radius: 8px;
|
| 130 |
+
background: rgba(188, 190, 236, 0.05);
|
| 131 |
+
font-size: 0.8em;
|
| 132 |
+
line-height: 1.4;
|
| 133 |
+
}
|
| 134 |
+
.control-panel-info ul {
|
| 135 |
+
margin: 6px 0 0 10px;
|
| 136 |
+
padding: 0;
|
| 137 |
+
display: flex;
|
| 138 |
+
flex-direction: column;
|
| 139 |
+
gap: 6px;
|
| 140 |
+
}
|
| 141 |
+
.control-panel-info li {
|
| 142 |
+
list-style: none;
|
| 143 |
+
}
|
| 144 |
+
.hint-key {
|
| 145 |
+
display: flex;
|
| 146 |
+
align-items: center;
|
| 147 |
+
gap: 6px;
|
| 148 |
+
}
|
| 149 |
+
.hint-key kbd {
|
| 150 |
+
padding: 2px 6px;
|
| 151 |
+
border-radius: 4px;
|
| 152 |
+
background: rgba(139, 127, 239, 0.35);
|
| 153 |
+
border: 1px solid rgba(139, 127, 239, 0.55);
|
| 154 |
+
font-size: 0.75em;
|
| 155 |
+
color: #fff;
|
| 156 |
+
font-weight: 600;
|
| 157 |
+
}
|
| 158 |
+
/* State info panel - top right */
|
| 159 |
+
.state-panel {
|
| 160 |
+
position: absolute; top: 20px; right: 20px;
|
| 161 |
+
background: rgba(33, 28, 68, 0.85);
|
| 162 |
+
backdrop-filter: blur(12px);
|
| 163 |
+
padding: 8px 12px; border-radius: 8px;
|
| 164 |
+
box-shadow: 0 3px 14px rgba(1, 4, 15, 0.35);
|
| 165 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
|
| 166 |
+
z-index: 100; min-width: 180px; max-width: 220px;
|
| 167 |
+
font-size: 0.75em; line-height: 1.3;
|
| 168 |
+
}
|
| 169 |
+
.state-panel strong { color: #fff; }
|
| 170 |
+
/* Camera controls - bottom left */
|
| 171 |
+
.camera-panel {
|
| 172 |
+
position: absolute; bottom: 20px; left: 20px;
|
| 173 |
+
background: rgba(33, 28, 68, 0.85);
|
| 174 |
+
backdrop-filter: blur(15px);
|
| 175 |
+
padding: 15px 20px; border-radius: 10px;
|
| 176 |
+
box-shadow: 0 4px 20px rgba(1, 4, 15, 0.5);
|
| 177 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
|
| 178 |
+
z-index: 100; width: 280px;
|
| 179 |
+
}
|
| 180 |
+
.camera-panel .control-group { margin-bottom: 10px; }
|
| 181 |
+
.camera-panel .control-group:last-child { margin-bottom: 0; }
|
| 182 |
+
.state-hint {
|
| 183 |
+
margin-top: 12px;
|
| 184 |
+
font-size: 0.75em;
|
| 185 |
+
color: rgba(255, 255, 255, 0.7);
|
| 186 |
+
line-height: 1.4;
|
| 187 |
+
}
|
| 188 |
+
.camera-panel.inside-panel {
|
| 189 |
+
position: relative;
|
| 190 |
+
bottom: auto;
|
| 191 |
+
left: auto;
|
| 192 |
+
background: rgba(33, 28, 68, 0.75);
|
| 193 |
+
border: 1px solid rgba(188, 190, 236, 0.2);
|
| 194 |
+
box-shadow: none;
|
| 195 |
+
margin-top: 16px;
|
| 196 |
+
}
|
| 197 |
+
/* Collapsible panel header - title is the button */
|
| 198 |
+
.panel-header {
|
| 199 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 200 |
+
cursor: pointer; margin-bottom: 15px;
|
| 201 |
+
padding: 4px 0; border-radius: 4px; transition: all 0.2s;
|
| 202 |
+
}
|
| 203 |
+
.panel-header:hover { background: rgba(188, 190, 236, 0.1); }
|
| 204 |
+
.panel-header h2 {
|
| 205 |
+
margin: 0; display: flex; align-items: center; gap: 8px;
|
| 206 |
+
}
|
| 207 |
+
.panel-header h2::after {
|
| 208 |
+
content: ''; font-weight: 300; font-size: 1.2em;
|
| 209 |
+
opacity: 0.6; transition: transform 0.2s;
|
| 210 |
+
}
|
| 211 |
+
.panel-header.collapsed h2::after { content: ''; }
|
| 212 |
+
.panel-content { transition: all 0.3s ease; overflow: hidden; }
|
| 213 |
+
.panel-content.collapsed { max-height: 0; opacity: 0; margin: 0; padding: 0; }
|
| 214 |
+
.overlay.collapsed { width: auto; min-width: 200px; }
|
| 215 |
+
.rl-buttons { display: flex; flex-direction: column; gap: 5px; margin: 10px 0; }
|
| 216 |
+
.rl-row { display: flex; justify-content: center; gap: 5px; }
|
| 217 |
+
.rl-btn {
|
| 218 |
+
padding: 12px 16px; min-width: 80px;
|
| 219 |
+
background: rgba(124, 107, 239, 0.4); border: 1px solid rgba(124, 107, 239, 0.5);
|
| 220 |
+
color: #fff; border-radius: 6px; cursor: pointer; transition: all 0.15s;
|
| 221 |
+
font-size: 0.85em; font-weight: 500;
|
| 222 |
+
}
|
| 223 |
+
.rl-btn:hover { background: rgba(124, 107, 239, 0.6); }
|
| 224 |
+
.rl-btn:active { background: rgba(124, 107, 239, 0.8); transform: scale(0.95); }
|
| 225 |
+
.rl-btn.stop { background: rgba(239, 107, 107, 0.4); border-color: rgba(239, 107, 107, 0.5); }
|
| 226 |
+
.rl-btn.stop:hover { background: rgba(239, 107, 107, 0.6); }
|
| 227 |
+
.cmd-display { font-family: monospace; font-size: 0.8em; opacity: 0.8; margin-top: 8px; color: var(--wb-secondary); }
|
| 228 |
+
.connection-status-inline {
|
| 229 |
+
padding: 6px 12px; border-radius: 4px;
|
| 230 |
+
font-size: 0.75em; font-weight: 600;
|
| 231 |
+
background: rgba(124, 107, 239, 0.25);
|
| 232 |
+
color: #fff; border: 1px solid rgba(124, 107, 239, 0.4);
|
| 233 |
+
margin-bottom: 12px;
|
| 234 |
+
text-align: center;
|
| 235 |
+
text-transform: uppercase;
|
| 236 |
+
letter-spacing: 0.5px;
|
| 237 |
+
}
|
| 238 |
+
.connection-status-inline.disconnected {
|
| 239 |
+
background: rgba(239, 107, 107, 0.25);
|
| 240 |
+
border-color: rgba(239, 107, 107, 0.4);
|
| 241 |
+
}
|
| 242 |
+
.connection-status-inline .status-text {
|
| 243 |
+
display: inline-block;
|
| 244 |
+
}
|
| 245 |
+
.connection-status-inline .status-loader {
|
| 246 |
+
display: none;
|
| 247 |
+
width: 12px;
|
| 248 |
+
height: 12px;
|
| 249 |
+
border: 2px solid rgba(255, 255, 255, 0.35);
|
| 250 |
+
border-top-color: #fff;
|
| 251 |
+
border-radius: 50%;
|
| 252 |
+
animation: status-spin 0.8s linear infinite;
|
| 253 |
+
margin-left: 8px;
|
| 254 |
+
}
|
| 255 |
+
.connection-status-inline.connecting {
|
| 256 |
+
background: rgba(139, 127, 239, 0.35);
|
| 257 |
+
border-color: rgba(255, 255, 255, 0.3);
|
| 258 |
+
}
|
| 259 |
+
.connection-status-inline.connecting .status-loader {
|
| 260 |
+
display: inline-block;
|
| 261 |
+
}
|
| 262 |
+
@keyframes status-spin {
|
| 263 |
+
from { transform: rotate(0deg); }
|
| 264 |
+
to { transform: rotate(360deg); }
|
| 265 |
+
}
|
| 266 |
+
.status-card {
|
| 267 |
+
padding: 10px 12px;
|
| 268 |
+
border-radius: 8px;
|
| 269 |
+
border-left: 3px solid var(--wb-success);
|
| 270 |
+
background: rgba(76, 175, 80, 0.15);
|
| 271 |
+
display: flex;
|
| 272 |
+
flex-direction: column;
|
| 273 |
+
gap: 6px;
|
| 274 |
+
margin-top: 10px;
|
| 275 |
+
transition: border-color 0.2s, background 0.2s;
|
| 276 |
+
}
|
| 277 |
+
.status-card strong {
|
| 278 |
+
font-size: 0.8em;
|
| 279 |
+
letter-spacing: 0.2em;
|
| 280 |
+
text-transform: uppercase;
|
| 281 |
+
color: #fff;
|
| 282 |
+
}
|
| 283 |
+
.status-card .status-card-text {
|
| 284 |
+
font-size: 0.75em;
|
| 285 |
+
color: rgba(255, 255, 255, 0.8);
|
| 286 |
+
}
|
| 287 |
+
.status-card.disconnected {
|
| 288 |
+
background: rgba(239, 107, 107, 0.15);
|
| 289 |
+
border-color: rgba(239, 107, 107, 0.7);
|
| 290 |
+
}
|
| 291 |
+
.trainer-status {
|
| 292 |
+
display: flex;
|
| 293 |
+
align-items: center;
|
| 294 |
+
gap: 6px;
|
| 295 |
+
font-size: 0.7em;
|
| 296 |
+
letter-spacing: 0.4px;
|
| 297 |
+
text-transform: uppercase;
|
| 298 |
+
}
|
| 299 |
+
.trainer-status-dot {
|
| 300 |
+
width: 8px;
|
| 301 |
+
height: 8px;
|
| 302 |
+
border-radius: 50%;
|
| 303 |
+
background: rgba(124, 107, 239, 0.6);
|
| 304 |
+
border: 1px solid rgba(255, 255, 255, 0.45);
|
| 305 |
+
}
|
| 306 |
+
.trainer-status.connected .trainer-status-dot {
|
| 307 |
+
background: #53e89b;
|
| 308 |
+
box-shadow: 0 0 6px rgba(83, 232, 155, 0.8);
|
| 309 |
+
}
|
| 310 |
+
.trainer-status.disconnected .trainer-status-dot {
|
| 311 |
+
background: #ef6b6b;
|
| 312 |
+
box-shadow: 0 0 6px rgba(239, 107, 107, 0.8);
|
| 313 |
+
}
|
| 314 |
+
.camera-controls { margin-top: 15px; }
|
| 315 |
+
select {
|
| 316 |
+
width: 100%; padding: 10px;
|
| 317 |
+
background: rgba(188, 190, 236, 0.1);
|
| 318 |
+
color: #fff; border: 1px solid rgba(188, 190, 236, 0.25);
|
| 319 |
+
border-radius: 6px; cursor: pointer;
|
| 320 |
+
font-size: 0.9em;
|
| 321 |
+
}
|
| 322 |
+
select option { background: var(--wb-accent); color: #fff; }
|
| 323 |
+
.robot-selector { margin-bottom: 20px; }
|
| 324 |
+
.scene-summary {
|
| 325 |
+
margin-top: 12px;
|
| 326 |
+
font-size: 0.8em;
|
| 327 |
+
display: flex;
|
| 328 |
+
align-items: baseline;
|
| 329 |
+
gap: 6px;
|
| 330 |
+
color: rgba(255, 255, 255, 0.75);
|
| 331 |
+
}
|
| 332 |
+
.scene-summary strong {
|
| 333 |
+
font-size: 0.9em;
|
| 334 |
+
opacity: 0.95;
|
| 335 |
+
}
|
| 336 |
+
.robot-info {
|
| 337 |
+
padding: 10px;
|
| 338 |
+
background: rgba(139, 127, 239, 0.2);
|
| 339 |
+
border-radius: 6px;
|
| 340 |
+
margin-top: 10px;
|
| 341 |
+
font-size: 0.8em;
|
| 342 |
+
border: 1px solid rgba(139, 127, 239, 0.3);
|
| 343 |
+
}
|
| 344 |
+
.arm-controls { display: none; }
|
| 345 |
+
.arm-controls.active { display: block; }
|
| 346 |
+
.locomotion-controls { display: block; }
|
| 347 |
+
.locomotion-controls.hidden { display: none; }
|
| 348 |
+
.gripper-btns { display: flex; gap: 10px; margin: 10px 0; }
|
| 349 |
+
.gripper-btns button { flex: 1; }
|
| 350 |
+
.target-sliders { margin: 10px 0; }
|
| 351 |
+
.target-sliders .slider-row { margin-bottom: 8px; }
|
| 352 |
+
.target-sliders label { width: 20px; display: inline-block; }
|
| 353 |
+
.mode-toggle { display: flex; gap: 5px; margin-bottom: 15px; }
|
| 354 |
+
.mode-toggle button {
|
| 355 |
+
flex: 1; padding: 8px;
|
| 356 |
+
background: rgba(188, 190, 236, 0.1);
|
| 357 |
+
border: 1px solid rgba(188, 190, 236, 0.2);
|
| 358 |
+
}
|
| 359 |
+
.mode-toggle button.active {
|
| 360 |
+
background: rgba(139, 127, 239, 0.5);
|
| 361 |
+
border-color: rgba(139, 127, 239, 0.7);
|
| 362 |
+
}
|
| 363 |
+
.ik-controls, .joint-controls { display: none; }
|
| 364 |
+
.ik-controls.active, .joint-controls.active { display: block; }
|
| 365 |
+
.joint-sliders .slider-row { margin-bottom: 6px; }
|
| 366 |
+
.joint-sliders label { width: 40px; display: inline-block; font-size: 0.75em; }
|
| 367 |
+
|
| 368 |
+
/* Jogging controls */
|
| 369 |
+
.jog-controls { margin: 10px 0; }
|
| 370 |
+
.jog-row {
|
| 371 |
+
display: flex;
|
| 372 |
+
align-items: center;
|
| 373 |
+
gap: 8px;
|
| 374 |
+
margin-bottom: 8px;
|
| 375 |
+
}
|
| 376 |
+
.jog-row label {
|
| 377 |
+
width: 30px;
|
| 378 |
+
display: inline-block;
|
| 379 |
+
font-size: 0.85em;
|
| 380 |
+
text-align: left;
|
| 381 |
+
}
|
| 382 |
+
.jog-btn {
|
| 383 |
+
width: 40px;
|
| 384 |
+
height: 40px;
|
| 385 |
+
padding: 0;
|
| 386 |
+
font-size: 1.5em;
|
| 387 |
+
font-weight: bold;
|
| 388 |
+
background: rgba(188, 190, 236, 0.15);
|
| 389 |
+
color: var(--wb-secondary);
|
| 390 |
+
border: 1px solid rgba(188, 190, 236, 0.3);
|
| 391 |
+
border-radius: 6px;
|
| 392 |
+
cursor: pointer;
|
| 393 |
+
transition: all 0.15s;
|
| 394 |
+
user-select: none;
|
| 395 |
+
-webkit-user-select: none;
|
| 396 |
+
flex-shrink: 0;
|
| 397 |
+
}
|
| 398 |
+
.jog-btn:hover {
|
| 399 |
+
background: rgba(139, 127, 239, 0.3);
|
| 400 |
+
border-color: rgba(139, 127, 239, 0.5);
|
| 401 |
+
transform: scale(1.05);
|
| 402 |
+
}
|
| 403 |
+
.jog-btn:active {
|
| 404 |
+
background: rgba(139, 127, 239, 0.5);
|
| 405 |
+
border-color: rgba(139, 127, 239, 0.7);
|
| 406 |
+
transform: scale(0.95);
|
| 407 |
+
}
|
| 408 |
+
.jog-row .val-display {
|
| 409 |
+
flex-grow: 1;
|
| 410 |
+
width: auto;
|
| 411 |
+
text-align: center;
|
| 412 |
+
font-family: monospace;
|
| 413 |
+
font-size: 0.9em;
|
| 414 |
+
color: #fff;
|
| 415 |
+
}
|
frontend/src/styles.css.tmp
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<style>
|
| 2 |
+
/* Wandelbots Corporate Design Colors:
|
| 3 |
+
Primary Dark: #01040f (Blue Charcoal)
|
| 4 |
+
Light/Secondary: #bcbeec (Spindle - lavender)
|
| 5 |
+
Accent: #211c44 (Port Gore - deep purple)
|
| 6 |
+
Logo Dark: #181838
|
| 7 |
+
*/
|
| 8 |
+
:root {
|
| 9 |
+
--wb-primary: #01040f;
|
| 10 |
+
--wb-secondary: #bcbeec;
|
| 11 |
+
--wb-accent: #211c44;
|
| 12 |
+
--wb-logo: #181838;
|
| 13 |
+
--wb-highlight: #8b7fef;
|
| 14 |
+
--wb-success: #7c6bef;
|
| 15 |
+
--wb-danger: #ef6b6b;
|
| 16 |
+
}
|
| 17 |
+
body, html {
|
| 18 |
+
margin: 0; padding: 0; width: 100%; height: 100%;
|
| 19 |
+
overflow: hidden; background: var(--wb-primary);
|
| 20 |
+
font-family: Arial, Helvetica, sans-serif;
|
| 21 |
+
user-select: none;
|
| 22 |
+
}
|
| 23 |
+
.video-container {
|
| 24 |
+
position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
|
| 25 |
+
cursor: grab;
|
| 26 |
+
}
|
| 27 |
+
.video-container:active { cursor: grabbing; }
|
| 28 |
+
.video-container img { width: 100%; height: 100%; object-fit: cover; }
|
| 29 |
+
.overlay {
|
| 30 |
+
position: absolute; top: 20px; left: 20px;
|
| 31 |
+
background: rgba(33, 28, 68, 0.85);
|
| 32 |
+
backdrop-filter: blur(15px);
|
| 33 |
+
padding: 25px; border-radius: 12px;
|
| 34 |
+
box-shadow: 0 8px 32px rgba(1, 4, 15, 0.6);
|
| 35 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
|
| 36 |
+
z-index: 100; width: 320px;
|
| 37 |
+
max-height: calc(100vh - 40px);
|
| 38 |
+
overflow-y: auto;
|
| 39 |
+
box-sizing: border-box;
|
| 40 |
+
}
|
| 41 |
+
.overlay h2 { margin: 0 0 15px 0; font-size: 1.1em; font-weight: 600; letter-spacing: 0.5px; color: #fff; }
|
| 42 |
+
.control-group { margin-bottom: 15px; }
|
| 43 |
+
.control-group label { display: block; margin-bottom: 5px; font-size: 0.8em; opacity: 0.8; color: var(--wb-secondary); }
|
| 44 |
+
.slider-row { display: flex; align-items: center; gap: 12px; }
|
| 45 |
+
input[type=range] {
|
| 46 |
+
flex-grow: 1; cursor: pointer;
|
| 47 |
+
accent-color: var(--wb-highlight);
|
| 48 |
+
}
|
| 49 |
+
.val-display { width: 60px; font-family: monospace; font-size: 0.9em; text-align: right; color: #fff; }
|
| 50 |
+
button {
|
| 51 |
+
flex: 1; padding: 10px;
|
| 52 |
+
background: rgba(188, 190, 236, 0.1);
|
| 53 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.25);
|
| 54 |
+
border-radius: 6px; cursor: pointer; transition: all 0.2s;
|
| 55 |
+
font-size: 0.85em;
|
| 56 |
+
}
|
| 57 |
+
button:hover { background: rgba(188, 190, 236, 0.2); border-color: rgba(188, 190, 236, 0.4); }
|
| 58 |
+
button.danger { background: rgba(239, 107, 107, 0.4); border-color: rgba(239, 107, 107, 0.5); }
|
| 59 |
+
button.danger:hover { background: rgba(239, 107, 107, 0.6); }
|
| 60 |
+
.stats { margin-top: 15px; font-size: 0.8em; opacity: 0.9; line-height: 1.6; color: var(--wb-secondary); }
|
| 61 |
+
.hint { position: absolute; bottom: 20px; right: 20px; color: rgba(188, 190, 236, 0.5); font-size: 0.8em; pointer-events: none; text-align: right; }
|
| 62 |
+
.rl-notifications {
|
| 63 |
+
position: absolute;
|
| 64 |
+
right: 24px;
|
| 65 |
+
bottom: 24px;
|
| 66 |
+
display: flex;
|
| 67 |
+
flex-direction: column;
|
| 68 |
+
gap: 8px;
|
| 69 |
+
z-index: 250;
|
| 70 |
+
}
|
| 71 |
+
.rl-notifications .notification {
|
| 72 |
+
min-width: 200px;
|
| 73 |
+
padding: 10px 14px;
|
| 74 |
+
background: rgba(9, 8, 29, 0.95);
|
| 75 |
+
border-radius: 12px;
|
| 76 |
+
border: 1px solid rgba(188, 190, 236, 0.25);
|
| 77 |
+
font-size: 0.75em;
|
| 78 |
+
color: #fff;
|
| 79 |
+
box-shadow: 0 10px 24px rgba(1, 4, 15, 0.5);
|
| 80 |
+
animation: toast-pop 0.35s ease;
|
| 81 |
+
}
|
| 82 |
+
@keyframes toast-pop {
|
| 83 |
+
from { transform: translateY(10px); opacity: 0; }
|
| 84 |
+
to { transform: translateY(0); opacity: 1; }
|
| 85 |
+
}
|
| 86 |
+
.overlay-tiles {
|
| 87 |
+
position: absolute;
|
| 88 |
+
right: 20px;
|
| 89 |
+
bottom: 50px;
|
| 90 |
+
top: auto;
|
| 91 |
+
width: 240px;
|
| 92 |
+
display: none;
|
| 93 |
+
flex-direction: column;
|
| 94 |
+
gap: 10px;
|
| 95 |
+
z-index: 200;
|
| 96 |
+
pointer-events: none;
|
| 97 |
+
}
|
| 98 |
+
.overlay-tile {
|
| 99 |
+
width: 100%;
|
| 100 |
+
height: 140px;
|
| 101 |
+
border-radius: 8px;
|
| 102 |
+
overflow: hidden;
|
| 103 |
+
background: rgba(0, 0, 0, 0.35);
|
| 104 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 105 |
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
| 106 |
+
display: flex;
|
| 107 |
+
flex-direction: column;
|
| 108 |
+
}
|
| 109 |
+
.overlay-tile img {
|
| 110 |
+
width: 100%;
|
| 111 |
+
height: 100%;
|
| 112 |
+
object-fit: cover;
|
| 113 |
+
filter: saturate(1.1);
|
| 114 |
+
}
|
| 115 |
+
.overlay-label {
|
| 116 |
+
position: absolute;
|
| 117 |
+
bottom: 4px;
|
| 118 |
+
left: 6px;
|
| 119 |
+
right: 6px;
|
| 120 |
+
font-size: 0.65em;
|
| 121 |
+
text-transform: uppercase;
|
| 122 |
+
letter-spacing: 0.2em;
|
| 123 |
+
color: rgba(255, 255, 255, 0.9);
|
| 124 |
+
text-shadow: 0 0 6px rgba(0, 0, 0, 0.6);
|
| 125 |
+
}
|
| 126 |
+
.control-panel-info {
|
| 127 |
+
margin-top: 12px;
|
| 128 |
+
padding: 10px;
|
| 129 |
+
border: 1px solid rgba(188, 190, 236, 0.2);
|
| 130 |
+
border-radius: 8px;
|
| 131 |
+
background: rgba(188, 190, 236, 0.05);
|
| 132 |
+
font-size: 0.8em;
|
| 133 |
+
line-height: 1.4;
|
| 134 |
+
}
|
| 135 |
+
.control-panel-info ul {
|
| 136 |
+
margin: 6px 0 0 10px;
|
| 137 |
+
padding: 0;
|
| 138 |
+
display: flex;
|
| 139 |
+
flex-direction: column;
|
| 140 |
+
gap: 6px;
|
| 141 |
+
}
|
| 142 |
+
.control-panel-info li {
|
| 143 |
+
list-style: none;
|
| 144 |
+
}
|
| 145 |
+
.hint-key {
|
| 146 |
+
display: flex;
|
| 147 |
+
align-items: center;
|
| 148 |
+
gap: 6px;
|
| 149 |
+
}
|
| 150 |
+
.hint-key kbd {
|
| 151 |
+
padding: 2px 6px;
|
| 152 |
+
border-radius: 4px;
|
| 153 |
+
background: rgba(139, 127, 239, 0.35);
|
| 154 |
+
border: 1px solid rgba(139, 127, 239, 0.55);
|
| 155 |
+
font-size: 0.75em;
|
| 156 |
+
color: #fff;
|
| 157 |
+
font-weight: 600;
|
| 158 |
+
}
|
| 159 |
+
/* State info panel - top right */
|
| 160 |
+
.state-panel {
|
| 161 |
+
position: absolute; top: 20px; right: 20px;
|
| 162 |
+
background: rgba(33, 28, 68, 0.85);
|
| 163 |
+
backdrop-filter: blur(12px);
|
| 164 |
+
padding: 8px 12px; border-radius: 8px;
|
| 165 |
+
box-shadow: 0 3px 14px rgba(1, 4, 15, 0.35);
|
| 166 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
|
| 167 |
+
z-index: 100; min-width: 180px; max-width: 220px;
|
| 168 |
+
font-size: 0.75em; line-height: 1.3;
|
| 169 |
+
}
|
| 170 |
+
.state-panel strong { color: #fff; }
|
| 171 |
+
/* Camera controls - bottom left */
|
| 172 |
+
.camera-panel {
|
| 173 |
+
position: absolute; bottom: 20px; left: 20px;
|
| 174 |
+
background: rgba(33, 28, 68, 0.85);
|
| 175 |
+
backdrop-filter: blur(15px);
|
| 176 |
+
padding: 15px 20px; border-radius: 10px;
|
| 177 |
+
box-shadow: 0 4px 20px rgba(1, 4, 15, 0.5);
|
| 178 |
+
color: var(--wb-secondary); border: 1px solid rgba(188, 190, 236, 0.15);
|
| 179 |
+
z-index: 100; width: 280px;
|
| 180 |
+
}
|
| 181 |
+
.camera-panel .control-group { margin-bottom: 10px; }
|
| 182 |
+
.camera-panel .control-group:last-child { margin-bottom: 0; }
|
| 183 |
+
.state-hint {
|
| 184 |
+
margin-top: 12px;
|
| 185 |
+
font-size: 0.75em;
|
| 186 |
+
color: rgba(255, 255, 255, 0.7);
|
| 187 |
+
line-height: 1.4;
|
| 188 |
+
}
|
| 189 |
+
.camera-panel.inside-panel {
|
| 190 |
+
position: relative;
|
| 191 |
+
bottom: auto;
|
| 192 |
+
left: auto;
|
| 193 |
+
background: rgba(33, 28, 68, 0.75);
|
| 194 |
+
border: 1px solid rgba(188, 190, 236, 0.2);
|
| 195 |
+
box-shadow: none;
|
| 196 |
+
margin-top: 16px;
|
| 197 |
+
}
|
| 198 |
+
/* Collapsible panel header - title is the button */
|
| 199 |
+
.panel-header {
|
| 200 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 201 |
+
cursor: pointer; margin-bottom: 15px;
|
| 202 |
+
padding: 4px 0; border-radius: 4px; transition: all 0.2s;
|
| 203 |
+
}
|
| 204 |
+
.panel-header:hover { background: rgba(188, 190, 236, 0.1); }
|
| 205 |
+
.panel-header h2 {
|
| 206 |
+
margin: 0; display: flex; align-items: center; gap: 8px;
|
| 207 |
+
}
|
| 208 |
+
.panel-header h2::after {
|
| 209 |
+
content: '−'; font-weight: 300; font-size: 1.2em;
|
| 210 |
+
opacity: 0.6; transition: transform 0.2s;
|
| 211 |
+
}
|
| 212 |
+
.panel-header.collapsed h2::after { content: '+'; }
|
| 213 |
+
.panel-content { transition: all 0.3s ease; overflow: hidden; }
|
| 214 |
+
.panel-content.collapsed { max-height: 0; opacity: 0; margin: 0; padding: 0; }
|
| 215 |
+
.overlay.collapsed { width: auto; min-width: 200px; }
|
| 216 |
+
.rl-buttons { display: flex; flex-direction: column; gap: 5px; margin: 10px 0; }
|
| 217 |
+
.rl-row { display: flex; justify-content: center; gap: 5px; }
|
| 218 |
+
.rl-btn {
|
| 219 |
+
padding: 12px 16px; min-width: 80px;
|
| 220 |
+
background: rgba(124, 107, 239, 0.4); border: 1px solid rgba(124, 107, 239, 0.5);
|
| 221 |
+
color: #fff; border-radius: 6px; cursor: pointer; transition: all 0.15s;
|
| 222 |
+
font-size: 0.85em; font-weight: 500;
|
| 223 |
+
}
|
| 224 |
+
.rl-btn:hover { background: rgba(124, 107, 239, 0.6); }
|
| 225 |
+
.rl-btn:active { background: rgba(124, 107, 239, 0.8); transform: scale(0.95); }
|
| 226 |
+
.rl-btn.stop { background: rgba(239, 107, 107, 0.4); border-color: rgba(239, 107, 107, 0.5); }
|
| 227 |
+
.rl-btn.stop:hover { background: rgba(239, 107, 107, 0.6); }
|
| 228 |
+
.cmd-display { font-family: monospace; font-size: 0.8em; opacity: 0.8; margin-top: 8px; color: var(--wb-secondary); }
|
| 229 |
+
.connection-status-inline {
|
| 230 |
+
padding: 6px 12px; border-radius: 4px;
|
| 231 |
+
font-size: 0.75em; font-weight: 600;
|
| 232 |
+
background: rgba(124, 107, 239, 0.25);
|
| 233 |
+
color: #fff; border: 1px solid rgba(124, 107, 239, 0.4);
|
| 234 |
+
margin-bottom: 12px;
|
| 235 |
+
text-align: center;
|
| 236 |
+
text-transform: uppercase;
|
| 237 |
+
letter-spacing: 0.5px;
|
| 238 |
+
}
|
| 239 |
+
.connection-status-inline.disconnected {
|
| 240 |
+
background: rgba(239, 107, 107, 0.25);
|
| 241 |
+
border-color: rgba(239, 107, 107, 0.4);
|
| 242 |
+
}
|
| 243 |
+
.connection-status-inline .status-text {
|
| 244 |
+
display: inline-block;
|
| 245 |
+
}
|
| 246 |
+
.connection-status-inline .status-loader {
|
| 247 |
+
display: none;
|
| 248 |
+
width: 12px;
|
| 249 |
+
height: 12px;
|
| 250 |
+
border: 2px solid rgba(255, 255, 255, 0.35);
|
| 251 |
+
border-top-color: #fff;
|
| 252 |
+
border-radius: 50%;
|
| 253 |
+
animation: status-spin 0.8s linear infinite;
|
| 254 |
+
margin-left: 8px;
|
| 255 |
+
}
|
| 256 |
+
.connection-status-inline.connecting {
|
| 257 |
+
background: rgba(139, 127, 239, 0.35);
|
| 258 |
+
border-color: rgba(255, 255, 255, 0.3);
|
| 259 |
+
}
|
| 260 |
+
.connection-status-inline.connecting .status-loader {
|
| 261 |
+
display: inline-block;
|
| 262 |
+
}
|
| 263 |
+
@keyframes status-spin {
|
| 264 |
+
from { transform: rotate(0deg); }
|
| 265 |
+
to { transform: rotate(360deg); }
|
| 266 |
+
}
|
| 267 |
+
.status-card {
|
| 268 |
+
padding: 10px 12px;
|
| 269 |
+
border-radius: 8px;
|
| 270 |
+
border-left: 3px solid var(--wb-success);
|
| 271 |
+
background: rgba(76, 175, 80, 0.15);
|
| 272 |
+
display: flex;
|
| 273 |
+
flex-direction: column;
|
| 274 |
+
gap: 6px;
|
| 275 |
+
margin-top: 10px;
|
| 276 |
+
transition: border-color 0.2s, background 0.2s;
|
| 277 |
+
}
|
| 278 |
+
.status-card strong {
|
| 279 |
+
font-size: 0.8em;
|
| 280 |
+
letter-spacing: 0.2em;
|
| 281 |
+
text-transform: uppercase;
|
| 282 |
+
color: #fff;
|
| 283 |
+
}
|
| 284 |
+
.status-card .status-card-text {
|
| 285 |
+
font-size: 0.75em;
|
| 286 |
+
color: rgba(255, 255, 255, 0.8);
|
| 287 |
+
}
|
| 288 |
+
.status-card.disconnected {
|
| 289 |
+
background: rgba(239, 107, 107, 0.15);
|
| 290 |
+
border-color: rgba(239, 107, 107, 0.7);
|
| 291 |
+
}
|
| 292 |
+
.trainer-status {
|
| 293 |
+
display: flex;
|
| 294 |
+
align-items: center;
|
| 295 |
+
gap: 6px;
|
| 296 |
+
font-size: 0.7em;
|
| 297 |
+
letter-spacing: 0.4px;
|
| 298 |
+
text-transform: uppercase;
|
| 299 |
+
}
|
| 300 |
+
.trainer-status-dot {
|
| 301 |
+
width: 8px;
|
| 302 |
+
height: 8px;
|
| 303 |
+
border-radius: 50%;
|
| 304 |
+
background: rgba(124, 107, 239, 0.6);
|
| 305 |
+
border: 1px solid rgba(255, 255, 255, 0.45);
|
| 306 |
+
}
|
| 307 |
+
.trainer-status.connected .trainer-status-dot {
|
| 308 |
+
background: #53e89b;
|
| 309 |
+
box-shadow: 0 0 6px rgba(83, 232, 155, 0.8);
|
| 310 |
+
}
|
| 311 |
+
.trainer-status.disconnected .trainer-status-dot {
|
| 312 |
+
background: #ef6b6b;
|
| 313 |
+
box-shadow: 0 0 6px rgba(239, 107, 107, 0.8);
|
| 314 |
+
}
|
| 315 |
+
.camera-controls { margin-top: 15px; }
|
| 316 |
+
select {
|
| 317 |
+
width: 100%; padding: 10px;
|
| 318 |
+
background: rgba(188, 190, 236, 0.1);
|
| 319 |
+
color: #fff; border: 1px solid rgba(188, 190, 236, 0.25);
|
| 320 |
+
border-radius: 6px; cursor: pointer;
|
| 321 |
+
font-size: 0.9em;
|
| 322 |
+
}
|
| 323 |
+
select option { background: var(--wb-accent); color: #fff; }
|
| 324 |
+
.robot-selector { margin-bottom: 20px; }
|
| 325 |
+
.scene-summary {
|
| 326 |
+
margin-top: 12px;
|
| 327 |
+
font-size: 0.8em;
|
| 328 |
+
display: flex;
|
| 329 |
+
align-items: baseline;
|
| 330 |
+
gap: 6px;
|
| 331 |
+
color: rgba(255, 255, 255, 0.75);
|
| 332 |
+
}
|
| 333 |
+
.scene-summary strong {
|
| 334 |
+
font-size: 0.9em;
|
| 335 |
+
opacity: 0.95;
|
| 336 |
+
}
|
| 337 |
+
.robot-info {
|
| 338 |
+
padding: 10px;
|
| 339 |
+
background: rgba(139, 127, 239, 0.2);
|
| 340 |
+
border-radius: 6px;
|
| 341 |
+
margin-top: 10px;
|
| 342 |
+
font-size: 0.8em;
|
| 343 |
+
border: 1px solid rgba(139, 127, 239, 0.3);
|
| 344 |
+
}
|
| 345 |
+
.arm-controls { display: none; }
|
| 346 |
+
.arm-controls.active { display: block; }
|
| 347 |
+
.locomotion-controls { display: block; }
|
| 348 |
+
.locomotion-controls.hidden { display: none; }
|
| 349 |
+
.gripper-btns { display: flex; gap: 10px; margin: 10px 0; }
|
| 350 |
+
.gripper-btns button { flex: 1; }
|
| 351 |
+
.target-sliders { margin: 10px 0; }
|
| 352 |
+
.target-sliders .slider-row { margin-bottom: 8px; }
|
| 353 |
+
.target-sliders label { width: 20px; display: inline-block; }
|
| 354 |
+
.mode-toggle { display: flex; gap: 5px; margin-bottom: 15px; }
|
| 355 |
+
.mode-toggle button {
|
| 356 |
+
flex: 1; padding: 8px;
|
| 357 |
+
background: rgba(188, 190, 236, 0.1);
|
| 358 |
+
border: 1px solid rgba(188, 190, 236, 0.2);
|
| 359 |
+
}
|
| 360 |
+
.mode-toggle button.active {
|
| 361 |
+
background: rgba(139, 127, 239, 0.5);
|
| 362 |
+
border-color: rgba(139, 127, 239, 0.7);
|
| 363 |
+
}
|
| 364 |
+
.ik-controls, .joint-controls { display: none; }
|
| 365 |
+
.ik-controls.active, .joint-controls.active { display: block; }
|
| 366 |
+
.joint-sliders .slider-row { margin-bottom: 6px; }
|
| 367 |
+
.joint-sliders label { width: 40px; display: inline-block; font-size: 0.75em; }
|
| 368 |
+
|
| 369 |
+
/* Jogging controls */
|
| 370 |
+
.jog-controls { margin: 10px 0; }
|
| 371 |
+
.jog-row {
|
| 372 |
+
display: flex;
|
| 373 |
+
align-items: center;
|
| 374 |
+
gap: 8px;
|
| 375 |
+
margin-bottom: 8px;
|
| 376 |
+
}
|
| 377 |
+
.jog-row label {
|
| 378 |
+
width: 30px;
|
| 379 |
+
display: inline-block;
|
| 380 |
+
font-size: 0.85em;
|
| 381 |
+
text-align: left;
|
| 382 |
+
}
|
| 383 |
+
.jog-btn {
|
| 384 |
+
width: 40px;
|
| 385 |
+
height: 40px;
|
| 386 |
+
padding: 0;
|
| 387 |
+
font-size: 1.5em;
|
| 388 |
+
font-weight: bold;
|
| 389 |
+
background: rgba(188, 190, 236, 0.15);
|
| 390 |
+
color: var(--wb-secondary);
|
| 391 |
+
border: 1px solid rgba(188, 190, 236, 0.3);
|
| 392 |
+
border-radius: 6px;
|
| 393 |
+
cursor: pointer;
|
| 394 |
+
transition: all 0.15s;
|
| 395 |
+
user-select: none;
|
| 396 |
+
-webkit-user-select: none;
|
| 397 |
+
flex-shrink: 0;
|
| 398 |
+
}
|
| 399 |
+
.jog-btn:hover {
|
| 400 |
+
background: rgba(139, 127, 239, 0.3);
|
| 401 |
+
border-color: rgba(139, 127, 239, 0.5);
|
| 402 |
+
transform: scale(1.05);
|
| 403 |
+
}
|
| 404 |
+
.jog-btn:active {
|
| 405 |
+
background: rgba(139, 127, 239, 0.5);
|
| 406 |
+
border-color: rgba(139, 127, 239, 0.7);
|
| 407 |
+
transform: scale(0.95);
|
| 408 |
+
}
|
| 409 |
+
.jog-row .val-display {
|
| 410 |
+
flex-grow: 1;
|
| 411 |
+
width: auto;
|
| 412 |
+
text-align: center;
|
| 413 |
+
font-family: monospace;
|
| 414 |
+
font-size: 0.9em;
|
| 415 |
+
color: #fff;
|
| 416 |
+
}
|
| 417 |
+
</style>
|
frontend/src/types/protocol.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Protocol type definitions for Nova-Sim WebSocket API.
|
| 3 |
+
*
|
| 4 |
+
* TypeScript equivalents of Python protocol_types.py
|
| 5 |
+
*/
|
| 6 |
+
// Helper to check if observation is UR5-specific
|
| 7 |
+
export function isUR5Observation(obs) {
|
| 8 |
+
return 'end_effector' in obs;
|
| 9 |
+
}
|
| 10 |
+
// Helper to check if robot is arm type
|
| 11 |
+
export function isArmRobot(robot) {
|
| 12 |
+
return robot === 'ur5' || robot === 'ur5_t_push';
|
| 13 |
+
}
|
frontend/src/types/protocol.ts
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Protocol type definitions for Nova-Sim WebSocket API.
|
| 3 |
+
*
|
| 4 |
+
* TypeScript equivalents of Python protocol_types.py
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
// ============================================================================
|
| 8 |
+
// Common Types
|
| 9 |
+
// ============================================================================
|
| 10 |
+
|
| 11 |
+
export type RobotType = 'g1' | 'spot' | 'ur5' | 'ur5_t_push';
|
| 12 |
+
export type SceneType = string | null;
|
| 13 |
+
export type ControlMode = 'ik' | 'joint';
|
| 14 |
+
|
| 15 |
+
// ============================================================================
|
| 16 |
+
// Action Messages (Client -> Server)
|
| 17 |
+
// ============================================================================
|
| 18 |
+
|
| 19 |
+
export interface ActionData {
|
| 20 |
+
// Cartesian translation velocities (m/s for UR5, normalized for locomotion)
|
| 21 |
+
vx?: number;
|
| 22 |
+
vy?: number;
|
| 23 |
+
vz?: number;
|
| 24 |
+
|
| 25 |
+
// Rotation velocity (rad/s for UR5, normalized for locomotion)
|
| 26 |
+
vyaw?: number;
|
| 27 |
+
|
| 28 |
+
// Cartesian rotation velocities (rad/s, UR5 only)
|
| 29 |
+
vrx?: number;
|
| 30 |
+
vry?: number;
|
| 31 |
+
vrz?: number;
|
| 32 |
+
|
| 33 |
+
// Joint velocities (rad/s, UR5 only)
|
| 34 |
+
j1?: number;
|
| 35 |
+
j2?: number;
|
| 36 |
+
j3?: number;
|
| 37 |
+
j4?: number;
|
| 38 |
+
j5?: number;
|
| 39 |
+
j6?: number;
|
| 40 |
+
|
| 41 |
+
// Gripper position (0=open, 255=closed, UR5 only)
|
| 42 |
+
gripper?: number;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
export interface ActionMessage {
|
| 46 |
+
type: 'action';
|
| 47 |
+
data: ActionData;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export interface TeleopActionData {
|
| 51 |
+
vx?: number;
|
| 52 |
+
vy?: number;
|
| 53 |
+
vz?: number;
|
| 54 |
+
// Backward compatibility: accept old dx/dy/dz format
|
| 55 |
+
dx?: number;
|
| 56 |
+
dy?: number;
|
| 57 |
+
dz?: number;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export interface TeleopActionMessage {
|
| 61 |
+
type: 'teleop_action';
|
| 62 |
+
data: TeleopActionData;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ============================================================================
|
| 66 |
+
// Other Client -> Server Messages
|
| 67 |
+
// ============================================================================
|
| 68 |
+
|
| 69 |
+
export interface ResetData {
|
| 70 |
+
seed?: number | null;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
export interface ResetMessage {
|
| 74 |
+
type: 'reset';
|
| 75 |
+
data?: ResetData;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export interface SwitchRobotData {
|
| 79 |
+
robot: RobotType;
|
| 80 |
+
scene?: string | null;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
export interface SwitchRobotMessage {
|
| 84 |
+
type: 'switch_robot';
|
| 85 |
+
data: SwitchRobotData;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
export interface HomeMessage {
|
| 89 |
+
type: 'home';
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export interface StopHomeMessage {
|
| 93 |
+
type: 'stop_home';
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
export interface CameraRotateData {
|
| 97 |
+
action: 'rotate';
|
| 98 |
+
dx: number;
|
| 99 |
+
dy: number;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
export interface CameraZoomData {
|
| 103 |
+
action: 'zoom';
|
| 104 |
+
dz: number;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
export interface CameraPanData {
|
| 108 |
+
action: 'pan';
|
| 109 |
+
dx: number;
|
| 110 |
+
dy: number;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
export interface CameraSetDistanceData {
|
| 114 |
+
action: 'set_distance';
|
| 115 |
+
distance: number;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export type CameraData = CameraRotateData | CameraZoomData | CameraPanData | CameraSetDistanceData;
|
| 119 |
+
|
| 120 |
+
export interface CameraMessage {
|
| 121 |
+
type: 'camera';
|
| 122 |
+
data: CameraData;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
export interface CameraFollowData {
|
| 126 |
+
follow: boolean;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
export interface CameraFollowMessage {
|
| 130 |
+
type: 'camera_follow';
|
| 131 |
+
data: CameraFollowData;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// ============================================================================
|
| 135 |
+
// UR5-Specific Client -> Server Messages
|
| 136 |
+
// ============================================================================
|
| 137 |
+
|
| 138 |
+
export interface ArmTargetData {
|
| 139 |
+
x: number;
|
| 140 |
+
y: number;
|
| 141 |
+
z: number;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
export interface ArmTargetMessage {
|
| 145 |
+
type: 'arm_target';
|
| 146 |
+
data: ArmTargetData;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
export interface ArmOrientationData {
|
| 150 |
+
roll: number;
|
| 151 |
+
pitch: number;
|
| 152 |
+
yaw: number;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
export interface ArmOrientationMessage {
|
| 156 |
+
type: 'arm_orientation';
|
| 157 |
+
data: ArmOrientationData;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
export interface UseOrientationData {
|
| 161 |
+
enabled: boolean;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
export interface UseOrientationMessage {
|
| 165 |
+
type: 'use_orientation';
|
| 166 |
+
data: UseOrientationData;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
export interface JointPositionsData {
|
| 170 |
+
positions: number[]; // Array of 6 joint angles in radians
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
export interface JointPositionsMessage {
|
| 174 |
+
type: 'joint_positions';
|
| 175 |
+
data: JointPositionsData;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
export interface ControlModeData {
|
| 179 |
+
mode: ControlMode;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
export interface ControlModeMessage {
|
| 183 |
+
type: 'control_mode';
|
| 184 |
+
data: ControlModeData;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
export interface GripperData {
|
| 188 |
+
action?: 'open' | 'close';
|
| 189 |
+
value?: number; // 0-255
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
export interface GripperMessage {
|
| 193 |
+
type: 'gripper';
|
| 194 |
+
data: GripperData;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
export interface SetNovaModeData {
|
| 198 |
+
enabled?: boolean; // Legacy: enable/disable all Nova features
|
| 199 |
+
state_streaming?: boolean;
|
| 200 |
+
ik?: boolean;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
export interface SetNovaModeMessage {
|
| 204 |
+
type: 'set_nova_mode';
|
| 205 |
+
data: SetNovaModeData;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// Jogging messages
|
| 209 |
+
export interface JogParams {
|
| 210 |
+
joint?: number; // 1-6
|
| 211 |
+
axis?: 'x' | 'y' | 'z';
|
| 212 |
+
direction?: '+' | '-';
|
| 213 |
+
velocity?: number;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
export interface StartJogData {
|
| 217 |
+
jog_type: 'joint' | 'cartesian_translation' | 'cartesian_rotation';
|
| 218 |
+
params: JogParams;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
export interface StartJogMessage {
|
| 222 |
+
type: 'start_jog';
|
| 223 |
+
data: StartJogData;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
export interface StopJogMessage {
|
| 227 |
+
type: 'stop_jog';
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
// ============================================================================
|
| 231 |
+
// Trainer Messages (Client -> Server)
|
| 232 |
+
// ============================================================================
|
| 233 |
+
|
| 234 |
+
export interface TrainerIdentityData {
|
| 235 |
+
trainer_id: string;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
export interface TrainerIdentityMessage {
|
| 239 |
+
type: 'trainer_identity';
|
| 240 |
+
data: TrainerIdentityData;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
export interface TrainerNotificationData {
|
| 244 |
+
message?: string;
|
| 245 |
+
level?: 'info' | 'warning' | 'error';
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
export interface TrainerNotificationMessage {
|
| 249 |
+
type: 'notification';
|
| 250 |
+
data: TrainerNotificationData;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
export interface EpisodeControlData {
|
| 254 |
+
action: 'terminate' | 'truncate';
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
export interface EpisodeControlMessage {
|
| 258 |
+
type: 'episode_control';
|
| 259 |
+
data: EpisodeControlData;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// ============================================================================
|
| 263 |
+
// Server -> Client Messages
|
| 264 |
+
// ============================================================================
|
| 265 |
+
|
| 266 |
+
export interface Position {
|
| 267 |
+
x: number;
|
| 268 |
+
y: number;
|
| 269 |
+
z: number;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
export interface Quaternion {
|
| 273 |
+
w: number;
|
| 274 |
+
x: number;
|
| 275 |
+
y: number;
|
| 276 |
+
z: number;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
export interface EulerAngles {
|
| 280 |
+
roll: number;
|
| 281 |
+
pitch: number;
|
| 282 |
+
yaw: number;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
export interface LocomotionObservation {
|
| 286 |
+
position: Position;
|
| 287 |
+
orientation: Quaternion;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
export interface UR5Observation {
|
| 291 |
+
end_effector: Position;
|
| 292 |
+
ee_orientation: Quaternion;
|
| 293 |
+
ee_target: Position;
|
| 294 |
+
ee_target_orientation: EulerAngles;
|
| 295 |
+
gripper: number; // 0-255
|
| 296 |
+
joint_positions: number[]; // 6 joint angles
|
| 297 |
+
joint_targets: number[]; // 6 target joint angles
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
export type Observation = LocomotionObservation | UR5Observation;
|
| 301 |
+
|
| 302 |
+
export interface NovaApiStatus {
|
| 303 |
+
connected: boolean;
|
| 304 |
+
state_streaming: boolean;
|
| 305 |
+
ik: boolean;
|
| 306 |
+
available?: boolean;
|
| 307 |
+
enabled?: boolean;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
export interface StateData {
|
| 311 |
+
robot: RobotType;
|
| 312 |
+
scene: string | null;
|
| 313 |
+
observation: Observation;
|
| 314 |
+
steps: number;
|
| 315 |
+
reward?: number;
|
| 316 |
+
teleop_action: ActionData;
|
| 317 |
+
trainer_connected: boolean;
|
| 318 |
+
|
| 319 |
+
// UR5-specific fields
|
| 320 |
+
control_mode?: ControlMode;
|
| 321 |
+
nova_api?: NovaApiStatus;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
export interface StateMessage {
|
| 325 |
+
type: 'state';
|
| 326 |
+
data: StateData;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
export interface TrainerStatusData {
|
| 330 |
+
connected: boolean;
|
| 331 |
+
trainer_count?: number;
|
| 332 |
+
identities?: string[];
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
export interface TrainerStatusMessage {
|
| 336 |
+
type: 'trainer_status';
|
| 337 |
+
data: TrainerStatusData;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
export interface TrainerNotificationBroadcast {
|
| 341 |
+
type: 'trainer_notification';
|
| 342 |
+
data: TrainerNotificationData;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// ============================================================================
|
| 346 |
+
// HTTP Response Types
|
| 347 |
+
// ============================================================================
|
| 348 |
+
|
| 349 |
+
export interface CameraFeed {
|
| 350 |
+
name: string;
|
| 351 |
+
label: string;
|
| 352 |
+
url?: string;
|
| 353 |
+
description?: string;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
export interface EnvResponse {
|
| 357 |
+
robot: RobotType;
|
| 358 |
+
scene?: string;
|
| 359 |
+
has_gripper?: boolean;
|
| 360 |
+
control_mode?: ControlMode;
|
| 361 |
+
action_space?: any;
|
| 362 |
+
observation_space?: any;
|
| 363 |
+
camera_feeds?: CameraFeed[];
|
| 364 |
+
home_pose?: number[];
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
export interface CommandInfo {
|
| 368 |
+
name: string;
|
| 369 |
+
description: string;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
export interface MetadataResponse {
|
| 373 |
+
robots: string[];
|
| 374 |
+
commands: CommandInfo[];
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// ============================================================================
|
| 378 |
+
// Union Types for All Messages
|
| 379 |
+
// ============================================================================
|
| 380 |
+
|
| 381 |
+
export type ClientMessage =
|
| 382 |
+
| ActionMessage
|
| 383 |
+
| TeleopActionMessage
|
| 384 |
+
| ResetMessage
|
| 385 |
+
| SwitchRobotMessage
|
| 386 |
+
| HomeMessage
|
| 387 |
+
| StopHomeMessage
|
| 388 |
+
| CameraMessage
|
| 389 |
+
| CameraFollowMessage
|
| 390 |
+
| ArmTargetMessage
|
| 391 |
+
| ArmOrientationMessage
|
| 392 |
+
| UseOrientationMessage
|
| 393 |
+
| JointPositionsMessage
|
| 394 |
+
| ControlModeMessage
|
| 395 |
+
| GripperMessage
|
| 396 |
+
| SetNovaModeMessage
|
| 397 |
+
| StartJogMessage
|
| 398 |
+
| StopJogMessage
|
| 399 |
+
| TrainerIdentityMessage
|
| 400 |
+
| TrainerNotificationMessage
|
| 401 |
+
| EpisodeControlMessage;
|
| 402 |
+
|
| 403 |
+
export type ServerMessage =
|
| 404 |
+
| StateMessage
|
| 405 |
+
| TrainerStatusMessage
|
| 406 |
+
| TrainerNotificationBroadcast;
|
| 407 |
+
|
| 408 |
+
// Helper to check if observation is UR5-specific
|
| 409 |
+
export function isUR5Observation(obs: Observation): obs is UR5Observation {
|
| 410 |
+
return 'end_effector' in obs;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
// Helper to check if robot is arm type
|
| 414 |
+
export function isArmRobot(robot: RobotType): boolean {
|
| 415 |
+
return robot === 'ur5' || robot === 'ur5_t_push';
|
| 416 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"module": "ESNext",
|
| 5 |
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
| 6 |
+
"moduleResolution": "bundler",
|
| 7 |
+
"strict": false,
|
| 8 |
+
"esModuleInterop": true,
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
"forceConsistentCasingInFileNames": true,
|
| 11 |
+
"resolveJsonModule": true,
|
| 12 |
+
"isolatedModules": true,
|
| 13 |
+
"noUnusedLocals": false,
|
| 14 |
+
"noUnusedParameters": false,
|
| 15 |
+
"noImplicitReturns": false,
|
| 16 |
+
"noImplicitAny": false,
|
| 17 |
+
"strictNullChecks": false
|
| 18 |
+
},
|
| 19 |
+
"include": ["src/**/*"],
|
| 20 |
+
"exclude": ["node_modules", "dist"]
|
| 21 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
|
| 3 |
+
export default defineConfig({
|
| 4 |
+
root: 'src',
|
| 5 |
+
base: '/nova-sim/',
|
| 6 |
+
build: {
|
| 7 |
+
outDir: '../dist',
|
| 8 |
+
emptyOutDir: true,
|
| 9 |
+
},
|
| 10 |
+
server: {
|
| 11 |
+
port: 3004,
|
| 12 |
+
strictPort: true,
|
| 13 |
+
proxy: {
|
| 14 |
+
'^/nova-sim/api/.*': {
|
| 15 |
+
target: 'http://localhost:5001',
|
| 16 |
+
changeOrigin: true,
|
| 17 |
+
ws: true,
|
| 18 |
+
rewrite: (path) => path,
|
| 19 |
+
},
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
appType: 'spa',
|
| 23 |
+
})
|
server.log
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Loaded RL policy from /Users/georgpuschel/repos/robot-ml/nova-sim/robots/g1/policy/motion.pt
|
| 2 |
+
Starting simulation loop...
|
| 3 |
+
* Serving Flask app 'mujoco_server'
|
| 4 |
+
* Debug mode: off
|
| 5 |
+
[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
| 6 |
+
* Running on all addresses (0.0.0.0)
|
| 7 |
+
* Running on http://127.0.0.1:3004
|
| 8 |
+
* Running on http://172.31.10.66:3004
|
| 9 |
+
[33mPress CTRL+C to quit[0m
|
| 10 |
+
127.0.0.1 - - [21/Jan/2026 12:31:13] "GET /nova-sim/api/v1 HTTP/1.1" 200 -
|
| 11 |
+
127.0.0.1 - - [21/Jan/2026 12:31:13] "GET /nova-sim/api/v1/video_feed HTTP/1.1" 200 -
|
| 12 |
+
127.0.0.1 - - [21/Jan/2026 12:31:13] "GET /nova-sim/api/v1/metadata HTTP/1.1" 200 -
|
| 13 |
+
127.0.0.1 - - [21/Jan/2026 12:31:13] "GET /nova-sim/api/v1/env HTTP/1.1" 200 -
|
| 14 |
+
127.0.0.1 - - [21/Jan/2026 12:31:13] "GET /nova-sim/api/v1/video_feed?ts=1768995073211 HTTP/1.1" 200 -
|
| 15 |
+
127.0.0.1 - - [21/Jan/2026 12:31:13] "GET /nova-sim/api/v1/env HTTP/1.1" 200 -
|
| 16 |
+
127.0.0.1 - - [21/Jan/2026 12:31:13] "GET /nova-sim/api/v1/env HTTP/1.1" 200 -
|
| 17 |
+
WebSocket client connected
|
| 18 |
+
[WS] Received message type: camera
|
| 19 |
+
[WS] Received message type: start_jog
|
| 20 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 21 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 22 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 23 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 24 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 25 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 26 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 27 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 28 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 29 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 30 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 31 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 32 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 33 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 34 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 35 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 36 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 37 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 38 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 39 |
+
[WS] Received message type: camera
|
| 40 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 41 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 42 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 43 |
+
[WS] Received message type: stop_jog
|
| 44 |
+
[WS] Received message type: camera
|
| 45 |
+
[WS] Received message type: camera
|
| 46 |
+
[WS] Received message type: camera
|
| 47 |
+
[WS] Received message type: camera
|
| 48 |
+
[WS] Received message type: camera
|
| 49 |
+
[WS] Received message type: camera
|
| 50 |
+
[WS] Received message type: camera
|
| 51 |
+
[WS] Received message type: camera
|
| 52 |
+
[WS] Received message type: camera
|
| 53 |
+
[WS] Received message type: camera
|
| 54 |
+
[WS] Received message type: camera
|
| 55 |
+
[WS] Received message type: camera
|
| 56 |
+
[WS] Received message type: camera
|
| 57 |
+
[WS] Received message type: camera
|
| 58 |
+
[WS] Received message type: camera
|
| 59 |
+
[WS] Received message type: camera
|
| 60 |
+
[WS] Received message type: camera
|
| 61 |
+
[WS] Received message type: camera
|
| 62 |
+
[WS] Received message type: camera
|
| 63 |
+
[WS] Received message type: camera
|
| 64 |
+
[WS] Received message type: camera
|
| 65 |
+
[WS] Received message type: camera
|
| 66 |
+
[WS] Received message type: camera
|
| 67 |
+
[WS] Received message type: camera
|
| 68 |
+
[WS] Received message type: camera
|
| 69 |
+
[WS] Received message type: camera
|
| 70 |
+
[WS] Received message type: camera
|
| 71 |
+
[WS] Received message type: camera
|
| 72 |
+
[WS] Received message type: camera
|
| 73 |
+
[WS] Received message type: camera
|
| 74 |
+
[WS] Received message type: start_jog
|
| 75 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 76 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 77 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 78 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 79 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 80 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 81 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 82 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 83 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 84 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 85 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 86 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 87 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 88 |
+
[WS] Received message type: stop_jog
|
| 89 |
+
[WS] Received message type: start_jog
|
| 90 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 91 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 92 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 93 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 94 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 95 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 96 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 97 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 98 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 99 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 100 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 101 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 102 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 103 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 104 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 105 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 106 |
+
[WS] Received message type: stop_jog
|
| 107 |
+
[WS] Received message type: start_jog
|
| 108 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 109 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 110 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 111 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 112 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 113 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 114 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 115 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 116 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 117 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 118 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 119 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 120 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 121 |
+
[WS] Received message type: stop_jog
|
| 122 |
+
[WS] Received message type: start_jog
|
| 123 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 124 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.05}
|
| 125 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.05}
|
| 126 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 127 |
+
[WS] Received message type: stop_jog
|
| 128 |
+
[WS] Received message type: start_jog
|
| 129 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 130 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 131 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 132 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 133 |
+
[WS] Received message type: stop_jog
|
| 134 |
+
[WS] Received message type: switch_robot
|
| 135 |
+
Robot switch requested: ur5 / scene: scene_with_gripper
|
| 136 |
+
Switching to robot: ur5
|
| 137 |
+
Nova API credentials detected - attempting bidirectional control
|
| 138 |
+
[Nova] Configuring Nova API with config: {'use_state_stream': True, 'use_ik': False, 'use_jogging': True}
|
| 139 |
+
[Nova] use_state_stream=True, use_ik=False, use_jogging=True
|
| 140 |
+
[Nova] Attempting to load config from environment variables...
|
| 141 |
+
[Nova] Config loaded from env: https://vaxtlucd.instance.wandelbots.io, controller=ur5e, motion_group=0@ur5e
|
| 142 |
+
[Nova] Creating NovaApiClient...
|
| 143 |
+
[Nova] Client created successfully. State streaming: True, IK: False, Jogging: True
|
| 144 |
+
[Nova] Starting state stream...
|
| 145 |
+
[Nova] Starting state stream thread...
|
| 146 |
+
[Nova] Connecting to state stream: wss://vaxtlucd.instance.wandelbots.io/api/v2/cells/cell/controllers/ur5e/motion-groups/0@ur5e/state-stream?response_rate=100
|
| 147 |
+
[Nova] State stream started successfully
|
| 148 |
+
[Nova] WARNING: State streaming enabled - simulation is now a digital twin.
|
| 149 |
+
[Nova] Robot state comes from Nova API, local targets/IK are ignored.
|
| 150 |
+
[Nova] Creating NovaJogger...
|
| 151 |
+
[Nova Jogger] Ready to connect (will connect on first jog command)
|
| 152 |
+
[Nova] Jogger connected successfully
|
| 153 |
+
[Nova] Jogging enabled - UI can send directional jog commands
|
| 154 |
+
[Nova] Configuring Nova API with config: {'use_state_stream': True, 'use_ik': False, 'use_jogging': True}
|
| 155 |
+
[Nova] State stream connected successfully
|
| 156 |
+
[Nova] use_state_stream=True, use_ik=False, use_jogging=True
|
| 157 |
+
[Nova] Attempting to load config from environment variables...
|
| 158 |
+
[Nova] Config loaded from env: https://vaxtlucd.instance.wandelbots.io, controller=ur5e, motion_group=0@ur5e
|
| 159 |
+
[Nova] Creating NovaApiClient...
|
| 160 |
+
[Nova] Client created successfully. State streaming: True, IK: False, Jogging: True
|
| 161 |
+
[Nova] Starting state stream...
|
| 162 |
+
[Nova] Starting state stream thread...
|
| 163 |
+
[Nova] Connecting to state stream: wss://vaxtlucd.instance.wandelbots.io/api/v2/cells/cell/controllers/ur5e/motion-groups/0@ur5e/state-stream?response_rate=100127.0.0.1 - - [21/Jan/2026 12:31:31] "GET /nova-sim/api/v1/env HTTP/1.1" 200 -
|
| 164 |
+
[Nova] State stream started successfully
|
| 165 |
+
[Nova] WARNING: State streaming enabled - simulation is now a digital twin.
|
| 166 |
+
[Nova] Robot state comes from Nova API, local targets/IK are ignored.
|
| 167 |
+
[Nova] Creating NovaJogger...
|
| 168 |
+
[Nova Jogger] Ready to connect (will connect on first jog command)
|
| 169 |
+
[Nova] Jogger connected successfully
|
| 170 |
+
[Nova] Jogging enabled - UI can send directional jog commands
|
| 171 |
+
|
| 172 |
+
Switched to ur5
|
| 173 |
+
[Nova] State stream connected successfully
|
| 174 |
+
[Nova] State stream update (seq=20357380, 6 joints) payload={"seq": 20357380, "joint_position": [-3.7937827110290527, -1.5640392303466797, 1.8428640365600586, -1.4148643016815186, -1.725486397743225, -0.5821775794029236], "timestamp": "2026-01-21T11:31:31.701604829Z"}
|
| 175 |
+
[WS] Received message type: camera
|
| 176 |
+
[WS] Received message type: camera
|
| 177 |
+
[WS] Received message type: start_jog
|
| 178 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 179 |
+
[Nova Jogger] Connecting to cartesian endpoint: wss://vaxtlucd.instance.wandelbots.io/api/v1/cells/cell/motion-groups/move-tcp
|
| 180 |
+
[Nova Jogger] Connected to cartesian endpoint
|
| 181 |
+
[Nova Jogger] Cartesian X translation at 50 mm/s (dir: 1)
|
| 182 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 183 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 184 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 185 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 186 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 187 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 188 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 189 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 190 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 191 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 192 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 193 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 194 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 195 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 196 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 197 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 198 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 199 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 200 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 201 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 202 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 203 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 204 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 205 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 206 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 207 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 208 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 209 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 210 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 211 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 212 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.05}
|
| 213 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.05}
|
| 214 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 215 |
+
[WS] Received message type: stop_jog
|
| 216 |
+
[Nova Jogger] Stopped (zero velocities sent)
|
| 217 |
+
[WS] Received message type: switch_robot
|
| 218 |
+
Robot switch requested: spot / scene: scene
|
| 219 |
+
Switching to robot: spot
|
| 220 |
+
Quadruped-PyMPC components loaded from local standalone implementation
|
| 221 |
+
PyMPC gait generator initialized
|
| 222 |
+
Switched to spot
|
| 223 |
+
[WS] Received message type: command
|
| 224 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 225 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 226 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 227 |
+
[WS] Received message type: teleop_action
|
| 228 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 229 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 230 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 231 |
+
[WS] Received message type: teleop_action
|
| 232 |
+
[WS] Received message type: teleop_action
|
| 233 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 234 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 235 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 236 |
+
[WS] Received message type: teleop_action
|
| 237 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 238 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 239 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 240 |
+
[WS] Received message type: teleop_action
|
| 241 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 242 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 243 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 244 |
+
[WS] Received message type: teleop_action
|
| 245 |
+
[WS] Received message type: command
|
| 246 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 247 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 248 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 249 |
+
[WS] Received message type: teleop_action
|
| 250 |
+
[WS] Received message type: command
|
| 251 |
+
[WS] Received message type: teleop_action
|
| 252 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 253 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 254 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 255 |
+
[WS] Received message type: command
|
| 256 |
+
[WS] Received message type: teleop_action
|
| 257 |
+
[WS] Received message type: command
|
| 258 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 259 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 260 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 261 |
+
[WS] Received message type: teleop_action
|
| 262 |
+
[WS] Received message type: command
|
| 263 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 264 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 265 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 266 |
+
[WS] Received message type: teleop_action
|
| 267 |
+
[WS] Received message type: command
|
| 268 |
+
[WS] Received message type: teleop_action
|
| 269 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 270 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 271 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 272 |
+
[WS] Received message type: command
|
| 273 |
+
[WS] Received message type: teleop_action
|
| 274 |
+
[WS] Received message type: command
|
| 275 |
+
[WS] Received message type: command
|
| 276 |
+
[WS] Received message type: teleop_action
|
| 277 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.005}
|
| 278 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.005}
|
| 279 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 280 |
+
[WS] Received message type: teleop_action
|
| 281 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.005}
|
| 282 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.005}
|
| 283 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 284 |
+
[WS] Received message type: teleop_action
|
| 285 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.005}
|
| 286 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.005}
|
| 287 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 288 |
+
[WS] Received message type: teleop_action
|
| 289 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.005}
|
| 290 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.005}
|
| 291 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 292 |
+
[WS] Received message type: teleop_action
|
| 293 |
+
[WS] Received message type: teleop_action
|
| 294 |
+
[WS] Received message type: command
|
| 295 |
+
[Broadcast] Non-zero teleop values: {'vyaw': -1.2}
|
| 296 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vyaw': -1.2}
|
| 297 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 298 |
+
[WS] Received message type: teleop_action
|
| 299 |
+
[WS] Received message type: command
|
| 300 |
+
[Broadcast] Non-zero teleop values: {'vyaw': -1.2}
|
| 301 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vyaw': -1.2}
|
| 302 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 303 |
+
[WS] Received message type: teleop_action
|
| 304 |
+
[WS] Received message type: command
|
| 305 |
+
[Broadcast] Non-zero teleop values: {'vyaw': -1.2}
|
| 306 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vyaw': -1.2}
|
| 307 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 308 |
+
[WS] Received message type: teleop_action
|
| 309 |
+
[WS] Received message type: command
|
| 310 |
+
[WS] Received message type: teleop_action
|
| 311 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.005}
|
| 312 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.005}
|
| 313 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 314 |
+
[WS] Received message type: command
|
| 315 |
+
[WS] Received message type: teleop_action
|
| 316 |
+
[WS] Received message type: command
|
| 317 |
+
[WS] Received message type: command
|
| 318 |
+
[Broadcast] Non-zero teleop values: {'vyaw': 1.2}
|
| 319 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vyaw': 1.2}
|
| 320 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 321 |
+
[WS] Received message type: teleop_action
|
| 322 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.005}
|
| 323 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.005}
|
| 324 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 325 |
+
[WS] Received message type: teleop_action
|
| 326 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.005}
|
| 327 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.005}
|
| 328 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 329 |
+
[WS] Received message type: teleop_action
|
| 330 |
+
[Nova] State stream update (seq=20358760, 6 joints) payload={"seq": 20358760, "joint_position": [-3.7214279174804688, -1.4229689836502075, 1.6873574256896973, -1.3912395238876343, -1.6946996450424194, -0.5160554051399231], "timestamp": "2026-01-21T11:31:41.768234831Z"}
|
| 331 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.005}
|
| 332 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.005}
|
| 333 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 334 |
+
[WS] Received message type: teleop_action
|
| 335 |
+
[WS] Received message type: teleop_action
|
| 336 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.005}
|
| 337 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.005}
|
| 338 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 339 |
+
[WS] Received message type: teleop_action
|
| 340 |
+
[WS] Received message type: command
|
| 341 |
+
[Broadcast] Non-zero teleop values: {'vyaw': 1.2}
|
| 342 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vyaw': 1.2}
|
| 343 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 344 |
+
[WS] Received message type: teleop_action
|
| 345 |
+
[WS] Received message type: command
|
| 346 |
+
[Broadcast] Non-zero teleop values: {'vyaw': 1.2}
|
| 347 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vyaw': 1.2}
|
| 348 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 349 |
+
[WS] Received message type: teleop_action
|
| 350 |
+
[WS] Received message type: command
|
| 351 |
+
[WS] Received message type: teleop_action
|
| 352 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.005}
|
| 353 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.005}
|
| 354 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 355 |
+
[WS] Received message type: command
|
| 356 |
+
[WS] Received message type: teleop_action
|
| 357 |
+
[WS] Received message type: command
|
| 358 |
+
[WS] Received message type: switch_robot
|
| 359 |
+
Robot switch requested: g1 / scene: scene
|
| 360 |
+
Switching to robot: g1
|
| 361 |
+
Switched to g1
|
| 362 |
+
[WS] Received message type: camera
|
| 363 |
+
[WS] Received message type: command
|
| 364 |
+
[WS] Received message type: teleop_action
|
| 365 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 366 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 367 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 368 |
+
[WS] Received message type: teleop_action
|
| 369 |
+
[WS] Received message type: teleop_action
|
| 370 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 371 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 372 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 373 |
+
[WS] Received message type: teleop_action
|
| 374 |
+
[WS] Received message type: teleop_action
|
| 375 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 376 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 377 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 378 |
+
[WS] Received message type: teleop_action
|
| 379 |
+
[WS] Received message type: command
|
| 380 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 381 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 382 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 383 |
+
[WS] Received message type: teleop_action
|
| 384 |
+
[WS] Received message type: command
|
| 385 |
+
[WS] Received message type: teleop_action
|
| 386 |
+
[WS] Received message type: command
|
| 387 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 388 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 389 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 390 |
+
[WS] Received message type: teleop_action
|
| 391 |
+
[WS] Received message type: command
|
| 392 |
+
[WS] Received message type: teleop_action
|
| 393 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 394 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 395 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 396 |
+
[WS] Received message type: command
|
| 397 |
+
[WS] Received message type: teleop_action
|
| 398 |
+
[WS] Received message type: command
|
| 399 |
+
[WS] Received message type: teleop_action
|
| 400 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 401 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 402 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 403 |
+
[WS] Received message type: command
|
| 404 |
+
[WS] Received message type: teleop_action
|
| 405 |
+
[WS] Received message type: command
|
| 406 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 407 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 408 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 409 |
+
[WS] Received message type: teleop_action
|
| 410 |
+
[WS] Received message type: command
|
| 411 |
+
[WS] Received message type: teleop_action
|
| 412 |
+
[WS] Received message type: command
|
| 413 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 414 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 415 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 416 |
+
[WS] Received message type: teleop_action
|
| 417 |
+
[WS] Received message type: command
|
| 418 |
+
[WS] Received message type: teleop_action
|
| 419 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 420 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 421 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 422 |
+
[WS] Received message type: command
|
| 423 |
+
[WS] Received message type: teleop_action
|
| 424 |
+
[WS] Received message type: command
|
| 425 |
+
[WS] Received message type: teleop_action
|
| 426 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 427 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 428 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 429 |
+
[WS] Received message type: command
|
| 430 |
+
[WS] Received message type: teleop_action
|
| 431 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 432 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 433 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 434 |
+
[WS] Received message type: command
|
| 435 |
+
[WS] Received message type: teleop_action
|
| 436 |
+
[WS] Received message type: command
|
| 437 |
+
[WS] Received message type: command
|
| 438 |
+
[WS] Received message type: teleop_action
|
| 439 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 440 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 441 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 442 |
+
[WS] Received message type: teleop_action
|
| 443 |
+
[WS] Received message type: teleop_action
|
| 444 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 445 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 446 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 447 |
+
[WS] Received message type: teleop_action
|
| 448 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 449 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 450 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 451 |
+
[WS] Received message type: teleop_action
|
| 452 |
+
[WS] Received message type: teleop_action
|
| 453 |
+
[WS] Received message type: command
|
| 454 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 455 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 456 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 457 |
+
[WS] Received message type: teleop_action
|
| 458 |
+
[WS] Received message type: command
|
| 459 |
+
[WS] Received message type: teleop_action
|
| 460 |
+
[WS] Received message type: command
|
| 461 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 462 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 463 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 464 |
+
[WS] Received message type: teleop_action
|
| 465 |
+
[WS] Received message type: command
|
| 466 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 467 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 468 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 469 |
+
[WS] Received message type: teleop_action
|
| 470 |
+
[WS] Received message type: command
|
| 471 |
+
[WS] Received message type: teleop_action
|
| 472 |
+
[WS] Received message type: command
|
| 473 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 474 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 475 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 476 |
+
[WS] Received message type: teleop_action
|
| 477 |
+
[WS] Received message type: command
|
| 478 |
+
[WS] Received message type: teleop_action
|
| 479 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 480 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 481 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 482 |
+
[WS] Received message type: command
|
| 483 |
+
[WS] Received message type: teleop_action
|
| 484 |
+
[WS] Received message type: command
|
| 485 |
+
[WS] Received message type: teleop_action
|
| 486 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 487 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 488 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0127.0.0.1 - - [21/Jan/2026 12:31:51] "GET /nova-sim/api/v1/env HTTP/1.1" 200 -
|
| 489 |
+
|
| 490 |
+
[WS] Received message type: command
|
| 491 |
+
[WS] Received message type: teleop_action
|
| 492 |
+
[WS] Received message type: command
|
| 493 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.8}
|
| 494 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.8}
|
| 495 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 496 |
+
[WS] Received message type: teleop_action
|
| 497 |
+
[WS] Received message type: command
|
| 498 |
+
[WS] Received message type: teleop_action
|
| 499 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 500 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 501 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 502 |
+
[WS] Received message type: command
|
| 503 |
+
[WS] Received message type: teleop_action
|
| 504 |
+
[WS] Received message type: command
|
| 505 |
+
[WS] Received message type: teleop_action
|
| 506 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 507 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 508 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 509 |
+
[WS] Received message type: command
|
| 510 |
+
[WS] Received message type: teleop_action
|
| 511 |
+
[WS] Received message type: command
|
| 512 |
+
[WS] Received message type: teleop_action
|
| 513 |
+
[Broadcast] Non-zero teleop values: {'vx': 0.005}
|
| 514 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': 0.005}
|
| 515 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 516 |
+
[WS] Received message type: command
|
| 517 |
+
[WS] Received message type: switch_robot
|
| 518 |
+
Robot switch requested: ur5_t_push / scene: scene_t_push
|
| 519 |
+
[WS] Received message type: set_nova_mode
|
| 520 |
+
Switching to robot: ur5_t_push
|
| 521 |
+
Nova API credentials detected - attempting bidirectional control
|
| 522 |
+
[Nova] State stream update (seq=20360140, 6 joints) payload={"seq": 20360140, "joint_position": [-3.7214279174804688, -1.4229689836502075, 1.6873574256896973, -1.3912395238876343, -1.6946996450424194, -0.5160554051399231], "timestamp": "2026-01-21T11:31:51.841308365Z"}
|
| 523 |
+
[Nova] Configuring Nova API with config: {'use_state_stream': True, 'use_ik': False, 'use_jogging': True}
|
| 524 |
+
[Nova] use_state_stream=True, use_ik=False, use_jogging=True
|
| 525 |
+
[Nova] Attempting to load config from environment variables...
|
| 526 |
+
[Nova] Config loaded from env: https://vaxtlucd.instance.wandelbots.io, controller=ur5e, motion_group=0@ur5e
|
| 527 |
+
[Nova] Creating NovaApiClient...
|
| 528 |
+
[Nova] Client created successfully. State streaming: True, IK: False, Jogging: True
|
| 529 |
+
[Nova] Starting state stream...
|
| 530 |
+
[Nova] Starting state stream thread...
|
| 531 |
+
[Nova] Connecting to state stream: wss://vaxtlucd.instance.wandelbots.io/api/v2/cells/cell/controllers/ur5e/motion-groups/0@ur5e/state-stream?response_rate=100
|
| 532 |
+
[Nova] State stream started successfully
|
| 533 |
+
[Nova] WARNING: State streaming enabled - simulation is now a digital twin.
|
| 534 |
+
[Nova] Robot state comes from Nova API, local targets/IK are ignored.
|
| 535 |
+
[Nova] Creating NovaJogger...
|
| 536 |
+
[Nova Jogger] Ready to connect (will connect on first jog command)
|
| 537 |
+
[Nova] Jogger connected successfully
|
| 538 |
+
[Nova] Jogging enabled - UI can send directional jog commands
|
| 539 |
+
[Nova] Configuring Nova API with config: {'use_state_stream': True, 'use_ik': False, 'use_jogging': True}
|
| 540 |
+
[Nova] State stream connected successfully
|
| 541 |
+
[Nova] use_state_stream=True, use_ik=False, use_jogging=True
|
| 542 |
+
[Nova] Attempting to load config from environment variables...
|
| 543 |
+
[Nova] Config loaded from env: https://vaxtlucd.instance.wandelbots.io, controller=ur5e, motion_group=0@ur5e
|
| 544 |
+
[Nova] Creating NovaApiClient...
|
| 545 |
+
[Nova] Client created successfully. State streaming: True, IK: False, Jogging: True
|
| 546 |
+
[Nova] Starting state stream...
|
| 547 |
+
[Nova] Starting state stream thread...
|
| 548 |
+
[Nova] Connecting to state stream: wss://vaxtlucd.instance.wandelbots.io/api/v2/cells/cell/controllers/ur5e/motion-groups/0@ur5e/state-stream?response_rate=100
|
| 549 |
+
[Nova] State stream started successfully
|
| 550 |
+
[Nova] WARNING: State streaming enabled - simulation is now a digital twin.
|
| 551 |
+
[Nova] Robot state comes from Nova API, local targets/IK are ignored.
|
| 552 |
+
[Nova] Creating NovaJogger...
|
| 553 |
+
[Nova Jogger] Ready to connect (will connect on first jog command)
|
| 554 |
+
[Nova] Jogger connected successfully
|
| 555 |
+
[Nova] Jogging enabled - UI can send directional jog commands
|
| 556 |
+
Switched to ur5_t_push
|
| 557 |
+
[Nova] State stream connected successfully
|
| 558 |
+
[Nova] State stream update (seq=20360191, 6 joints) payload={"seq": 20360191, "joint_position": [-3.7214279174804688, -1.4229689836502075, 1.6873574256896973, -1.3912395238876343, -1.6946996450424194, -0.5160554051399231], "timestamp": "2026-01-21T11:31:52.214014194Z"}
|
| 559 |
+
[WS] Received message type: set_nova_mode
|
| 560 |
+
[Nova] Configuring Nova API with config: {'use_state_stream': True, 'use_ik': False, 'use_jogging': True}
|
| 561 |
+
[Nova] use_state_stream=True, use_ik=False, use_jogging=True
|
| 562 |
+
[Nova] Attempting to load config from environment variables...
|
| 563 |
+
[Nova] Config loaded from env: https://vaxtlucd.instance.wandelbots.io, controller=ur5e, motion_group=0@ur5e
|
| 564 |
+
[Nova] Creating NovaApiClient...
|
| 565 |
+
[Nova] Client created successfully. State streaming: True, IK: False, Jogging: True
|
| 566 |
+
[Nova] Starting state stream...
|
| 567 |
+
[Nova] Starting state stream thread...
|
| 568 |
+
[Nova] Connecting to state stream: wss://vaxtlucd.instance.wandelbots.io/api/v2/cells/cell/controllers/ur5e/motion-groups/0@ur5e/state-stream?response_rate=100
|
| 569 |
+
[Nova] State stream started successfully
|
| 570 |
+
[Nova] WARNING: State streaming enabled - simulation is now a digital twin.
|
| 571 |
+
[Nova] Robot state comes from Nova API, local targets/IK are ignored.
|
| 572 |
+
[Nova] Creating NovaJogger...
|
| 573 |
+
[Nova Jogger] Ready to connect (will connect on first jog command)
|
| 574 |
+
[Nova] Jogger connected successfully
|
| 575 |
+
[Nova] Jogging enabled - UI can send directional jog commands
|
| 576 |
+
[Nova] State stream connected successfully
|
| 577 |
+
[Nova] State stream update (seq=20360550, 6 joints) payload={"seq": 20360550, "joint_position": [-3.7214279174804688, -1.4229689836502075, 1.6873574256896973, -1.3912395238876343, -1.6946996450424194, -0.5160554051399231], "timestamp": "2026-01-21T11:31:54.840564316Z"}
|
| 578 |
+
[WS] Received message type: camera
|
| 579 |
+
[WS] Received message type: camera
|
| 580 |
+
[WS] Received message type: camera
|
| 581 |
+
[WS] Received message type: camera
|
| 582 |
+
[WS] Received message type: camera
|
| 583 |
+
[WS] Received message type: camera
|
| 584 |
+
[WS] Received message type: camera
|
| 585 |
+
[WS] Received message type: camera
|
| 586 |
+
[WS] Received message type: camera
|
| 587 |
+
[WS] Received message type: camera
|
| 588 |
+
[WS] Received message type: camera
|
| 589 |
+
[WS] Received message type: camera
|
| 590 |
+
[WS] Received message type: camera
|
| 591 |
+
[WS] Received message type: camera
|
| 592 |
+
[WS] Received message type: camera
|
| 593 |
+
[WS] Received message type: camera
|
| 594 |
+
[WS] Received message type: camera
|
| 595 |
+
[WS] Received message type: camera
|
| 596 |
+
[WS] Received message type: camera
|
| 597 |
+
[WS] Received message type: camera
|
| 598 |
+
[WS] Received message type: camera
|
| 599 |
+
[WS] Received message type: camera
|
| 600 |
+
[WS] Received message type: camera
|
| 601 |
+
[WS] Received message type: camera
|
| 602 |
+
[WS] Received message type: camera
|
| 603 |
+
[WS] Received message type: camera
|
| 604 |
+
[WS] Received message type: camera
|
| 605 |
+
[WS] Received message type: camera
|
| 606 |
+
[WS] Received message type: camera
|
| 607 |
+
[WS] Received message type: camera
|
| 608 |
+
[WS] Received message type: camera
|
| 609 |
+
[WS] Received message type: camera
|
| 610 |
+
[WS] Received message type: camera
|
| 611 |
+
[WS] Received message type: camera
|
| 612 |
+
[WS] Received message type: camera
|
| 613 |
+
[WS] Received message type: camera
|
| 614 |
+
[WS] Received message type: camera
|
| 615 |
+
[WS] Received message type: camera
|
| 616 |
+
[WS] Received message type: camera
|
| 617 |
+
[WS] Received message type: camera
|
| 618 |
+
[WS] Received message type: camera
|
| 619 |
+
[WS] Received message type: camera
|
| 620 |
+
[WS] Received message type: camera
|
| 621 |
+
[WS] Received message type: camera
|
| 622 |
+
[WS] Received message type: camera
|
| 623 |
+
[WS] Received message type: camera
|
| 624 |
+
[WS] Received message type: camera
|
| 625 |
+
[WS] Received message type: camera
|
| 626 |
+
[WS] Received message type: camera
|
| 627 |
+
[WS] Received message type: camera
|
| 628 |
+
[WS] Received message type: camera
|
| 629 |
+
[WS] Received message type: camera
|
| 630 |
+
[WS] Received message type: camera
|
| 631 |
+
[WS] Received message type: camera
|
| 632 |
+
[WS] Received message type: camera
|
| 633 |
+
[WS] Received message type: camera
|
| 634 |
+
[WS] Received message type: camera
|
| 635 |
+
[WS] Received message type: camera
|
| 636 |
+
[WS] Received message type: camera
|
| 637 |
+
[WS] Received message type: camera
|
| 638 |
+
[WS] Received message type: camera
|
| 639 |
+
[WS] Received message type: camera
|
| 640 |
+
[WS] Received message type: camera
|
| 641 |
+
[WS] Received message type: camera
|
| 642 |
+
[WS] Received message type: camera
|
| 643 |
+
[WS] Received message type: camera
|
| 644 |
+
[WS] Received message type: camera
|
| 645 |
+
[WS] Received message type: camera
|
| 646 |
+
[WS] Received message type: camera
|
| 647 |
+
[WS] Received message type: camera
|
| 648 |
+
[WS] Received message type: camera
|
| 649 |
+
[WS] Received message type: camera
|
| 650 |
+
[WS] Received message type: camera
|
| 651 |
+
[WS] Received message type: camera
|
| 652 |
+
[WS] Received message type: camera
|
| 653 |
+
[WS] Received message type: camera
|
| 654 |
+
[WS] Received message type: camera
|
| 655 |
+
[WS] Received message type: camera
|
| 656 |
+
[WS] Received message type: camera
|
| 657 |
+
[WS] Received message type: camera
|
| 658 |
+
[WS] Received message type: camera
|
| 659 |
+
[WS] Received message type: start_jog
|
| 660 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 661 |
+
[Nova Jogger] Connecting to cartesian endpoint: wss://vaxtlucd.instance.wandelbots.io/api/v1/cells/cell/motion-groups/move-tcp
|
| 662 |
+
[Nova Jogger] Connected to cartesian endpoint
|
| 663 |
+
[Nova Jogger] Cartesian Y translation at 50 mm/s (dir: -1)
|
| 664 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 665 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 666 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 667 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 668 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 669 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 670 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 671 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 672 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 673 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 674 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 675 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 676 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 677 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 678 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 679 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 680 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 681 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 682 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 683 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 684 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 685 |
+
[WS] Received message type: stop_jog
|
| 686 |
+
[Nova Jogger] Stopped (zero velocities sent)
|
| 687 |
+
[WS] Received message type: start_jog
|
| 688 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 689 |
+
[Nova Jogger] Cartesian X translation at 50 mm/s (dir: -1)
|
| 690 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 691 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 692 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 693 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 694 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 695 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 696 |
+
[WS] Received message type: stop_jog
|
| 697 |
+
[Nova Jogger] Stopped (zero velocities sent)
|
| 698 |
+
[WS] Received message type: camera
|
| 699 |
+
[WS] Received message type: camera
|
| 700 |
+
[WS] Received message type: camera
|
| 701 |
+
[WS] Received message type: camera
|
| 702 |
+
[WS] Received message type: camera
|
| 703 |
+
[WS] Received message type: camera
|
| 704 |
+
[WS] Received message type: camera
|
| 705 |
+
[WS] Received message type: camera
|
| 706 |
+
[WS] Received message type: camera
|
| 707 |
+
[WS] Received message type: camera
|
| 708 |
+
[WS] Received message type: camera
|
| 709 |
+
[WS] Received message type: camera
|
| 710 |
+
[WS] Received message type: camera
|
| 711 |
+
[WS] Received message type: camera
|
| 712 |
+
[WS] Received message type: camera
|
| 713 |
+
[WS] Received message type: camera
|
| 714 |
+
[WS] Received message type: camera
|
| 715 |
+
[WS] Received message type: camera
|
| 716 |
+
[WS] Received message type: camera
|
| 717 |
+
[WS] Received message type: camera
|
| 718 |
+
[WS] Received message type: camera
|
| 719 |
+
[WS] Received message type: camera
|
| 720 |
+
[WS] Received message type: camera
|
| 721 |
+
[WS] Received message type: camera
|
| 722 |
+
[WS] Received message type: camera
|
| 723 |
+
[WS] Received message type: start_jog
|
| 724 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 725 |
+
[Nova Jogger] Cartesian X translation at 50 mm/s (dir: -1)
|
| 726 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 727 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 728 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 729 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 730 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 731 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 732 |
+
[Nova] State stream update (seq=20361508, 6 joints) payload={"seq": 20361508, "joint_position": [-3.7214279174804688, -1.4229689836502075, 1.6873574256896973, -1.3912395238876343, -1.6946996450424194, -0.5160554051399231], "timestamp": "2026-01-21T11:32:01.860604486Z"}
|
| 733 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 734 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 735 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 736 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 737 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 738 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 739 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 740 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 741 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 742 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 743 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 744 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 745 |
+
[WS] Received message type: stop_jog
|
| 746 |
+
[Nova Jogger] Stopped (zero velocities sent)
|
| 747 |
+
[WS] Received message type: start_jog
|
| 748 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 749 |
+
[Nova Jogger] Cartesian Y translation at 50 mm/s (dir: -1)
|
| 750 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 751 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 752 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 753 |
+
[Nova Jogger] Receive error: no close frame received or sent
|
| 754 |
+
[WS] Received message type: stop_jog
|
| 755 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 756 |
+
[WS] Received message type: start_jog
|
| 757 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 758 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 759 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 760 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.05}
|
| 761 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.05}
|
| 762 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 763 |
+
[WS] Received message type: start_jog
|
| 764 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 765 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 766 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 767 |
+
[WS] Received message type: stop_jog
|
| 768 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 769 |
+
[WS] Received message type: start_jog
|
| 770 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 771 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 772 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 773 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 774 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 775 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 776 |
+
[WS] Received message type: start_jog
|
| 777 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 778 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 779 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 780 |
+
[WS] Received message type: start_jog
|
| 781 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 782 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 783 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 784 |
+
[WS] Received message type: stop_jog
|
| 785 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 786 |
+
[WS] Received message type: start_jog
|
| 787 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 788 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 789 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 790 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.05}
|
| 791 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.05}
|
| 792 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 793 |
+
[WS] Received message type: stop_jog
|
| 794 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 795 |
+
[WS] Received message type: start_jog
|
| 796 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 797 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 798 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 799 |
+
[WS] Received message type: stop_jog
|
| 800 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 801 |
+
[WS] Received message type: start_jog
|
| 802 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 803 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 804 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 805 |
+
[WS] Received message type: start_jog
|
| 806 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 807 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 808 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 809 |
+
[Broadcast] Non-zero teleop values: {'vx': -0.05}
|
| 810 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vx': -0.05}
|
| 811 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 812 |
+
[WS] Received message type: start_jog
|
| 813 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 814 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 815 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 816 |
+
[WS] Received message type: stop_jog
|
| 817 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 818 |
+
[WS] Received message type: start_jog
|
| 819 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 820 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 821 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 822 |
+
[WS] Received message type: stop_jog
|
| 823 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 824 |
+
[WS] Received message type: start_jog
|
| 825 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 826 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 827 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 828 |
+
[WS] Received message type: start_jog
|
| 829 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 830 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 831 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 832 |
+
[WS] Received message type: stop_jog
|
| 833 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 834 |
+
[WS] Received message type: start_jog
|
| 835 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 836 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 837 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 838 |
+
[Broadcast] Non-zero teleop values: {'vy': 0.05}
|
| 839 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': 0.05}
|
| 840 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 841 |
+
[WS] Received message type: start_jog
|
| 842 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 843 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 844 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 845 |
+
[WS] Received message type: start_jog
|
| 846 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 847 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 848 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 849 |
+
[WS] Received message type: stop_jog
|
| 850 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 851 |
+
[WS] Received message type: start_jog
|
| 852 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 853 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 854 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 855 |
+
[WS] Received message type: stop_jog
|
| 856 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 857 |
+
[WS] Received message type: start_jog
|
| 858 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 859 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 860 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 861 |
+
[Broadcast] Non-zero teleop values: {'vy': -0.05}
|
| 862 |
+
[Broadcast] Sending to 1 clients, teleop non-zero values: {'vy': -0.05}
|
| 863 |
+
[Broadcast] Actual JSON teleop_action j3 = 0.0
|
| 864 |
+
[WS] Received message type: stop_jog
|
| 865 |
+
[Nova Jogger] Failed to stop: no close frame received or sent
|
| 866 |
+
[WS] Received message type: start_jog
|
| 867 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 868 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 869 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'y', 'direction': '+', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 870 |
+
[WS] Received message type: start_jog
|
| 871 |
+
[Server] Received start_jog: jog_type=cartesian_translation, params={'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
| 872 |
+
[Nova Jogger] Failed to start cartesian translation: no close frame received or sent
|
| 873 |
+
[Server] Failed to start jog: cartesian_translation, {'axis': 'x', 'direction': '-', 'velocity': 50, 'tcp_id': 'Flange', 'coord_system_id': 'world'}
|
templates/index.html
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<title>Nova Sim - Wandelbots Robot Simulator</title>
|
| 5 |
+
<meta charset="utf-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<script type="module" crossorigin src="/nova-sim/assets/index-CwMJ-B8R.js"></script>
|
| 8 |
+
<link rel="stylesheet" crossorigin href="/nova-sim/assets/index-DoqnwcEU.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<!-- Video viewport -->
|
| 12 |
+
<div class="video-container" id="viewport">
|
| 13 |
+
<img src="/nova-sim/api/v1/video_feed" draggable="false" alt="Robot simulation video feed">
|
| 14 |
+
</div>
|
| 15 |
+
|
| 16 |
+
<!-- Main control overlay -->
|
| 17 |
+
<div class="overlay" id="main_overlay">
|
| 18 |
+
<!-- Content will be generated by JavaScript -->
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<!-- State panel -->
|
| 22 |
+
<div class="state-panel" id="state_panel">
|
| 23 |
+
<!-- Content will be generated by JavaScript -->
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<!-- Camera panel -->
|
| 27 |
+
<div class="camera-panel" id="camera_panel">
|
| 28 |
+
<!-- Content will be generated by JavaScript -->
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<!-- Overlay tiles for auxiliary camera feeds -->
|
| 32 |
+
<div class="overlay-tiles" id="overlay_tiles">
|
| 33 |
+
<!-- Generated dynamically -->
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<!-- Notification area -->
|
| 37 |
+
<div class="rl-notifications" id="rl_notifications">
|
| 38 |
+
<!-- Toast notifications appear here -->
|
| 39 |
+
</div>
|
| 40 |
+
</body>
|
| 41 |
+
</html>
|