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 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 (includes all assets)
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 3004
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
- ### Native (Recommended for Development)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Start the server (default reward threshold: -0.1)
112
- python mujoco_server.py
 
 
 
113
 
114
- # Or with custom reward threshold for auto episode termination
115
- python mujoco_server.py --reward-threshold -0.05 # Stricter (5cm from target)
116
- python mujoco_server.py --reward-threshold -0.2 # Lenient (20cm from target)
117
 
118
- # Open browser at http://localhost:3004/nova-sim/api/v1
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
- ### Getting Started
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:3004 nova-sim
 
 
135
  ```
136
 
137
  ### Configuration & Tuning
@@ -156,7 +221,7 @@ services:
156
  nova-sim:
157
  build: .
158
  ports:
159
- - "3004: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: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: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: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: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: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
+ WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
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
+ Press CTRL+C to quit
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>