blanchon commited on
Commit
6ce4ca6
·
0 Parent(s):
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +56 -0
  2. .gitattributes +4 -0
  3. .gitignore +23 -0
  4. .gitmodules +6 -0
  5. .npmrc +1 -0
  6. .prettierignore +12 -0
  7. .prettierrc +16 -0
  8. Dockerfile +48 -0
  9. README.md +297 -0
  10. bun.lock +0 -0
  11. components.json +16 -0
  12. eslint.config.js +36 -0
  13. external/.gitkeep +0 -0
  14. external/RobotHub-InferenceServer +1 -0
  15. external/RobotHub-TransportServer +1 -0
  16. log.txt +1 -0
  17. package.json +63 -0
  18. packages/feetech.js/README.md +24 -0
  19. packages/feetech.js/debug.mjs +15 -0
  20. packages/feetech.js/index.d.ts +50 -0
  21. packages/feetech.js/index.mjs +65 -0
  22. packages/feetech.js/lowLevelSDK.mjs +1235 -0
  23. packages/feetech.js/package.json +38 -0
  24. packages/feetech.js/scsServoSDK.mjs +1205 -0
  25. packages/feetech.js/scsservo_constants.mjs +53 -0
  26. packages/feetech.js/test.html +770 -0
  27. src/app.css +122 -0
  28. src/app.d.ts +20 -0
  29. src/app.html +12 -0
  30. src/lib/components/3d/Floor.svelte +24 -0
  31. src/lib/components/3d/elements/compute/ComputeGridItem.svelte +51 -0
  32. src/lib/components/3d/elements/compute/Computes.svelte +87 -0
  33. src/lib/components/3d/elements/compute/GPU.svelte +138 -0
  34. src/lib/components/3d/elements/compute/GPUModel.svelte +200 -0
  35. src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte +382 -0
  36. src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte +291 -0
  37. src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte +288 -0
  38. src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte +276 -0
  39. src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte +48 -0
  40. src/lib/components/3d/elements/compute/status/ComputeConnectionFlowBoxUIKit.svelte +56 -0
  41. src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte +84 -0
  42. src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte +91 -0
  43. src/lib/components/3d/elements/compute/status/ComputeStatusBillboard.svelte +56 -0
  44. src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte +81 -0
  45. src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte +77 -0
  46. src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte +82 -0
  47. src/lib/components/3d/elements/robot/RobotGridItem.svelte +169 -0
  48. src/lib/components/3d/elements/robot/Robots.svelte +81 -0
  49. src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfBox.ts +3 -0
  50. src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfCylinder.ts +4 -0
.dockerignore ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build outputs (will be built in container)
5
+ build/
6
+ .svelte-kit/
7
+ dist/
8
+
9
+ # Development files
10
+ .env*
11
+ !.env.example
12
+
13
+ # IDE files
14
+ .vscode/
15
+ .idea/
16
+ *.swp
17
+ *.swo
18
+
19
+ # OS files
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # Git
24
+ .git/
25
+ .gitignore
26
+
27
+ # Logs
28
+ *.log
29
+ npm-debug.log*
30
+ pnpm-debug.log*
31
+ bun-debug.log*
32
+ lerna-debug.log*
33
+
34
+ # Cache directories
35
+ .cache/
36
+ .temp/
37
+ .tmp/
38
+
39
+ # Test files
40
+ coverage/
41
+ .nyc_output/
42
+
43
+ # Other build artifacts
44
+ *.tgz
45
+ *.tar.gz
46
+
47
+ # Docker files
48
+ Dockerfile*
49
+ docker-compose*
50
+ .dockerignore
51
+
52
+ # Documentation that's not needed in container
53
+ README.md
54
+ CHANGELOG.md
55
+ *.md
56
+ !LICENSE
.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.stl filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
3
+ static/gpu/scene.bin filter=lfs diff=lfs merge=lfs -text
4
+ static/video.mp4 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+
3
+ # Output
4
+ .output
5
+ .vercel
6
+ .netlify
7
+ .wrangler
8
+ /.svelte-kit
9
+ /build
10
+
11
+ # OS
12
+ .DS_Store
13
+ Thumbs.db
14
+
15
+ # Env
16
+ .env
17
+ .env.*
18
+ !.env.example
19
+ !.env.test
20
+
21
+ # Vite
22
+ vite.config.js.timestamp-*
23
+ vite.config.ts.timestamp-*
.gitmodules ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [submodule "external/RobotHub-TransportServer"]
2
+ path = external/RobotHub-TransportServer
3
+ url = https://github.com/julien-blanchon/RobotHub-TransportServer
4
+ [submodule "external/RobotHub-InferenceServer"]
5
+ path = external/RobotHub-InferenceServer
6
+ url = https://github.com/julien-blanchon/RobotHub-InferenceServer
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Package Managers
2
+ package-lock.json
3
+ pnpm-lock.yaml
4
+ yarn.lock
5
+ bun.lock
6
+ bun.lockb
7
+
8
+ # Src python
9
+ src-python/
10
+ node_modules/
11
+ build/
12
+ .svelte-kit/
.prettierrc ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": false,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7
+ "overrides": [
8
+ {
9
+ "files": "*.svelte",
10
+ "options": {
11
+ "parser": "svelte",
12
+ "svelteSortOrder": "options-scripts-markup-styles"
13
+ }
14
+ }
15
+ ]
16
+ }
Dockerfile ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-stage Dockerfile for LeRobot Arena Frontend
2
+ # Stage 1: Build the Svelte application with Bun
3
+ FROM oven/bun:1-alpine AS builder
4
+
5
+ WORKDIR /app
6
+
7
+ # Install git for dependencies that might need it
8
+ RUN apk add --no-cache git
9
+
10
+ # Copy package files for dependency resolution (better caching)
11
+ COPY package.json bun.lock* ./
12
+
13
+ # Copy local packages that are linked in package.json
14
+ COPY packages/ ./packages/
15
+
16
+ # Install dependencies
17
+ RUN bun install --frozen-lockfile
18
+
19
+ # Copy source code
20
+ COPY . .
21
+
22
+ # Build the static application
23
+ RUN bun run build
24
+
25
+ # Stage 2: Serve with Bun's simple static server
26
+ FROM oven/bun:1-alpine AS production
27
+
28
+ # Set up a new user named "user" with user ID 1000 (required for HF Spaces)
29
+ RUN adduser -D -u 1000 user
30
+
31
+ # Switch to the "user" user
32
+ USER user
33
+
34
+ # Set home to the user's home directory
35
+ ENV HOME=/home/user \
36
+ PATH=/home/user/.local/bin:$PATH
37
+
38
+ # Set the working directory to the user's home directory
39
+ WORKDIR $HOME/app
40
+
41
+ # Copy built application from previous stage with proper ownership
42
+ COPY --chown=user --from=builder /app/build ./
43
+
44
+ # Expose port 7860 (HF Spaces default)
45
+ EXPOSE 7860
46
+
47
+ # Start simple static server using Bun
48
+ CMD ["bun", "--bun", "serve", ".", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: LeRobot Arena Frontend
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: static
7
+ app_build_command: bun install && bun run build
8
+ app_file: build/index.html
9
+ pinned: false
10
+ license: mit
11
+ short_description: A web-based robotics control and simulation platform
12
+ tags:
13
+ - robotics
14
+ - control
15
+ - simulation
16
+ - svelte
17
+ - static
18
+ - frontend
19
+ ---
20
+
21
+ # 🤖 LeRobot Arena
22
+
23
+ A web-based robotics control and simulation platform that bridges digital twins and physical robots. Built with Svelte for the frontend and FastAPI for the backend.
24
+
25
+ ## 🚀 Simple Deployment Options
26
+
27
+ Here are the easiest ways to deploy this Svelte frontend:
28
+
29
+ ### 🏆 Option 1: Hugging Face Spaces (Static) - RECOMMENDED ✨
30
+
31
+ **Automatic deployment** (easiest):
32
+ 1. **Fork this repository** to your GitHub account
33
+ 2. **Create a new Space** on [Hugging Face Spaces](https://huggingface.co/spaces)
34
+ 3. **Connect your GitHub repo** - it will auto-detect the static SDK
35
+ 4. **Push to main branch** - auto-builds and deploys!
36
+
37
+ The frontmatter is already configured with:
38
+ ```yaml
39
+ sdk: static
40
+ app_build_command: bun install && bun run build
41
+ app_file: build/index.html
42
+ ```
43
+
44
+ **Manual upload**:
45
+ 1. Run `bun install && bun run build` locally
46
+ 2. Create a Space with "Static HTML" SDK
47
+ 3. Upload all files from `build/` folder
48
+
49
+ ### 🚀 Option 2: Vercel - One-Click Deploy
50
+
51
+ [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new)
52
+
53
+ Settings: Build command `bun run build`, Output directory `build`
54
+
55
+ ### 📁 Option 3: Netlify - Drag & Drop
56
+
57
+ 1. Build locally: `bun install && bun run build`
58
+ 2. Drag `build/` folder to [Netlify](https://netlify.com)
59
+
60
+ ### 🆓 Option 4: GitHub Pages
61
+
62
+ Add this workflow file (`.github/workflows/deploy.yml`):
63
+ ```yaml
64
+ name: Deploy to GitHub Pages
65
+ on:
66
+ push:
67
+ branches: [ main ]
68
+ jobs:
69
+ deploy:
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: oven-sh/setup-bun@v1
74
+ - run: bun install --frozen-lockfile
75
+ - run: bun run build
76
+ - uses: peaceiris/actions-gh-pages@v3
77
+ with:
78
+ github_token: ${{ secrets.GITHUB_TOKEN }}
79
+ publish_dir: ./build
80
+ ```
81
+
82
+ ### 🐳 Option 5: Docker (Optional)
83
+
84
+ For local development or custom hosting:
85
+ ```bash
86
+ docker build -t lerobot-arena-frontend .
87
+ docker run -p 7860:7860 lerobot-arena-frontend
88
+ ```
89
+
90
+ The Docker setup uses Bun's simple static server - much simpler than the complex server.js approach!
91
+
92
+ ## 🛠️ Development Setup
93
+
94
+ For local development with hot-reload capabilities:
95
+
96
+ ### Frontend Development
97
+
98
+ ```bash
99
+ # Install dependencies
100
+ bun install
101
+
102
+ # Start the development server
103
+ bun run dev
104
+
105
+ # Or open in browser automatically
106
+ bun run dev -- --open
107
+ ```
108
+
109
+ ### Backend Development
110
+
111
+ ```bash
112
+ # Navigate to Python backend
113
+ cd src-python
114
+
115
+ # Install Python dependencies (using uv)
116
+ uv sync
117
+
118
+ # Or using pip
119
+ pip install -e .
120
+
121
+ # Start the backend server
122
+ python start_server.py
123
+ ```
124
+
125
+ ### Building Standalone Executable
126
+
127
+ The backend can be packaged as a standalone executable using box-packager:
128
+
129
+ ```bash
130
+ # Navigate to Python backend
131
+ cd src-python
132
+
133
+ # Install box-packager (if not already installed)
134
+ uv pip install box-packager
135
+
136
+ # Package the application
137
+ box package
138
+
139
+ # The executable will be in target/release/lerobot-arena-server
140
+ ./target/release/lerobot-arena-server
141
+ ```
142
+
143
+ Note: Requires [Rust/Cargo](https://rustup.rs/) to be installed for box-packager to work.
144
+
145
+ ## 📋 Project Structure
146
+
147
+ ```
148
+ lerobot-arena/
149
+ ├── src/ # Svelte frontend source
150
+ │ ├── lib/ # Reusable components and utilities
151
+ │ ├── routes/ # SvelteKit routes
152
+ │ └── app.html # App template
153
+ ├── src-python/ # Python backend
154
+ │ ├── src/ # Python source code
155
+ │ ├── start_server.py # Server entry point
156
+ │ ├── target/ # Box-packager build output (excluded from git)
157
+ │ └── pyproject.toml # Python dependencies
158
+ ├── static/ # Static assets
159
+ ├── Dockerfile # Docker configuration
160
+ ├── docker-compose.yml # Docker Compose setup
161
+ └── package.json # Node.js dependencies
162
+ ```
163
+
164
+ ## 🐳 Docker Information
165
+
166
+ The Docker setup includes:
167
+
168
+ - **Multi-stage build**: Optimized for production using Bun and uv
169
+ - **Automatic startup**: Both services start together
170
+ - **Port mapping**: Backend on 8080, Frontend on 7860 (HF Spaces compatible)
171
+ - **Static file serving**: Compiled Svelte app served efficiently
172
+ - **User permissions**: Properly configured for Hugging Face Spaces
173
+ - **Standalone executable**: Backend packaged with box-packager for faster startup
174
+
175
+ For detailed Docker documentation, see [DOCKER_README.md](./DOCKER_README.md).
176
+
177
+ ## 🔧 Building for Production
178
+
179
+ ### Frontend Only
180
+
181
+ ```bash
182
+ bun run build
183
+ ```
184
+
185
+ ### Backend Standalone Executable
186
+
187
+ ```bash
188
+ cd src-python
189
+ box package
190
+ ```
191
+
192
+ ### Complete Docker Build
193
+
194
+ ```bash
195
+ docker-compose up --build
196
+ ```
197
+
198
+ ## 🌐 What's Included
199
+
200
+ - **Real-time Robot Control**: WebSocket-based communication
201
+ - **3D Visualization**: Three.js integration for robot visualization
202
+ - **URDF Support**: Load and display robot models
203
+ - **Multi-robot Management**: Control multiple robots simultaneously
204
+ - **WebSocket API**: Real-time bidirectional communication
205
+ - **Standalone Distribution**: Self-contained executable with box-packager
206
+
207
+ ## 🚨 Troubleshooting
208
+
209
+ ### Port Conflicts
210
+
211
+ If ports 8080 or 7860 are already in use:
212
+
213
+ ```bash
214
+ # Check what's using the ports
215
+ lsof -i :8080
216
+ lsof -i :7860
217
+
218
+ # Use different ports
219
+ docker run -p 8081:8080 -p 7861:7860 lerobot-arena
220
+ ```
221
+
222
+ ### Container Issues
223
+
224
+ ```bash
225
+ # View logs
226
+ docker-compose logs lerobot-arena
227
+
228
+ # Rebuild without cache
229
+ docker-compose build --no-cache
230
+ docker-compose up
231
+ ```
232
+
233
+ ### Development Issues
234
+
235
+ ```bash
236
+ # Clear node modules and reinstall
237
+ rm -rf node_modules
238
+ bun install
239
+
240
+ # Clear Svelte kit cache
241
+ rm -rf .svelte-kit
242
+ bun run dev
243
+ ```
244
+
245
+ ### Box-packager Issues
246
+
247
+ ```bash
248
+ # Clean build artifacts
249
+ cd src-python
250
+ box clean
251
+
252
+ # Rebuild executable
253
+ box package
254
+
255
+ # Install cargo if missing
256
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
257
+ ```
258
+
259
+ ## 🚀 Hugging Face Spaces Deployment
260
+
261
+ This project is configured for **Static HTML** deployment on Hugging Face Spaces (much simpler than Docker!):
262
+
263
+ **Manual Upload (Easiest):**
264
+ 1. Run `bun install && bun run build` locally
265
+ 2. Create a new Space with "Static HTML" SDK
266
+ 3. Upload all files from `build/` folder
267
+ 4. Your app is live!
268
+
269
+ **GitHub Integration:**
270
+ 1. Fork this repository
271
+ 2. Create a Space and connect your GitHub repo
272
+ 3. The Static HTML SDK will be auto-detected from the README frontmatter
273
+ 4. Push changes to auto-deploy
274
+
275
+ No Docker, no complex setup - just static files! 🎉
276
+
277
+ ## 📚 Additional Documentation
278
+
279
+ - [Docker Setup Guide](./DOCKER_README.md) - Detailed Docker instructions
280
+ - [Robot Architecture](./ROBOT_ARCHITECTURE.md) - System architecture overview
281
+ - [Robot Instancing Guide](./ROBOT_INSTANCING_README.md) - Multi-robot setup
282
+
283
+ ## 🤝 Contributing
284
+
285
+ 1. Fork the repository
286
+ 2. Create a feature branch
287
+ 3. Make your changes
288
+ 4. Test with Docker: `docker-compose up --build`
289
+ 5. Submit a pull request
290
+
291
+ ## 📄 License
292
+
293
+ This project is licensed under the MIT License.
294
+
295
+ ---
296
+
297
+ **Built with ❤️ for the robotics community** 🤖
bun.lock ADDED
The diff for this file is too large to render. See raw diff
 
components.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://next.shadcn-svelte.com/schema.json",
3
+ "tailwind": {
4
+ "css": "src/app.css",
5
+ "baseColor": "stone"
6
+ },
7
+ "aliases": {
8
+ "components": "@/components",
9
+ "utils": "$lib/utils",
10
+ "ui": "@/components/ui",
11
+ "hooks": "$lib/hooks",
12
+ "lib": "$lib"
13
+ },
14
+ "typescript": true,
15
+ "registry": "https://next.shadcn-svelte.com/registry"
16
+ }
eslint.config.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import prettier from "eslint-config-prettier";
2
+ import js from "@eslint/js";
3
+ import { includeIgnoreFile } from "@eslint/compat";
4
+ import svelte from "eslint-plugin-svelte";
5
+ import globals from "globals";
6
+ import { fileURLToPath } from "node:url";
7
+ import ts from "typescript-eslint";
8
+ import svelteConfig from "./svelte.config.js";
9
+
10
+ const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
11
+
12
+ export default ts.config(
13
+ includeIgnoreFile(gitignorePath),
14
+ js.configs.recommended,
15
+ ...ts.configs.recommended,
16
+ ...svelte.configs.recommended,
17
+ prettier,
18
+ ...svelte.configs.prettier,
19
+ {
20
+ languageOptions: {
21
+ globals: { ...globals.browser, ...globals.node }
22
+ },
23
+ rules: { "no-undef": "off" }
24
+ },
25
+ {
26
+ files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
27
+ languageOptions: {
28
+ parserOptions: {
29
+ projectService: true,
30
+ extraFileExtensions: [".svelte"],
31
+ parser: ts.parser,
32
+ svelteConfig
33
+ }
34
+ }
35
+ }
36
+ );
external/.gitkeep ADDED
File without changes
external/RobotHub-InferenceServer ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 1d17b329ca89abd535b88b07e5404aaead3a9c25
external/RobotHub-TransportServer ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit 8aedc84a7635fc0cbbd3a0671a5e1cf50616dad0
log.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ .venv/bin/python: can't open file '/Users/julienblanchon/Git/lerobot-arena/lerobot-arena/src-python-video/src/main.py': [Errno 2] No such file or directory
package.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "my-app",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite dev",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "prepare": "svelte-kit sync || echo ''",
11
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13
+ "format": "prettier --write .",
14
+ "lint": "prettier --check . && eslint ."
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/compat": "^1.2.9",
18
+ "@eslint/js": "^9.28.0",
19
+ "@iconify/json": "^2.2.346",
20
+ "@iconify/svelte": "^5.0.0",
21
+ "@iconify/tailwind4": "^1.0.6",
22
+ "@internationalized/date": "^3.8.2",
23
+ "@lucide/svelte": "^0.511.0",
24
+ "@sveltejs/adapter-auto": "^6.0.1",
25
+ "@sveltejs/adapter-static": "^3.0.8",
26
+ "@sveltejs/kit": "^2.21.2",
27
+ "@sveltejs/vite-plugin-svelte": "^5.1.0",
28
+ "@tailwindcss/vite": "^4.0.0",
29
+ "bits-ui": "^2.4.1",
30
+ "eslint": "^9.28.0",
31
+ "eslint-config-prettier": "^10.1.5",
32
+ "eslint-plugin-svelte": "^3.9.1",
33
+ "globals": "^16.2.0",
34
+ "layerchart": "1.0.11",
35
+ "mode-watcher": "^1.0.7",
36
+ "prettier": "^3.5.3",
37
+ "prettier-plugin-svelte": "^3.4.0",
38
+ "prettier-plugin-tailwindcss": "^0.6.11",
39
+ "svelte": "^5.33.17",
40
+ "svelte-check": "^4.2.1",
41
+ "svelte-sonner": "^1.0.4",
42
+ "tailwind-variants": "^1.0.0",
43
+ "tailwindcss": "^4.0.0",
44
+ "tw-animate-css": "^1.3.4",
45
+ "typescript": "^5.8.3",
46
+ "typescript-eslint": "^8.33.1",
47
+ "vaul-svelte": "^1.0.0-next.7",
48
+ "vite": "^6.3.5"
49
+ },
50
+ "dependencies": {
51
+ "@threlte/core": "^8.0.4",
52
+ "@threlte/extras": "^9.2.1",
53
+ "@types/three": "0.177.0",
54
+ "clsx": "^2.1.1",
55
+ "feetech.js": "file:./packages/feetech.js",
56
+ "@robohub/transport-server-client": "file:../backend/transport-server/client/js",
57
+ "@robohub/inference-server-client": "file:../backend/inference-server/client",
58
+ "tailwind-merge": "^3.3.0",
59
+ "three": "^0.177.0",
60
+ "threlte-uikit": "^1.1.0",
61
+ "zod": "^3.25.56"
62
+ }
63
+ }
packages/feetech.js/README.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # feetech.js
2
+
3
+ Control feetech servos through browser
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ # Install the package
9
+ npm install feetech.js
10
+ ```
11
+
12
+ ```javascript
13
+ import { scsServoSDK } from "feetech.js";
14
+
15
+ await scsServoSDK.connect();
16
+
17
+ const position = await scsServoSDK.readPosition(1);
18
+ console.log(position); // 1122
19
+ ```
20
+
21
+ ## Example usage:
22
+
23
+ - simple example: [test.html](./test.html)
24
+ - the bambot website: [bambot.org](https://bambot.org)
packages/feetech.js/debug.mjs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Debug configuration for feetech.js
3
+ * Set DEBUG_ENABLED to false to disable all console.log statements for performance
4
+ */
5
+ export const DEBUG_ENABLED = true; // Set to true to enable debug logging
6
+
7
+ /**
8
+ * Conditional logging function that respects the DEBUG_ENABLED flag
9
+ * @param {...any} args - Arguments to log
10
+ */
11
+ export const debugLog = (...args) => {
12
+ if (DEBUG_ENABLED) {
13
+ console.log(...args);
14
+ }
15
+ };
packages/feetech.js/index.d.ts ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type ConnectionOptions = {
2
+ baudRate?: number;
3
+ protocolEnd?: number;
4
+ };
5
+
6
+ export type ServoPositions = Map<number, number> | Record<number, number>;
7
+ export type ServoSpeeds = Map<number, number> | Record<number, number>;
8
+
9
+ export interface ScsServoSDK {
10
+ // Connection management
11
+ connect(options?: ConnectionOptions): Promise<true>;
12
+ disconnect(): Promise<true>;
13
+ isConnected(): boolean;
14
+
15
+ // Servo locking operations
16
+ lockServo(servoId: number): Promise<"success">;
17
+ unlockServo(servoId: number): Promise<"success">;
18
+ lockServos(servoIds: number[]): Promise<"success">;
19
+ unlockServos(servoIds: number[]): Promise<"success">;
20
+ lockServosForProduction(servoIds: number[]): Promise<"success">;
21
+ unlockServosForManualMovement(servoIds: number[]): Promise<"success">;
22
+
23
+ // Read operations (no locking needed)
24
+ readPosition(servoId: number): Promise<number>;
25
+ syncReadPositions(servoIds: number[]): Promise<Map<number, number>>;
26
+
27
+ // Write operations - LOCKED MODE (respects servo locks)
28
+ writePosition(servoId: number, position: number): Promise<"success">;
29
+ writeTorqueEnable(servoId: number, enable: boolean): Promise<"success">;
30
+
31
+ // Write operations - UNLOCKED MODE (temporary unlock for operation)
32
+ writePositionUnlocked(servoId: number, position: number): Promise<"success">;
33
+ writePositionAndDisableTorque(servoId: number, position: number, waitTimeMs?: number): Promise<"success">;
34
+ writeTorqueEnableUnlocked(servoId: number, enable: boolean): Promise<"success">;
35
+
36
+ // Sync write operations
37
+ syncWritePositions(servoPositions: ServoPositions): Promise<"success">;
38
+
39
+ // Configuration functions
40
+ setBaudRate(servoId: number, baudRateIndex: number): Promise<"success">;
41
+ setServoId(currentServoId: number, newServoId: number): Promise<"success">;
42
+ setWheelMode(servoId: number): Promise<"success">;
43
+ setPositionMode(servoId: number): Promise<"success">;
44
+ }
45
+
46
+ export const scsServoSDK: ScsServoSDK;
47
+
48
+ // Debug exports
49
+ export const DEBUG_ENABLED: boolean;
50
+ export function debugLog(message: string): void;
packages/feetech.js/index.mjs ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Import all functions from the unified scsServoSDK module
2
+ import {
3
+ connect,
4
+ disconnect,
5
+ isConnected,
6
+ lockServo,
7
+ unlockServo,
8
+ lockServos,
9
+ unlockServos,
10
+ lockServosForProduction,
11
+ unlockServosForManualMovement,
12
+ readPosition,
13
+ syncReadPositions,
14
+ writePosition,
15
+ writeTorqueEnable,
16
+ writePositionUnlocked,
17
+ writePositionAndDisableTorque,
18
+ writeTorqueEnableUnlocked,
19
+ syncWritePositions,
20
+ setBaudRate,
21
+ setServoId,
22
+ setWheelMode,
23
+ setPositionMode
24
+ } from "./scsServoSDK.mjs";
25
+
26
+ // Create the unified SCS servo SDK object
27
+ export const scsServoSDK = {
28
+ // Connection management
29
+ connect,
30
+ disconnect,
31
+ isConnected,
32
+
33
+ // Servo locking operations
34
+ lockServo,
35
+ unlockServo,
36
+ lockServos,
37
+ unlockServos,
38
+ lockServosForProduction,
39
+ unlockServosForManualMovement,
40
+
41
+ // Read operations (no locking needed)
42
+ readPosition,
43
+ syncReadPositions,
44
+
45
+ // Write operations - LOCKED MODE (respects servo locks)
46
+ writePosition,
47
+ writeTorqueEnable,
48
+
49
+ // Write operations - UNLOCKED MODE (temporary unlock for operation)
50
+ writePositionUnlocked,
51
+ writePositionAndDisableTorque,
52
+ writeTorqueEnableUnlocked,
53
+
54
+ // Sync write operations
55
+ syncWritePositions,
56
+
57
+ // Configuration functions
58
+ setBaudRate,
59
+ setServoId,
60
+ setWheelMode,
61
+ setPositionMode
62
+ };
63
+
64
+ // Export debug configuration for easy access
65
+ export { DEBUG_ENABLED, debugLog } from "./debug.mjs";
packages/feetech.js/lowLevelSDK.mjs ADDED
@@ -0,0 +1,1235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Import debug logging function
2
+ import { debugLog } from "./debug.mjs";
3
+
4
+ // Constants
5
+ export const BROADCAST_ID = 0xfe; // 254
6
+ export const MAX_ID = 0xfc; // 252
7
+
8
+ // Protocol instructions
9
+ export const INST_PING = 1;
10
+ export const INST_READ = 2;
11
+ export const INST_WRITE = 3;
12
+ export const INST_REG_WRITE = 4;
13
+ export const INST_ACTION = 5;
14
+ export const INST_SYNC_WRITE = 131; // 0x83
15
+ export const INST_SYNC_READ = 130; // 0x82
16
+ export const INST_STATUS = 85; // 0x55, status packet instruction (0x55)
17
+
18
+ // Communication results
19
+ export const COMM_SUCCESS = 0; // tx or rx packet communication success
20
+ export const COMM_PORT_BUSY = -1; // Port is busy (in use)
21
+ export const COMM_TX_FAIL = -2; // Failed transmit instruction packet
22
+ export const COMM_RX_FAIL = -3; // Failed get status packet
23
+ export const COMM_TX_ERROR = -4; // Incorrect instruction packet
24
+ export const COMM_RX_WAITING = -5; // Now receiving status packet
25
+ export const COMM_RX_TIMEOUT = -6; // There is no status packet
26
+ export const COMM_RX_CORRUPT = -7; // Incorrect status packet
27
+ export const COMM_NOT_AVAILABLE = -9;
28
+
29
+ // Packet constants
30
+ export const TXPACKET_MAX_LEN = 250;
31
+ export const RXPACKET_MAX_LEN = 250;
32
+
33
+ // Protocol Packet positions
34
+ export const PKT_HEADER0 = 0;
35
+ export const PKT_HEADER1 = 1;
36
+ export const PKT_ID = 2;
37
+ export const PKT_LENGTH = 3;
38
+ export const PKT_INSTRUCTION = 4;
39
+ export const PKT_ERROR = 4;
40
+ export const PKT_PARAMETER0 = 5;
41
+
42
+ // Protocol Error bits
43
+ export const ERRBIT_VOLTAGE = 1;
44
+ export const ERRBIT_ANGLE = 2;
45
+ export const ERRBIT_OVERHEAT = 4;
46
+ export const ERRBIT_OVERELE = 8;
47
+ export const ERRBIT_OVERLOAD = 32;
48
+
49
+ // Default settings
50
+ const DEFAULT_BAUDRATE = 1000000;
51
+ const LATENCY_TIMER = 16;
52
+
53
+ // Global protocol end state
54
+ let SCS_END = 0; // (STS/SMS=0, SCS=1)
55
+
56
+ // Utility functions for handling word operations
57
+ export function SCS_LOWORD(l) {
58
+ return l & 0xffff;
59
+ }
60
+
61
+ export function SCS_HIWORD(l) {
62
+ return (l >> 16) & 0xffff;
63
+ }
64
+
65
+ export function SCS_LOBYTE(w) {
66
+ if (SCS_END === 0) {
67
+ return w & 0xff;
68
+ } else {
69
+ return (w >> 8) & 0xff;
70
+ }
71
+ }
72
+
73
+ export function SCS_HIBYTE(w) {
74
+ if (SCS_END === 0) {
75
+ return (w >> 8) & 0xff;
76
+ } else {
77
+ return w & 0xff;
78
+ }
79
+ }
80
+
81
+ export function SCS_MAKEWORD(a, b) {
82
+ if (SCS_END === 0) {
83
+ return (a & 0xff) | ((b & 0xff) << 8);
84
+ } else {
85
+ return (b & 0xff) | ((a & 0xff) << 8);
86
+ }
87
+ }
88
+
89
+ export function SCS_MAKEDWORD(a, b) {
90
+ return (a & 0xffff) | ((b & 0xffff) << 16);
91
+ }
92
+
93
+ export function SCS_TOHOST(a, b) {
94
+ if (a & (1 << b)) {
95
+ return -(a & ~(1 << b));
96
+ } else {
97
+ return a;
98
+ }
99
+ }
100
+
101
+ export class PortHandler {
102
+ constructor() {
103
+ this.port = null;
104
+ this.reader = null;
105
+ this.writer = null;
106
+ this.isOpen = false;
107
+ this.isUsing = false;
108
+ this.baudrate = DEFAULT_BAUDRATE;
109
+ this.packetStartTime = 0;
110
+ this.packetTimeout = 0;
111
+ this.txTimePerByte = 0;
112
+ }
113
+
114
+ async requestPort() {
115
+ try {
116
+ this.port = await navigator.serial.requestPort();
117
+ return true;
118
+ } catch (err) {
119
+ console.error("Error requesting serial port:", err);
120
+ return false;
121
+ }
122
+ }
123
+
124
+ async openPort() {
125
+ if (!this.port) {
126
+ return false;
127
+ }
128
+
129
+ try {
130
+ await this.port.open({ baudRate: this.baudrate });
131
+ this.reader = this.port.readable.getReader();
132
+ this.writer = this.port.writable.getWriter();
133
+ this.isOpen = true;
134
+ this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
135
+ return true;
136
+ } catch (err) {
137
+ console.error("Error opening port:", err);
138
+ return false;
139
+ }
140
+ }
141
+
142
+ async closePort() {
143
+ if (this.reader) {
144
+ await this.reader.releaseLock();
145
+ this.reader = null;
146
+ }
147
+
148
+ if (this.writer) {
149
+ await this.writer.releaseLock();
150
+ this.writer = null;
151
+ }
152
+
153
+ if (this.port && this.isOpen) {
154
+ await this.port.close();
155
+ this.isOpen = false;
156
+ }
157
+ }
158
+
159
+ async clearPort() {
160
+ if (this.reader) {
161
+ await this.reader.releaseLock();
162
+ this.reader = this.port.readable.getReader();
163
+ }
164
+ }
165
+
166
+ setBaudRate(baudrate) {
167
+ this.baudrate = baudrate;
168
+ this.txTimePerByte = (1000.0 / this.baudrate) * 10.0;
169
+ return true;
170
+ }
171
+
172
+ getBaudRate() {
173
+ return this.baudrate;
174
+ }
175
+
176
+ async writePort(data) {
177
+ if (!this.isOpen || !this.writer) {
178
+ return 0;
179
+ }
180
+
181
+ try {
182
+ await this.writer.write(new Uint8Array(data));
183
+ return data.length;
184
+ } catch (err) {
185
+ console.error("Error writing to port:", err);
186
+ return 0;
187
+ }
188
+ }
189
+
190
+ async readPort(length) {
191
+ if (!this.isOpen || !this.reader) {
192
+ return [];
193
+ }
194
+
195
+ try {
196
+ // Increase timeout for more reliable data reception
197
+ const timeoutMs = 500;
198
+ let totalBytes = [];
199
+ const startTime = performance.now();
200
+
201
+ // Continue reading until we get enough bytes or timeout
202
+ while (totalBytes.length < length) {
203
+ // Create a timeout promise
204
+ const timeoutPromise = new Promise((resolve) => {
205
+ setTimeout(() => resolve({ value: new Uint8Array(), done: false, timeout: true }), 100); // Short internal timeout
206
+ });
207
+
208
+ // Race between reading and timeout
209
+ const result = await Promise.race([this.reader.read(), timeoutPromise]);
210
+
211
+ if (result.timeout) {
212
+ // Internal timeout - check if we've exceeded total timeout
213
+ if (performance.now() - startTime > timeoutMs) {
214
+ debugLog(`readPort total timeout after ${timeoutMs}ms`);
215
+ break;
216
+ }
217
+ continue; // Try reading again
218
+ }
219
+
220
+ if (result.done) {
221
+ debugLog("Reader done, stream closed");
222
+ break;
223
+ }
224
+
225
+ if (result.value.length === 0) {
226
+ // If there's no data but we haven't timed out yet, wait briefly and try again
227
+ await new Promise((resolve) => setTimeout(resolve, 10));
228
+
229
+ // Check if we've exceeded total timeout
230
+ if (performance.now() - startTime > timeoutMs) {
231
+ debugLog(`readPort total timeout after ${timeoutMs}ms`);
232
+ break;
233
+ }
234
+ continue;
235
+ }
236
+
237
+ // Add received bytes to our total
238
+ const newData = Array.from(result.value);
239
+ totalBytes.push(...newData);
240
+ debugLog(
241
+ `Read ${newData.length} bytes:`,
242
+ newData.map((b) => b.toString(16).padStart(2, "0")).join(" ")
243
+ );
244
+
245
+ // If we've got enough data, we can stop
246
+ if (totalBytes.length >= length) {
247
+ break;
248
+ }
249
+ }
250
+
251
+ return totalBytes;
252
+ } catch (err) {
253
+ console.error("Error reading from port:", err);
254
+ return [];
255
+ }
256
+ }
257
+
258
+ setPacketTimeout(packetLength) {
259
+ this.packetStartTime = this.getCurrentTime();
260
+ this.packetTimeout = this.txTimePerByte * packetLength + LATENCY_TIMER * 2.0 + 2.0;
261
+ }
262
+
263
+ setPacketTimeoutMillis(msec) {
264
+ this.packetStartTime = this.getCurrentTime();
265
+ this.packetTimeout = msec;
266
+ }
267
+
268
+ isPacketTimeout() {
269
+ if (this.getTimeSinceStart() > this.packetTimeout) {
270
+ this.packetTimeout = 0;
271
+ return true;
272
+ }
273
+ return false;
274
+ }
275
+
276
+ getCurrentTime() {
277
+ return performance.now();
278
+ }
279
+
280
+ getTimeSinceStart() {
281
+ const timeSince = this.getCurrentTime() - this.packetStartTime;
282
+ if (timeSince < 0.0) {
283
+ this.packetStartTime = this.getCurrentTime();
284
+ }
285
+ return timeSince;
286
+ }
287
+ }
288
+
289
+ export class PacketHandler {
290
+ constructor(protocolEnd = 0) {
291
+ SCS_END = protocolEnd;
292
+ debugLog(`PacketHandler initialized with protocol_end=${protocolEnd} (STS/SMS=0, SCS=1)`);
293
+ }
294
+
295
+ getProtocolVersion() {
296
+ return 1.0;
297
+ }
298
+
299
+ // 获取当前协议端设置的方法
300
+ getProtocolEnd() {
301
+ return SCS_END;
302
+ }
303
+
304
+ getTxRxResult(result) {
305
+ if (result === COMM_SUCCESS) {
306
+ return "[TxRxResult] Communication success!";
307
+ } else if (result === COMM_PORT_BUSY) {
308
+ return "[TxRxResult] Port is in use!";
309
+ } else if (result === COMM_TX_FAIL) {
310
+ return "[TxRxResult] Failed transmit instruction packet!";
311
+ } else if (result === COMM_RX_FAIL) {
312
+ return "[TxRxResult] Failed get status packet from device!";
313
+ } else if (result === COMM_TX_ERROR) {
314
+ return "[TxRxResult] Incorrect instruction packet!";
315
+ } else if (result === COMM_RX_WAITING) {
316
+ return "[TxRxResult] Now receiving status packet!";
317
+ } else if (result === COMM_RX_TIMEOUT) {
318
+ return "[TxRxResult] There is no status packet!";
319
+ } else if (result === COMM_RX_CORRUPT) {
320
+ return "[TxRxResult] Incorrect status packet!";
321
+ } else if (result === COMM_NOT_AVAILABLE) {
322
+ return "[TxRxResult] Protocol does not support this function!";
323
+ } else {
324
+ return "";
325
+ }
326
+ }
327
+
328
+ getRxPacketError(error) {
329
+ if (error & ERRBIT_VOLTAGE) {
330
+ return "[RxPacketError] Input voltage error!";
331
+ }
332
+ if (error & ERRBIT_ANGLE) {
333
+ return "[RxPacketError] Angle sen error!";
334
+ }
335
+ if (error & ERRBIT_OVERHEAT) {
336
+ return "[RxPacketError] Overheat error!";
337
+ }
338
+ if (error & ERRBIT_OVERELE) {
339
+ return "[RxPacketError] OverEle error!";
340
+ }
341
+ if (error & ERRBIT_OVERLOAD) {
342
+ return "[RxPacketError] Overload error!";
343
+ }
344
+ return "";
345
+ }
346
+
347
+ async txPacket(port, txpacket) {
348
+ let checksum = 0;
349
+ const totalPacketLength = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
350
+
351
+ if (port.isUsing) {
352
+ return COMM_PORT_BUSY;
353
+ }
354
+ port.isUsing = true;
355
+
356
+ // Check max packet length
357
+ if (totalPacketLength > TXPACKET_MAX_LEN) {
358
+ port.isUsing = false;
359
+ return COMM_TX_ERROR;
360
+ }
361
+
362
+ // Make packet header
363
+ txpacket[PKT_HEADER0] = 0xff;
364
+ txpacket[PKT_HEADER1] = 0xff;
365
+
366
+ // Add checksum to packet
367
+ for (let idx = 2; idx < totalPacketLength - 1; idx++) {
368
+ checksum += txpacket[idx];
369
+ }
370
+
371
+ txpacket[totalPacketLength - 1] = ~checksum & 0xff;
372
+
373
+ // TX packet
374
+ await port.clearPort();
375
+ const writtenPacketLength = await port.writePort(txpacket);
376
+ if (totalPacketLength !== writtenPacketLength) {
377
+ port.isUsing = false;
378
+ return COMM_TX_FAIL;
379
+ }
380
+
381
+ return COMM_SUCCESS;
382
+ }
383
+
384
+ async rxPacket(port) {
385
+ let rxpacket = [];
386
+ let result = COMM_RX_FAIL;
387
+
388
+ let waitLength = 6; // minimum length (HEADER0 HEADER1 ID LENGTH)
389
+
390
+ while (true) {
391
+ const data = await port.readPort(waitLength - rxpacket.length);
392
+ rxpacket.push(...data);
393
+
394
+ if (rxpacket.length >= waitLength) {
395
+ // Find packet header
396
+ let headerIndex = -1;
397
+ for (let i = 0; i < rxpacket.length - 1; i++) {
398
+ if (rxpacket[i] === 0xff && rxpacket[i + 1] === 0xff) {
399
+ headerIndex = i;
400
+ break;
401
+ }
402
+ }
403
+
404
+ if (headerIndex === 0) {
405
+ // Found at the beginning of the packet
406
+ if (rxpacket[PKT_ID] > 0xfd || rxpacket[PKT_LENGTH] > RXPACKET_MAX_LEN) {
407
+ // Invalid ID or length
408
+ rxpacket.shift();
409
+ continue;
410
+ }
411
+
412
+ // Recalculate expected packet length
413
+ if (waitLength !== rxpacket[PKT_LENGTH] + PKT_LENGTH + 1) {
414
+ waitLength = rxpacket[PKT_LENGTH] + PKT_LENGTH + 1;
415
+ continue;
416
+ }
417
+
418
+ if (rxpacket.length < waitLength) {
419
+ // Check timeout
420
+ if (port.isPacketTimeout()) {
421
+ result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
422
+ break;
423
+ }
424
+ continue;
425
+ }
426
+
427
+ // Calculate checksum
428
+ let checksum = 0;
429
+ for (let i = 2; i < waitLength - 1; i++) {
430
+ checksum += rxpacket[i];
431
+ }
432
+ checksum = ~checksum & 0xff;
433
+
434
+ // Verify checksum
435
+ if (rxpacket[waitLength - 1] === checksum) {
436
+ result = COMM_SUCCESS;
437
+ } else {
438
+ result = COMM_RX_CORRUPT;
439
+ }
440
+ break;
441
+ } else if (headerIndex > 0) {
442
+ // Remove unnecessary bytes before header
443
+ rxpacket = rxpacket.slice(headerIndex);
444
+ continue;
445
+ }
446
+ }
447
+
448
+ // Check timeout
449
+ if (port.isPacketTimeout()) {
450
+ result = rxpacket.length === 0 ? COMM_RX_TIMEOUT : COMM_RX_CORRUPT;
451
+ break;
452
+ }
453
+ }
454
+
455
+ if (result !== COMM_SUCCESS) {
456
+ debugLog(
457
+ `rxPacket result: ${result}, packet: ${rxpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
458
+ );
459
+ } else {
460
+ console.debug(
461
+ `rxPacket successful: ${rxpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
462
+ );
463
+ }
464
+ return [rxpacket, result];
465
+ }
466
+
467
+ async txRxPacket(port, txpacket) {
468
+ let rxpacket = null;
469
+ let error = 0;
470
+ let result = COMM_TX_FAIL;
471
+
472
+ try {
473
+ // Check if port is already in use
474
+ if (port.isUsing) {
475
+ debugLog("Port is busy, cannot start new transaction");
476
+ return [rxpacket, COMM_PORT_BUSY, error];
477
+ }
478
+
479
+ // TX packet
480
+ debugLog(
481
+ "Sending packet:",
482
+ txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")
483
+ );
484
+
485
+ // Remove retry logic and just send once
486
+ result = await this.txPacket(port, txpacket);
487
+ debugLog(`TX result: ${result}`);
488
+
489
+ if (result !== COMM_SUCCESS) {
490
+ debugLog(`TX failed with result: ${result}`);
491
+ port.isUsing = false; // Important: Release the port on TX failure
492
+ return [rxpacket, result, error];
493
+ }
494
+
495
+ // If ID is broadcast, no need to wait for status packet
496
+ if (txpacket[PKT_ID] === BROADCAST_ID) {
497
+ port.isUsing = false;
498
+ return [rxpacket, result, error];
499
+ }
500
+
501
+ // Set packet timeout
502
+ if (txpacket[PKT_INSTRUCTION] === INST_READ) {
503
+ const length = txpacket[PKT_PARAMETER0 + 1];
504
+ // For READ instructions, we expect response to include the data
505
+ port.setPacketTimeout(length + 10); // Add extra buffer
506
+ debugLog(`Set READ packet timeout for ${length + 10} bytes`);
507
+ } else {
508
+ // For other instructions, we expect a status packet
509
+ port.setPacketTimeout(10); // HEADER0 HEADER1 ID LENGTH ERROR CHECKSUM + buffer
510
+ debugLog(`Set standard packet timeout for 10 bytes`);
511
+ }
512
+
513
+ // RX packet - no retries, just attempt once
514
+ debugLog(`Receiving packet`);
515
+
516
+ // Clear port before receiving to ensure clean state
517
+ await port.clearPort();
518
+
519
+ const [rxpacketResult, resultRx] = await this.rxPacket(port);
520
+ rxpacket = rxpacketResult;
521
+
522
+ // Check if received packet is valid
523
+ if (resultRx !== COMM_SUCCESS) {
524
+ debugLog(`Rx failed with result: ${resultRx}`);
525
+ port.isUsing = false;
526
+ return [rxpacket, resultRx, error];
527
+ }
528
+
529
+ // Verify packet structure
530
+ if (rxpacket.length < 6) {
531
+ debugLog(`Received packet too short (${rxpacket.length} bytes)`);
532
+ port.isUsing = false;
533
+ return [rxpacket, COMM_RX_CORRUPT, error];
534
+ }
535
+
536
+ // Verify packet ID matches the sent ID
537
+ if (rxpacket[PKT_ID] !== txpacket[PKT_ID]) {
538
+ debugLog(
539
+ `Received packet ID (${rxpacket[PKT_ID]}) doesn't match sent ID (${txpacket[PKT_ID]})`
540
+ );
541
+ port.isUsing = false;
542
+ return [rxpacket, COMM_RX_CORRUPT, error];
543
+ }
544
+
545
+ // Packet looks valid
546
+ error = rxpacket[PKT_ERROR];
547
+ port.isUsing = false; // Release port on success
548
+ return [rxpacket, resultRx, error];
549
+ } catch (err) {
550
+ console.error("Exception in txRxPacket:", err);
551
+ port.isUsing = false; // Release port on exception
552
+ return [rxpacket, COMM_RX_FAIL, error];
553
+ }
554
+ }
555
+
556
+ async ping(port, scsId) {
557
+ let modelNumber = 0;
558
+ let error = 0;
559
+
560
+ try {
561
+ if (scsId >= BROADCAST_ID) {
562
+ debugLog(`Cannot ping broadcast ID ${scsId}`);
563
+ return [modelNumber, COMM_NOT_AVAILABLE, error];
564
+ }
565
+
566
+ const txpacket = new Array(6).fill(0);
567
+ txpacket[PKT_ID] = scsId;
568
+ txpacket[PKT_LENGTH] = 2;
569
+ txpacket[PKT_INSTRUCTION] = INST_PING;
570
+
571
+ debugLog(`Pinging servo ID ${scsId}...`);
572
+
573
+ // 发送ping指令并获取响应
574
+ const [rxpacket, result, err] = await this.txRxPacket(port, txpacket);
575
+ error = err;
576
+
577
+ // 与Python SDK保持一致:如���ping成功,尝试读取地址3的型号信息
578
+ if (result === COMM_SUCCESS) {
579
+ debugLog(`Ping to servo ID ${scsId} succeeded, reading model number from address 3`);
580
+ // 读取地址3的型号信息(2字节)
581
+ const [data, dataResult, dataError] = await this.readTxRx(port, scsId, 3, 2);
582
+
583
+ if (dataResult === COMM_SUCCESS && data && data.length >= 2) {
584
+ modelNumber = SCS_MAKEWORD(data[0], data[1]);
585
+ debugLog(`Model number read: ${modelNumber}`);
586
+ } else {
587
+ debugLog(`Could not read model number: ${this.getTxRxResult(dataResult)}`);
588
+ }
589
+ } else {
590
+ debugLog(`Ping failed with result: ${result}, error: ${error}`);
591
+ }
592
+
593
+ return [modelNumber, result, error];
594
+ } catch (error) {
595
+ console.error(`Exception in ping():`, error);
596
+ return [0, COMM_RX_FAIL, 0];
597
+ }
598
+ }
599
+
600
+ // Read methods
601
+ async readTxRx(port, scsId, address, length) {
602
+ if (scsId >= BROADCAST_ID) {
603
+ debugLog("Cannot read from broadcast ID");
604
+ return [[], COMM_NOT_AVAILABLE, 0];
605
+ }
606
+
607
+ // Create read packet
608
+ const txpacket = new Array(8).fill(0);
609
+ txpacket[PKT_ID] = scsId;
610
+ txpacket[PKT_LENGTH] = 4;
611
+ txpacket[PKT_INSTRUCTION] = INST_READ;
612
+ txpacket[PKT_PARAMETER0] = address;
613
+ txpacket[PKT_PARAMETER0 + 1] = length;
614
+
615
+ debugLog(`Reading ${length} bytes from address ${address} for servo ID ${scsId}`);
616
+
617
+ // Send packet and get response
618
+ const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
619
+
620
+ // Process the result
621
+ if (result !== COMM_SUCCESS) {
622
+ debugLog(`Read failed with result: ${result}, error: ${error}`);
623
+ return [[], result, error];
624
+ }
625
+
626
+ if (!rxpacket || rxpacket.length < PKT_PARAMETER0 + length) {
627
+ debugLog(
628
+ `Invalid response packet: expected at least ${PKT_PARAMETER0 + length} bytes, got ${rxpacket ? rxpacket.length : 0}`
629
+ );
630
+ return [[], COMM_RX_CORRUPT, error];
631
+ }
632
+
633
+ // Extract data from response
634
+ const data = [];
635
+ debugLog(
636
+ `Response packet length: ${rxpacket.length}, extracting ${length} bytes from offset ${PKT_PARAMETER0}`
637
+ );
638
+ debugLog(
639
+ `Response data bytes: ${rxpacket
640
+ .slice(PKT_PARAMETER0, PKT_PARAMETER0 + length)
641
+ .map((b) => "0x" + b.toString(16).padStart(2, "0"))
642
+ .join(" ")}`
643
+ );
644
+
645
+ for (let i = 0; i < length; i++) {
646
+ data.push(rxpacket[PKT_PARAMETER0 + i]);
647
+ }
648
+
649
+ debugLog(
650
+ `Successfully read ${length} bytes: ${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
651
+ );
652
+ return [data, result, error];
653
+ }
654
+
655
+ async read1ByteTxRx(port, scsId, address) {
656
+ const [data, result, error] = await this.readTxRx(port, scsId, address, 1);
657
+ const value = data.length > 0 ? data[0] : 0;
658
+ return [value, result, error];
659
+ }
660
+
661
+ async read2ByteTxRx(port, scsId, address) {
662
+ const [data, result, error] = await this.readTxRx(port, scsId, address, 2);
663
+
664
+ let value = 0;
665
+ if (data.length >= 2) {
666
+ value = SCS_MAKEWORD(data[0], data[1]);
667
+ }
668
+
669
+ return [value, result, error];
670
+ }
671
+
672
+ async read4ByteTxRx(port, scsId, address) {
673
+ const [data, result, error] = await this.readTxRx(port, scsId, address, 4);
674
+
675
+ let value = 0;
676
+ if (data.length >= 4) {
677
+ const loword = SCS_MAKEWORD(data[0], data[1]);
678
+ const hiword = SCS_MAKEWORD(data[2], data[3]);
679
+ value = SCS_MAKEDWORD(loword, hiword);
680
+
681
+ debugLog(
682
+ `read4ByteTxRx: data=${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
683
+ );
684
+ debugLog(
685
+ ` loword=${loword} (0x${loword.toString(16)}), hiword=${hiword} (0x${hiword.toString(16)})`
686
+ );
687
+ debugLog(` value=${value} (0x${value.toString(16)})`);
688
+ }
689
+
690
+ return [value, result, error];
691
+ }
692
+
693
+ // Write methods
694
+ async writeTxRx(port, scsId, address, length, data) {
695
+ if (scsId >= BROADCAST_ID) {
696
+ return [COMM_NOT_AVAILABLE, 0];
697
+ }
698
+
699
+ // Create write packet
700
+ const txpacket = new Array(length + 7).fill(0);
701
+ txpacket[PKT_ID] = scsId;
702
+ txpacket[PKT_LENGTH] = length + 3;
703
+ txpacket[PKT_INSTRUCTION] = INST_WRITE;
704
+ txpacket[PKT_PARAMETER0] = address;
705
+
706
+ // Add data
707
+ for (let i = 0; i < length; i++) {
708
+ txpacket[PKT_PARAMETER0 + 1 + i] = data[i] & 0xff;
709
+ }
710
+
711
+ // Send packet and get response
712
+ const [rxpacket, result, error] = await this.txRxPacket(port, txpacket);
713
+
714
+ return [result, error];
715
+ }
716
+
717
+ async write1ByteTxRx(port, scsId, address, data) {
718
+ const dataArray = [data & 0xff];
719
+ return await this.writeTxRx(port, scsId, address, 1, dataArray);
720
+ }
721
+
722
+ async write2ByteTxRx(port, scsId, address, data) {
723
+ const dataArray = [SCS_LOBYTE(data), SCS_HIBYTE(data)];
724
+ return await this.writeTxRx(port, scsId, address, 2, dataArray);
725
+ }
726
+
727
+ async write4ByteTxRx(port, scsId, address, data) {
728
+ const dataArray = [
729
+ SCS_LOBYTE(SCS_LOWORD(data)),
730
+ SCS_HIBYTE(SCS_LOWORD(data)),
731
+ SCS_LOBYTE(SCS_HIWORD(data)),
732
+ SCS_HIBYTE(SCS_HIWORD(data))
733
+ ];
734
+ return await this.writeTxRx(port, scsId, address, 4, dataArray);
735
+ }
736
+
737
+ // Add syncReadTx for GroupSyncRead functionality
738
+ async syncReadTx(port, startAddress, dataLength, param, paramLength) {
739
+ // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
740
+ const txpacket = new Array(paramLength + 8).fill(0);
741
+
742
+ txpacket[PKT_ID] = BROADCAST_ID;
743
+ txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
744
+ txpacket[PKT_INSTRUCTION] = INST_SYNC_READ;
745
+ txpacket[PKT_PARAMETER0] = startAddress;
746
+ txpacket[PKT_PARAMETER0 + 1] = dataLength;
747
+
748
+ // Add parameters
749
+ for (let i = 0; i < paramLength; i++) {
750
+ txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
751
+ }
752
+
753
+ // Calculate checksum
754
+ const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
755
+
756
+ // Add headers
757
+ txpacket[PKT_HEADER0] = 0xff;
758
+ txpacket[PKT_HEADER1] = 0xff;
759
+
760
+ // Calculate checksum
761
+ let checksum = 0;
762
+ for (let i = 2; i < totalLen - 1; i++) {
763
+ checksum += txpacket[i] & 0xff;
764
+ }
765
+ txpacket[totalLen - 1] = ~checksum & 0xff;
766
+
767
+ debugLog(
768
+ `SyncReadTx: ${txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
769
+ );
770
+
771
+ // Send packet
772
+ await port.clearPort();
773
+ const bytesWritten = await port.writePort(txpacket);
774
+ if (bytesWritten !== totalLen) {
775
+ return COMM_TX_FAIL;
776
+ }
777
+
778
+ // Set timeout based on expected response size
779
+ port.setPacketTimeout((6 + dataLength) * paramLength);
780
+
781
+ return COMM_SUCCESS;
782
+ }
783
+
784
+ // Add syncWriteTxOnly for GroupSyncWrite functionality
785
+ async syncWriteTxOnly(port, startAddress, dataLength, param, paramLength) {
786
+ // Create packet: HEADER0 HEADER1 ID LEN INST START_ADDR DATA_LEN PARAM... CHKSUM
787
+ const txpacket = new Array(paramLength + 8).fill(0);
788
+
789
+ txpacket[PKT_ID] = BROADCAST_ID;
790
+ txpacket[PKT_LENGTH] = paramLength + 4; // 4: INST START_ADDR DATA_LEN CHKSUM
791
+ txpacket[PKT_INSTRUCTION] = INST_SYNC_WRITE;
792
+ txpacket[PKT_PARAMETER0] = startAddress;
793
+ txpacket[PKT_PARAMETER0 + 1] = dataLength;
794
+
795
+ // Add parameters
796
+ for (let i = 0; i < paramLength; i++) {
797
+ txpacket[PKT_PARAMETER0 + 2 + i] = param[i];
798
+ }
799
+
800
+ // Calculate checksum
801
+ const totalLen = txpacket[PKT_LENGTH] + 4; // 4: HEADER0 HEADER1 ID LENGTH
802
+
803
+ // Add headers
804
+ txpacket[PKT_HEADER0] = 0xff;
805
+ txpacket[PKT_HEADER1] = 0xff;
806
+
807
+ // Calculate checksum
808
+ let checksum = 0;
809
+ for (let i = 2; i < totalLen - 1; i++) {
810
+ checksum += txpacket[i] & 0xff;
811
+ }
812
+ txpacket[totalLen - 1] = ~checksum & 0xff;
813
+
814
+ debugLog(
815
+ `SyncWriteTxOnly: ${txpacket.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
816
+ );
817
+
818
+ // Send packet - for sync write, we don't need a response
819
+ await port.clearPort();
820
+ const bytesWritten = await port.writePort(txpacket);
821
+ if (bytesWritten !== totalLen) {
822
+ return COMM_TX_FAIL;
823
+ }
824
+
825
+ return COMM_SUCCESS;
826
+ }
827
+
828
+ // 辅助方法:格式化数据包结构以方便调试
829
+ formatPacketStructure(packet) {
830
+ if (!packet || packet.length < 4) {
831
+ return "Invalid packet (too short)";
832
+ }
833
+
834
+ try {
835
+ let result = "";
836
+ result += `HEADER: ${packet[0].toString(16).padStart(2, "0")} ${packet[1].toString(16).padStart(2, "0")} | `;
837
+ result += `ID: ${packet[2]} | `;
838
+ result += `LENGTH: ${packet[3]} | `;
839
+
840
+ if (packet.length >= 5) {
841
+ result += `ERROR/INST: ${packet[4].toString(16).padStart(2, "0")} | `;
842
+ }
843
+
844
+ if (packet.length >= 6) {
845
+ result += "PARAMS: ";
846
+ for (let i = 5; i < packet.length - 1; i++) {
847
+ result += `${packet[i].toString(16).padStart(2, "0")} `;
848
+ }
849
+ result += `| CHECKSUM: ${packet[packet.length - 1].toString(16).padStart(2, "0")}`;
850
+ }
851
+
852
+ return result;
853
+ } catch (e) {
854
+ return "Error formatting packet: " + e.message;
855
+ }
856
+ }
857
+
858
+ /**
859
+ * 从响应包中解析舵机型号
860
+ * @param {Array} rxpacket - 响应数据包
861
+ * @returns {number} 舵机型号
862
+ */
863
+ parseModelNumber(rxpacket) {
864
+ if (!rxpacket || rxpacket.length < 7) {
865
+ return 0;
866
+ }
867
+
868
+ // 检查是否有参数字段
869
+ if (rxpacket.length <= PKT_PARAMETER0 + 1) {
870
+ return 0;
871
+ }
872
+
873
+ const param1 = rxpacket[PKT_PARAMETER0];
874
+ const param2 = rxpacket[PKT_PARAMETER0 + 1];
875
+
876
+ if (SCS_END === 0) {
877
+ // STS/SMS 协议的字节顺序
878
+ return SCS_MAKEWORD(param1, param2);
879
+ } else {
880
+ // SCS 协议的字节顺序
881
+ return SCS_MAKEWORD(param2, param1);
882
+ }
883
+ }
884
+
885
+ /**
886
+ * Verify packet header
887
+ * @param {Array} packet - The packet to verify
888
+ * @returns {Number} COMM_SUCCESS if packet is valid, error code otherwise
889
+ */
890
+ getPacketHeader(packet) {
891
+ if (!packet || packet.length < 4) {
892
+ return COMM_RX_CORRUPT;
893
+ }
894
+
895
+ // Check header
896
+ if (packet[PKT_HEADER0] !== 0xff || packet[PKT_HEADER1] !== 0xff) {
897
+ return COMM_RX_CORRUPT;
898
+ }
899
+
900
+ // Check ID validity
901
+ if (packet[PKT_ID] > 0xfd) {
902
+ return COMM_RX_CORRUPT;
903
+ }
904
+
905
+ // Check length
906
+ if (packet.length != packet[PKT_LENGTH] + 4) {
907
+ return COMM_RX_CORRUPT;
908
+ }
909
+
910
+ // Calculate checksum
911
+ let checksum = 0;
912
+ for (let i = 2; i < packet.length - 1; i++) {
913
+ checksum += packet[i] & 0xff;
914
+ }
915
+ checksum = ~checksum & 0xff;
916
+
917
+ // Verify checksum
918
+ if (packet[packet.length - 1] !== checksum) {
919
+ return COMM_RX_CORRUPT;
920
+ }
921
+
922
+ return COMM_SUCCESS;
923
+ }
924
+ }
925
+
926
+ /**
927
+ * GroupSyncRead class
928
+ * - This class is used to read multiple servos with the same control table address at once
929
+ */
930
+ export class GroupSyncRead {
931
+ constructor(port, ph, startAddress, dataLength) {
932
+ this.port = port;
933
+ this.ph = ph;
934
+ this.startAddress = startAddress;
935
+ this.dataLength = dataLength;
936
+
937
+ this.isAvailableServiceID = new Set();
938
+ this.dataDict = new Map();
939
+ this.param = [];
940
+ this.clearParam();
941
+ }
942
+
943
+ makeParam() {
944
+ this.param = [];
945
+ for (const id of this.isAvailableServiceID) {
946
+ this.param.push(id);
947
+ }
948
+ return this.param.length;
949
+ }
950
+
951
+ addParam(scsId) {
952
+ if (this.isAvailableServiceID.has(scsId)) {
953
+ return false;
954
+ }
955
+
956
+ this.isAvailableServiceID.add(scsId);
957
+ this.dataDict.set(scsId, new Array(this.dataLength).fill(0));
958
+ return true;
959
+ }
960
+
961
+ removeParam(scsId) {
962
+ if (!this.isAvailableServiceID.has(scsId)) {
963
+ return false;
964
+ }
965
+
966
+ this.isAvailableServiceID.delete(scsId);
967
+ this.dataDict.delete(scsId);
968
+ return true;
969
+ }
970
+
971
+ clearParam() {
972
+ this.isAvailableServiceID.clear();
973
+ this.dataDict.clear();
974
+ return true;
975
+ }
976
+
977
+ async txPacket() {
978
+ if (this.isAvailableServiceID.size === 0) {
979
+ return COMM_NOT_AVAILABLE;
980
+ }
981
+
982
+ const paramLength = this.makeParam();
983
+ return await this.ph.syncReadTx(
984
+ this.port,
985
+ this.startAddress,
986
+ this.dataLength,
987
+ this.param,
988
+ paramLength
989
+ );
990
+ }
991
+
992
+ async rxPacket() {
993
+ let result = COMM_RX_FAIL;
994
+
995
+ if (this.isAvailableServiceID.size === 0) {
996
+ return COMM_NOT_AVAILABLE;
997
+ }
998
+
999
+ // Set all servos' data as invalid
1000
+ for (const id of this.isAvailableServiceID) {
1001
+ this.dataDict.set(id, new Array(this.dataLength).fill(0));
1002
+ }
1003
+
1004
+ const [rxpacket, rxResult] = await this.ph.rxPacket(this.port);
1005
+ if (rxResult !== COMM_SUCCESS || !rxpacket || rxpacket.length === 0) {
1006
+ return rxResult;
1007
+ }
1008
+
1009
+ // More tolerant of packets with unexpected values in the PKT_ERROR field
1010
+ // Don't require INST_STATUS to be exactly 0x55
1011
+ debugLog(
1012
+ `GroupSyncRead rxPacket: ID=${rxpacket[PKT_ID]}, ERROR/INST field=0x${rxpacket[PKT_ERROR].toString(16)}`
1013
+ );
1014
+
1015
+ // Check if the packet matches any of the available IDs
1016
+ if (!this.isAvailableServiceID.has(rxpacket[PKT_ID])) {
1017
+ debugLog(
1018
+ `Received packet with ID ${rxpacket[PKT_ID]} which is not in the available IDs list`
1019
+ );
1020
+ return COMM_RX_CORRUPT;
1021
+ }
1022
+
1023
+ // Extract data for the matching ID
1024
+ const scsId = rxpacket[PKT_ID];
1025
+ const data = new Array(this.dataLength).fill(0);
1026
+
1027
+ // Extract the parameter data, which should start at PKT_PARAMETER0
1028
+ if (rxpacket.length < PKT_PARAMETER0 + this.dataLength) {
1029
+ debugLog(
1030
+ `Packet too short: expected ${PKT_PARAMETER0 + this.dataLength} bytes, got ${rxpacket.length}`
1031
+ );
1032
+ return COMM_RX_CORRUPT;
1033
+ }
1034
+
1035
+ for (let i = 0; i < this.dataLength; i++) {
1036
+ data[i] = rxpacket[PKT_PARAMETER0 + i];
1037
+ }
1038
+
1039
+ // Update the data dict
1040
+ this.dataDict.set(scsId, data);
1041
+ debugLog(
1042
+ `Updated data for servo ID ${scsId}: ${data.map((b) => "0x" + b.toString(16).padStart(2, "0")).join(" ")}`
1043
+ );
1044
+
1045
+ // Continue receiving until timeout or all data is received
1046
+ if (this.isAvailableServiceID.size > 1) {
1047
+ result = await this.rxPacket();
1048
+ } else {
1049
+ result = COMM_SUCCESS;
1050
+ }
1051
+
1052
+ return result;
1053
+ }
1054
+
1055
+ async txRxPacket() {
1056
+ try {
1057
+ // First check if port is being used
1058
+ if (this.port.isUsing) {
1059
+ debugLog("Port is busy, cannot start sync read operation");
1060
+ return COMM_PORT_BUSY;
1061
+ }
1062
+
1063
+ // Start the transmission
1064
+ debugLog("Starting sync read TX/RX operation...");
1065
+ let result = await this.txPacket();
1066
+ if (result !== COMM_SUCCESS) {
1067
+ debugLog(`Sync read TX failed with result: ${result}`);
1068
+ return result;
1069
+ }
1070
+
1071
+ // Get a single response with a standard timeout
1072
+ debugLog(`Attempting to receive a response...`);
1073
+
1074
+ // Receive a single response
1075
+ result = await this.rxPacket();
1076
+
1077
+ // Release port
1078
+ this.port.isUsing = false;
1079
+
1080
+ return result;
1081
+ } catch (error) {
1082
+ console.error("Exception in GroupSyncRead txRxPacket:", error);
1083
+ // Make sure port is released
1084
+ this.port.isUsing = false;
1085
+ return COMM_RX_FAIL;
1086
+ }
1087
+ }
1088
+
1089
+ isAvailable(scsId, address, dataLength) {
1090
+ if (!this.isAvailableServiceID.has(scsId)) {
1091
+ return false;
1092
+ }
1093
+
1094
+ const startAddr = this.startAddress;
1095
+ const endAddr = startAddr + this.dataLength - 1;
1096
+
1097
+ const reqStartAddr = address;
1098
+ const reqEndAddr = reqStartAddr + dataLength - 1;
1099
+
1100
+ if (reqStartAddr < startAddr || reqEndAddr > endAddr) {
1101
+ return false;
1102
+ }
1103
+
1104
+ const data = this.dataDict.get(scsId);
1105
+ if (!data || data.length === 0) {
1106
+ return false;
1107
+ }
1108
+
1109
+ return true;
1110
+ }
1111
+
1112
+ getData(scsId, address, dataLength) {
1113
+ if (!this.isAvailable(scsId, address, dataLength)) {
1114
+ return 0;
1115
+ }
1116
+
1117
+ const startAddr = this.startAddress;
1118
+ const data = this.dataDict.get(scsId);
1119
+
1120
+ // Calculate data offset
1121
+ const dataOffset = address - startAddr;
1122
+
1123
+ // Combine bytes according to dataLength
1124
+ switch (dataLength) {
1125
+ case 1:
1126
+ return data[dataOffset];
1127
+ case 2:
1128
+ return SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]);
1129
+ case 4:
1130
+ return SCS_MAKEDWORD(
1131
+ SCS_MAKEWORD(data[dataOffset], data[dataOffset + 1]),
1132
+ SCS_MAKEWORD(data[dataOffset + 2], data[dataOffset + 3])
1133
+ );
1134
+ default:
1135
+ return 0;
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ /**
1141
+ * GroupSyncWrite class
1142
+ * - This class is used to write multiple servos with the same control table address at once
1143
+ */
1144
+ export class GroupSyncWrite {
1145
+ constructor(port, ph, startAddress, dataLength) {
1146
+ this.port = port;
1147
+ this.ph = ph;
1148
+ this.startAddress = startAddress;
1149
+ this.dataLength = dataLength;
1150
+
1151
+ this.isAvailableServiceID = new Set();
1152
+ this.dataDict = new Map();
1153
+ this.param = [];
1154
+ this.clearParam();
1155
+ }
1156
+
1157
+ makeParam() {
1158
+ this.param = [];
1159
+ for (const id of this.isAvailableServiceID) {
1160
+ // Add ID to parameter
1161
+ this.param.push(id);
1162
+
1163
+ // Add data to parameter
1164
+ const data = this.dataDict.get(id);
1165
+ for (let i = 0; i < this.dataLength; i++) {
1166
+ this.param.push(data[i]);
1167
+ }
1168
+ }
1169
+ return this.param.length;
1170
+ }
1171
+
1172
+ addParam(scsId, data) {
1173
+ if (this.isAvailableServiceID.has(scsId)) {
1174
+ return false;
1175
+ }
1176
+
1177
+ if (data.length !== this.dataLength) {
1178
+ console.error(
1179
+ `Data length (${data.length}) doesn't match required length (${this.dataLength})`
1180
+ );
1181
+ return false;
1182
+ }
1183
+
1184
+ this.isAvailableServiceID.add(scsId);
1185
+ this.dataDict.set(scsId, data);
1186
+ return true;
1187
+ }
1188
+
1189
+ removeParam(scsId) {
1190
+ if (!this.isAvailableServiceID.has(scsId)) {
1191
+ return false;
1192
+ }
1193
+
1194
+ this.isAvailableServiceID.delete(scsId);
1195
+ this.dataDict.delete(scsId);
1196
+ return true;
1197
+ }
1198
+
1199
+ changeParam(scsId, data) {
1200
+ if (!this.isAvailableServiceID.has(scsId)) {
1201
+ return false;
1202
+ }
1203
+
1204
+ if (data.length !== this.dataLength) {
1205
+ console.error(
1206
+ `Data length (${data.length}) doesn't match required length (${this.dataLength})`
1207
+ );
1208
+ return false;
1209
+ }
1210
+
1211
+ this.dataDict.set(scsId, data);
1212
+ return true;
1213
+ }
1214
+
1215
+ clearParam() {
1216
+ this.isAvailableServiceID.clear();
1217
+ this.dataDict.clear();
1218
+ return true;
1219
+ }
1220
+
1221
+ async txPacket() {
1222
+ if (this.isAvailableServiceID.size === 0) {
1223
+ return COMM_NOT_AVAILABLE;
1224
+ }
1225
+
1226
+ const paramLength = this.makeParam();
1227
+ return await this.ph.syncWriteTxOnly(
1228
+ this.port,
1229
+ this.startAddress,
1230
+ this.dataLength,
1231
+ this.param,
1232
+ paramLength
1233
+ );
1234
+ }
1235
+ }
packages/feetech.js/package.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "feetech.js",
3
+ "version": "0.0.8",
4
+ "description": "javascript sdk for feetech servos",
5
+ "main": "index.mjs",
6
+ "files": [
7
+ "*.mjs",
8
+ "*.ts"
9
+ ],
10
+ "type": "module",
11
+ "engines": {
12
+ "node": ">=12.17.0"
13
+ },
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/julien-blanchon/RoboHub.git#main:frontend/packages/feetech.js"
20
+ },
21
+ "keywords": [
22
+ "feetech",
23
+ "sdk",
24
+ "js",
25
+ "javascript",
26
+ "sts3215",
27
+ "3215",
28
+ "scs",
29
+ "scs3215",
30
+ "st3215"
31
+ ],
32
+ "author": "timqian",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/julien-blanchon/RoboHub.git#main:frontend/packages/feetech.js"
36
+ },
37
+ "homepage": "https://github.com/julien-blanchon/RoboHub.git#main:frontend/packages/feetech.js"
38
+ }
packages/feetech.js/scsServoSDK.mjs ADDED
@@ -0,0 +1,1205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ PortHandler,
3
+ PacketHandler,
4
+ COMM_SUCCESS,
5
+ COMM_RX_TIMEOUT,
6
+ COMM_RX_CORRUPT,
7
+ COMM_TX_FAIL,
8
+ COMM_NOT_AVAILABLE,
9
+ SCS_LOBYTE,
10
+ SCS_HIBYTE,
11
+ SCS_MAKEWORD,
12
+ GroupSyncRead, // Import GroupSyncRead
13
+ GroupSyncWrite // Import GroupSyncWrite
14
+ } from "./lowLevelSDK.mjs";
15
+
16
+ // Import address constants from the correct file
17
+ import {
18
+ ADDR_SCS_PRESENT_POSITION,
19
+ ADDR_SCS_GOAL_POSITION,
20
+ ADDR_SCS_TORQUE_ENABLE,
21
+ ADDR_SCS_GOAL_ACC,
22
+ ADDR_SCS_GOAL_SPEED
23
+ } from "./scsservo_constants.mjs";
24
+
25
+ // Import debug logging function
26
+ import { debugLog } from "./debug.mjs";
27
+
28
+ // Define constants not present in scsservo_constants.mjs
29
+ const ADDR_SCS_MODE = 33;
30
+ const ADDR_SCS_LOCK = 55;
31
+ const ADDR_SCS_ID = 5; // Address for Servo ID
32
+ const ADDR_SCS_BAUD_RATE = 6; // Address for Baud Rate
33
+
34
+ // Module-level variables for handlers
35
+ let portHandler = null;
36
+ let packetHandler = null;
37
+
38
+ /**
39
+ * Unified Servo SDK with flexible locking control
40
+ * Supports both locked (respects servo locks) and unlocked (temporary unlock) operations
41
+ */
42
+
43
+ /**
44
+ * Connects to the serial port and initializes handlers.
45
+ * @param {object} [options] - Connection options.
46
+ * @param {number} [options.baudRate=1000000] - The baud rate for the serial connection.
47
+ * @param {number} [options.protocolEnd=0] - The protocol end setting (0 for STS/SMS, 1 for SCS).
48
+ * @returns {Promise<true>} Resolves with true on successful connection.
49
+ * @throws {Error} If connection fails or port cannot be opened/selected.
50
+ */
51
+ export async function connect(options = {}) {
52
+ if (portHandler && portHandler.isOpen) {
53
+ debugLog("Already connected to servo system.");
54
+ return true;
55
+ }
56
+
57
+ const { baudRate = 1000000, protocolEnd = 0 } = options;
58
+
59
+ try {
60
+ portHandler = new PortHandler();
61
+ const portRequested = await portHandler.requestPort();
62
+ if (!portRequested) {
63
+ portHandler = null;
64
+ throw new Error("Failed to select a serial port.");
65
+ }
66
+
67
+ portHandler.setBaudRate(baudRate);
68
+ const portOpened = await portHandler.openPort();
69
+ if (!portOpened) {
70
+ await portHandler.closePort().catch(console.error);
71
+ portHandler = null;
72
+ throw new Error(`Failed to open port at baudrate ${baudRate}.`);
73
+ }
74
+
75
+ packetHandler = new PacketHandler(protocolEnd);
76
+ debugLog(`Connected to servo system at ${baudRate} baud, protocol end: ${protocolEnd}.`);
77
+ return true;
78
+ } catch (err) {
79
+ console.error("Error during servo connection:", err);
80
+ if (portHandler) {
81
+ try {
82
+ await portHandler.closePort();
83
+ } catch (closeErr) {
84
+ console.error("Error closing port after connection failure:", closeErr);
85
+ }
86
+ }
87
+ portHandler = null;
88
+ packetHandler = null;
89
+ throw new Error(`Servo connection failed: ${err.message}`);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Disconnects from the serial port.
95
+ * @returns {Promise<true>} Resolves with true on successful disconnection.
96
+ * @throws {Error} If disconnection fails.
97
+ */
98
+ export async function disconnect() {
99
+ if (!portHandler || !portHandler.isOpen) {
100
+ debugLog("Already disconnected from servo system.");
101
+ return true;
102
+ }
103
+
104
+ try {
105
+ await portHandler.closePort();
106
+ portHandler = null;
107
+ packetHandler = null;
108
+ debugLog("Disconnected from servo system.");
109
+ return true;
110
+ } catch (err) {
111
+ console.error("Error during servo disconnection:", err);
112
+ portHandler = null;
113
+ packetHandler = null;
114
+ throw new Error(`Servo disconnection failed: ${err.message}`);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Checks if the SDK is currently connected.
120
+ * @returns {boolean} True if connected, false otherwise.
121
+ */
122
+ export function isConnected() {
123
+ return !!(portHandler && portHandler.isOpen && packetHandler);
124
+ }
125
+
126
+ /**
127
+ * Checks if the SDK is connected. Throws an error if not.
128
+ * @throws {Error} If not connected.
129
+ */
130
+ function checkConnection() {
131
+ if (!portHandler || !packetHandler) {
132
+ throw new Error("Not connected to servo system. Call connect() first.");
133
+ }
134
+ }
135
+
136
+ // =============================================================================
137
+ // SERVO LOCKING OPERATIONS
138
+ // =============================================================================
139
+
140
+ /**
141
+ * Locks a servo to prevent configuration changes.
142
+ * @param {number} servoId - The ID of the servo (1-252).
143
+ * @returns {Promise<"success">} Resolves with "success".
144
+ * @throws {Error} If not connected, write fails, or an exception occurs.
145
+ */
146
+ export async function lockServo(servoId) {
147
+ checkConnection();
148
+ try {
149
+ debugLog(`🔒 Locking servo ${servoId}...`);
150
+ const [result, error] = await packetHandler.write1ByteTxRx(
151
+ portHandler,
152
+ servoId,
153
+ ADDR_SCS_LOCK,
154
+ 1
155
+ );
156
+
157
+ if (result !== COMM_SUCCESS) {
158
+ throw new Error(
159
+ `Error locking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`
160
+ );
161
+ }
162
+ debugLog(`🔒 Servo ${servoId} locked successfully`);
163
+ return "success";
164
+ } catch (err) {
165
+ console.error(`Exception locking servo ${servoId}:`, err);
166
+ throw new Error(`Failed to lock servo ${servoId}: ${err.message}`);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Unlocks a servo to allow configuration changes.
172
+ * @param {number} servoId - The ID of the servo (1-252).
173
+ * @returns {Promise<"success">} Resolves with "success".
174
+ * @throws {Error} If not connected, write fails, or an exception occurs.
175
+ */
176
+ export async function unlockServo(servoId) {
177
+ checkConnection();
178
+ try {
179
+ debugLog(`🔓 Unlocking servo ${servoId}...`);
180
+ const [result, error] = await packetHandler.write1ByteTxRx(
181
+ portHandler,
182
+ servoId,
183
+ ADDR_SCS_LOCK,
184
+ 0
185
+ );
186
+
187
+ if (result !== COMM_SUCCESS) {
188
+ throw new Error(
189
+ `Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`
190
+ );
191
+ }
192
+ debugLog(`🔓 Servo ${servoId} unlocked successfully`);
193
+ return "success";
194
+ } catch (err) {
195
+ console.error(`Exception unlocking servo ${servoId}:`, err);
196
+ throw new Error(`Failed to unlock servo ${servoId}: ${err.message}`);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Locks multiple servos sequentially.
202
+ * @param {number[]} servoIds - Array of servo IDs to lock.
203
+ * @returns {Promise<"success">} Resolves with "success".
204
+ * @throws {Error} If any servo fails to lock.
205
+ */
206
+ export async function lockServos(servoIds) {
207
+ checkConnection();
208
+ debugLog(`🔒 Locking ${servoIds.length} servos: [${servoIds.join(', ')}]`);
209
+
210
+ // Lock servos sequentially to avoid port conflicts
211
+ for (const servoId of servoIds) {
212
+ await lockServo(servoId);
213
+ }
214
+
215
+ debugLog(`🔒 All ${servoIds.length} servos locked successfully`);
216
+ return "success";
217
+ }
218
+
219
+ /**
220
+ * Locks servos for production use by both locking configuration and enabling torque.
221
+ * This ensures servos are truly locked and controlled by the system.
222
+ * @param {number[]} servoIds - Array of servo IDs to lock for production.
223
+ * @returns {Promise<"success">} Resolves with "success".
224
+ * @throws {Error} If any servo fails to lock or enable torque.
225
+ */
226
+ export async function lockServosForProduction(servoIds) {
227
+ checkConnection();
228
+ debugLog(`🔒 Locking ${servoIds.length} servos for production use: [${servoIds.join(', ')}]`);
229
+
230
+ // Lock servos sequentially and enable torque for each
231
+ for (const servoId of servoIds) {
232
+ try {
233
+ debugLog(`🔒 Locking servo ${servoId} for production...`);
234
+
235
+ // 1. Lock the servo configuration
236
+ const [lockResult, lockError] = await packetHandler.write1ByteTxRx(
237
+ portHandler,
238
+ servoId,
239
+ ADDR_SCS_LOCK,
240
+ 1
241
+ );
242
+
243
+ if (lockResult !== COMM_SUCCESS) {
244
+ throw new Error(`Error locking servo ${servoId}: ${packetHandler.getTxRxResult(lockResult)}, Error: ${lockError}`);
245
+ }
246
+
247
+ // 2. Enable torque to make servo controllable
248
+ const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(
249
+ portHandler,
250
+ servoId,
251
+ ADDR_SCS_TORQUE_ENABLE,
252
+ 1
253
+ );
254
+
255
+ if (torqueResult !== COMM_SUCCESS) {
256
+ console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
257
+ // Don't throw here, locking is more important than torque enable
258
+ }
259
+
260
+ debugLog(`🔒 Servo ${servoId} locked and torque enabled for production`);
261
+ } catch (err) {
262
+ console.error(`Exception locking servo ${servoId} for production:`, err);
263
+ throw new Error(`Failed to lock servo ${servoId} for production: ${err.message}`);
264
+ }
265
+ }
266
+
267
+ debugLog(`🔒 All ${servoIds.length} servos locked for production successfully`);
268
+ return "success";
269
+ }
270
+
271
+ /**
272
+ * Unlocks multiple servos sequentially.
273
+ * @param {number[]} servoIds - Array of servo IDs to unlock.
274
+ * @returns {Promise<"success">} Resolves with "success".
275
+ * @throws {Error} If any servo fails to unlock.
276
+ */
277
+ export async function unlockServos(servoIds) {
278
+ checkConnection();
279
+ debugLog(`🔓 Unlocking ${servoIds.length} servos: [${servoIds.join(', ')}]`);
280
+
281
+ // Unlock servos sequentially to avoid port conflicts
282
+ for (const servoId of servoIds) {
283
+ await unlockServo(servoId);
284
+ }
285
+
286
+ debugLog(`🔓 All ${servoIds.length} servos unlocked successfully`);
287
+ return "success";
288
+ }
289
+
290
+ /**
291
+ * Safely unlocks servos for manual movement by unlocking configuration and disabling torque.
292
+ * This is the safest way to leave servos when disconnecting/cleaning up.
293
+ * @param {number[]} servoIds - Array of servo IDs to unlock safely.
294
+ * @returns {Promise<"success">} Resolves with "success".
295
+ * @throws {Error} If any servo fails to unlock or disable torque.
296
+ */
297
+ export async function unlockServosForManualMovement(servoIds) {
298
+ checkConnection();
299
+ debugLog(`🔓 Safely unlocking ${servoIds.length} servos for manual movement: [${servoIds.join(', ')}]`);
300
+
301
+ // Unlock servos sequentially and disable torque for each
302
+ for (const servoId of servoIds) {
303
+ try {
304
+ debugLog(`🔓 Safely unlocking servo ${servoId} for manual movement...`);
305
+
306
+ // 1. Disable torque first (makes servo freely movable)
307
+ const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(
308
+ portHandler,
309
+ servoId,
310
+ ADDR_SCS_TORQUE_ENABLE,
311
+ 0
312
+ );
313
+
314
+ if (torqueResult !== COMM_SUCCESS) {
315
+ console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
316
+ // Continue anyway, unlocking is more important
317
+ }
318
+
319
+ // 2. Unlock the servo configuration
320
+ const [unlockResult, unlockError] = await packetHandler.write1ByteTxRx(
321
+ portHandler,
322
+ servoId,
323
+ ADDR_SCS_LOCK,
324
+ 0
325
+ );
326
+
327
+ if (unlockResult !== COMM_SUCCESS) {
328
+ throw new Error(`Error unlocking servo ${servoId}: ${packetHandler.getTxRxResult(unlockResult)}, Error: ${unlockError}`);
329
+ }
330
+
331
+ debugLog(`🔓 Servo ${servoId} safely unlocked - torque disabled and configuration unlocked`);
332
+ } catch (err) {
333
+ console.error(`Exception safely unlocking servo ${servoId}:`, err);
334
+ throw new Error(`Failed to safely unlock servo ${servoId}: ${err.message}`);
335
+ }
336
+ }
337
+
338
+ debugLog(`🔓 All ${servoIds.length} servos safely unlocked for manual movement`);
339
+ return "success";
340
+ }
341
+
342
+ // =============================================================================
343
+ // READ OPERATIONS (No locking needed)
344
+ // =============================================================================
345
+
346
+ /**
347
+ * Reads the current position of a servo.
348
+ * @param {number} servoId - The ID of the servo (1-252).
349
+ * @returns {Promise<number>} Resolves with the position (0-4095).
350
+ * @throws {Error} If not connected, read fails, or an exception occurs.
351
+ */
352
+ export async function readPosition(servoId) {
353
+ checkConnection();
354
+ try {
355
+ const [position, result, error] = await packetHandler.read2ByteTxRx(
356
+ portHandler,
357
+ servoId,
358
+ ADDR_SCS_PRESENT_POSITION
359
+ );
360
+
361
+ if (result !== COMM_SUCCESS) {
362
+ throw new Error(
363
+ `Error reading position from servo ${servoId}: ${packetHandler.getTxRxResult(
364
+ result
365
+ )}, Error code: ${error}`
366
+ );
367
+ }
368
+ return position & 0xffff;
369
+ } catch (err) {
370
+ console.error(`Exception reading position from servo ${servoId}:`, err);
371
+ throw new Error(`Exception reading position from servo ${servoId}: ${err.message}`);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Reads the current baud rate index of a servo.
377
+ * @param {number} servoId - The ID of the servo (1-252).
378
+ * @returns {Promise<number>} Resolves with the baud rate index (0-7).
379
+ * @throws {Error} If not connected, read fails, or an exception occurs.
380
+ */
381
+ export async function readBaudRate(servoId) {
382
+ checkConnection();
383
+ try {
384
+ const [baudIndex, result, error] = await packetHandler.read1ByteTxRx(
385
+ portHandler,
386
+ servoId,
387
+ ADDR_SCS_BAUD_RATE
388
+ );
389
+
390
+ if (result !== COMM_SUCCESS) {
391
+ throw new Error(
392
+ `Error reading baud rate from servo ${servoId}: ${packetHandler.getTxRxResult(
393
+ result
394
+ )}, Error code: ${error}`
395
+ );
396
+ }
397
+ return baudIndex;
398
+ } catch (err) {
399
+ console.error(`Exception reading baud rate from servo ${servoId}:`, err);
400
+ throw new Error(`Exception reading baud rate from servo ${servoId}: ${err.message}`);
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Reads the current operating mode of a servo.
406
+ * @param {number} servoId - The ID of the servo (1-252).
407
+ * @returns {Promise<number>} Resolves with the mode (0 for position, 1 for wheel).
408
+ * @throws {Error} If not connected, read fails, or an exception occurs.
409
+ */
410
+ export async function readMode(servoId) {
411
+ checkConnection();
412
+ try {
413
+ const [modeValue, result, error] = await packetHandler.read1ByteTxRx(
414
+ portHandler,
415
+ servoId,
416
+ ADDR_SCS_MODE
417
+ );
418
+
419
+ if (result !== COMM_SUCCESS) {
420
+ throw new Error(
421
+ `Error reading mode from servo ${servoId}: ${packetHandler.getTxRxResult(
422
+ result
423
+ )}, Error code: ${error}`
424
+ );
425
+ }
426
+ return modeValue;
427
+ } catch (err) {
428
+ console.error(`Exception reading mode from servo ${servoId}:`, err);
429
+ throw new Error(`Exception reading mode from servo ${servoId}: ${err.message}`);
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Reads the current position of multiple servos synchronously.
435
+ * @param {number[]} servoIds - An array of servo IDs (1-252) to read from.
436
+ * @returns {Promise<Map<number, number>>} Resolves with a Map where keys are servo IDs and values are positions (0-4095).
437
+ * @throws {Error} If not connected or transmission fails completely.
438
+ */
439
+ export async function syncReadPositions(servoIds) {
440
+ checkConnection();
441
+ if (!Array.isArray(servoIds) || servoIds.length === 0) {
442
+ debugLog("Sync Read: No servo IDs provided.");
443
+ return new Map();
444
+ }
445
+
446
+ const startAddress = ADDR_SCS_PRESENT_POSITION;
447
+ const dataLength = 2;
448
+ const groupSyncRead = new GroupSyncRead(portHandler, packetHandler, startAddress, dataLength);
449
+ const positions = new Map();
450
+ const validIds = [];
451
+
452
+ // Add parameters for each valid servo ID
453
+ servoIds.forEach((id) => {
454
+ if (id >= 1 && id <= 252) {
455
+ if (groupSyncRead.addParam(id)) {
456
+ validIds.push(id);
457
+ } else {
458
+ console.warn(`Sync Read: Failed to add param for servo ID ${id} (maybe duplicate or invalid).`);
459
+ }
460
+ } else {
461
+ console.warn(`Sync Read: Invalid servo ID ${id} skipped.`);
462
+ }
463
+ });
464
+
465
+ if (validIds.length === 0) {
466
+ debugLog("Sync Read: No valid servo IDs to read.");
467
+ return new Map();
468
+ }
469
+
470
+ try {
471
+ let txResult = await groupSyncRead.txPacket();
472
+ if (txResult !== COMM_SUCCESS) {
473
+ throw new Error(`Sync Read txPacket failed: ${packetHandler.getTxRxResult(txResult)}`);
474
+ }
475
+
476
+ let rxResult = await groupSyncRead.rxPacket();
477
+ if (rxResult !== COMM_SUCCESS) {
478
+ console.warn(`Sync Read rxPacket overall result: ${packetHandler.getTxRxResult(rxResult)}. Checking individual servos.`);
479
+ }
480
+
481
+ const failedIds = [];
482
+ validIds.forEach((id) => {
483
+ const isAvailable = groupSyncRead.isAvailable(id, startAddress, dataLength);
484
+ if (isAvailable) {
485
+ const position = groupSyncRead.getData(id, startAddress, dataLength);
486
+ positions.set(id, position & 0xffff);
487
+ } else {
488
+ failedIds.push(id);
489
+ }
490
+ });
491
+
492
+ if (failedIds.length > 0) {
493
+ console.warn(`Sync Read: Data not available for servo IDs: ${failedIds.join(", ")}. Got ${positions.size}/${validIds.length} servos successfully.`);
494
+ }
495
+
496
+ return positions;
497
+ } catch (err) {
498
+ console.error("Exception during syncReadPositions:", err);
499
+ throw new Error(`Sync Read failed: ${err.message}`);
500
+ }
501
+ }
502
+
503
+ // =============================================================================
504
+ // WRITE OPERATIONS - LOCKED MODE (Respects servo locks)
505
+ // =============================================================================
506
+
507
+ /**
508
+ * Writes a target position to a servo (respects locks).
509
+ * Will fail if the servo is locked.
510
+ * @param {number} servoId - The ID of the servo (1-252).
511
+ * @param {number} position - The target position value (0-4095).
512
+ * @returns {Promise<"success">} Resolves with "success".
513
+ * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
514
+ */
515
+ export async function writePosition(servoId, position) {
516
+ checkConnection();
517
+ try {
518
+ if (position < 0 || position > 4095) {
519
+ throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
520
+ }
521
+ const targetPosition = Math.round(position);
522
+
523
+ const [result, error] = await packetHandler.write2ByteTxRx(
524
+ portHandler,
525
+ servoId,
526
+ ADDR_SCS_GOAL_POSITION,
527
+ targetPosition
528
+ );
529
+
530
+ if (result !== COMM_SUCCESS) {
531
+ throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
532
+ }
533
+ return "success";
534
+ } catch (err) {
535
+ console.error(`Exception writing position to servo ${servoId}:`, err);
536
+ throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`);
537
+ }
538
+ }
539
+
540
+ /**
541
+ * Enables or disables the torque of a servo (respects locks).
542
+ * @param {number} servoId - The ID of the servo (1-252).
543
+ * @param {boolean} enable - True to enable torque, false to disable.
544
+ * @returns {Promise<"success">} Resolves with "success".
545
+ * @throws {Error} If not connected, write fails, or an exception occurs.
546
+ */
547
+ export async function writeTorqueEnable(servoId, enable) {
548
+ checkConnection();
549
+ try {
550
+ const enableValue = enable ? 1 : 0;
551
+ const [result, error] = await packetHandler.write1ByteTxRx(
552
+ portHandler,
553
+ servoId,
554
+ ADDR_SCS_TORQUE_ENABLE,
555
+ enableValue
556
+ );
557
+
558
+ if (result !== COMM_SUCCESS) {
559
+ throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
560
+ }
561
+ return "success";
562
+ } catch (err) {
563
+ console.error(`Exception setting torque for servo ${servoId}:`, err);
564
+ throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`);
565
+ }
566
+ }
567
+
568
+ // =============================================================================
569
+ // WRITE OPERATIONS - UNLOCKED MODE (Temporary unlock for operation)
570
+ // =============================================================================
571
+
572
+ /**
573
+ * Helper to attempt locking a servo, logging errors without throwing.
574
+ * @param {number} servoId
575
+ */
576
+ async function tryLockServo(servoId) {
577
+ try {
578
+ await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
579
+ } catch (lockErr) {
580
+ console.error(`Failed to re-lock servo ${servoId}:`, lockErr);
581
+ }
582
+ }
583
+
584
+ /**
585
+ * Writes a target position to a servo with temporary unlocking.
586
+ * Temporarily unlocks the servo, writes the position, then locks it back.
587
+ * @param {number} servoId - The ID of the servo (1-252).
588
+ * @param {number} position - The target position value (0-4095).
589
+ * @returns {Promise<"success">} Resolves with "success".
590
+ * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
591
+ */
592
+ export async function writePositionUnlocked(servoId, position) {
593
+ checkConnection();
594
+ let unlocked = false;
595
+ try {
596
+ if (position < 0 || position > 4095) {
597
+ throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
598
+ }
599
+ const targetPosition = Math.round(position);
600
+
601
+ debugLog(`🔓 Temporarily unlocking servo ${servoId} for position write...`);
602
+
603
+ // 1. Unlock servo configuration first
604
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
605
+ if (resUnlock !== COMM_SUCCESS) {
606
+ debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
607
+ } else {
608
+ unlocked = true;
609
+ }
610
+
611
+ // 2. Write the position
612
+ const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition);
613
+ if (result !== COMM_SUCCESS) {
614
+ throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
615
+ }
616
+
617
+ // 3. Lock servo configuration back
618
+ if (unlocked) {
619
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
620
+ if (resLock !== COMM_SUCCESS) {
621
+ console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
622
+ } else {
623
+ unlocked = false;
624
+ }
625
+ }
626
+
627
+ return "success";
628
+ } catch (err) {
629
+ console.error(`Exception writing position to servo ${servoId}:`, err);
630
+ if (unlocked) {
631
+ await tryLockServo(servoId);
632
+ }
633
+ throw new Error(`Failed to write position to servo ${servoId}: ${err.message}`);
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Writes a target position and disables torque for manual movement.
639
+ * @param {number} servoId - The ID of the servo (1-252).
640
+ * @param {number} position - The target position value (0-4095).
641
+ * @param {number} waitTimeMs - Time to wait for servo to reach position (milliseconds).
642
+ * @returns {Promise<"success">} Resolves with "success".
643
+ * @throws {Error} If not connected, position is out of range, write fails, or an exception occurs.
644
+ */
645
+ export async function writePositionAndDisableTorque(servoId, position, waitTimeMs = 1500) {
646
+ checkConnection();
647
+ let unlocked = false;
648
+ try {
649
+ if (position < 0 || position > 4095) {
650
+ throw new Error(`Invalid position value ${position} for servo ${servoId}. Must be between 0 and 4095.`);
651
+ }
652
+ const targetPosition = Math.round(position);
653
+
654
+ debugLog(`🔓 Moving servo ${servoId} to position ${targetPosition}, waiting ${waitTimeMs}ms, then disabling torque...`);
655
+
656
+ // 1. Unlock servo configuration first
657
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
658
+ if (resUnlock !== COMM_SUCCESS) {
659
+ debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
660
+ } else {
661
+ unlocked = true;
662
+ }
663
+
664
+ // 2. Enable torque first
665
+ const [torqueEnableResult, torqueEnableError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 1);
666
+ if (torqueEnableResult !== COMM_SUCCESS) {
667
+ console.warn(`Warning: Failed to enable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueEnableResult)}, Error: ${torqueEnableError}`);
668
+ } else {
669
+ debugLog(`✅ Torque enabled for servo ${servoId}`);
670
+ }
671
+
672
+ // 3. Write the position
673
+ const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_POSITION, targetPosition);
674
+ if (result !== COMM_SUCCESS) {
675
+ throw new Error(`Error writing position to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
676
+ }
677
+
678
+ // 4. Wait for servo to reach position
679
+ debugLog(`⏳ Waiting ${waitTimeMs}ms for servo ${servoId} to reach position ${targetPosition}...`);
680
+ await new Promise(resolve => setTimeout(resolve, waitTimeMs));
681
+
682
+ // 5. Disable torque
683
+ const [torqueResult, torqueError] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, 0);
684
+ if (torqueResult !== COMM_SUCCESS) {
685
+ console.warn(`Warning: Failed to disable torque for servo ${servoId}: ${packetHandler.getTxRxResult(torqueResult)}, Error: ${torqueError}`);
686
+ } else {
687
+ debugLog(`✅ Torque disabled for servo ${servoId} - now movable by hand`);
688
+ }
689
+
690
+ // 6. Lock servo configuration back
691
+ if (unlocked) {
692
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
693
+ if (resLock !== COMM_SUCCESS) {
694
+ console.warn(`Warning: Failed to re-lock servo ${servoId} after position write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
695
+ } else {
696
+ unlocked = false;
697
+ }
698
+ }
699
+
700
+ return "success";
701
+ } catch (err) {
702
+ console.error(`Exception writing position and disabling torque for servo ${servoId}:`, err);
703
+ if (unlocked) {
704
+ await tryLockServo(servoId);
705
+ }
706
+ throw new Error(`Failed to write position and disable torque for servo ${servoId}: ${err.message}`);
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Enables or disables the torque of a servo with temporary unlocking.
712
+ * @param {number} servoId - The ID of the servo (1-252).
713
+ * @param {boolean} enable - True to enable torque, false to disable.
714
+ * @returns {Promise<"success">} Resolves with "success".
715
+ * @throws {Error} If not connected, write fails, or an exception occurs.
716
+ */
717
+ export async function writeTorqueEnableUnlocked(servoId, enable) {
718
+ checkConnection();
719
+ let unlocked = false;
720
+ try {
721
+ const enableValue = enable ? 1 : 0;
722
+
723
+ debugLog(`🔓 Temporarily unlocking servo ${servoId} for torque enable write...`);
724
+
725
+ // 1. Unlock servo configuration first
726
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
727
+ if (resUnlock !== COMM_SUCCESS) {
728
+ debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
729
+ } else {
730
+ unlocked = true;
731
+ }
732
+
733
+ // 2. Write the torque enable
734
+ const [result, error] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_TORQUE_ENABLE, enableValue);
735
+ if (result !== COMM_SUCCESS) {
736
+ throw new Error(`Error setting torque for servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error code: ${error}`);
737
+ }
738
+
739
+ // 3. Lock servo configuration back
740
+ if (unlocked) {
741
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
742
+ if (resLock !== COMM_SUCCESS) {
743
+ console.warn(`Warning: Failed to re-lock servo ${servoId} after torque enable write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
744
+ } else {
745
+ unlocked = false;
746
+ }
747
+ }
748
+
749
+ return "success";
750
+ } catch (err) {
751
+ console.error(`Exception setting torque for servo ${servoId}:`, err);
752
+ if (unlocked) {
753
+ await tryLockServo(servoId);
754
+ }
755
+ throw new Error(`Exception setting torque for servo ${servoId}: ${err.message}`);
756
+ }
757
+ }
758
+
759
+ // =============================================================================
760
+ // SYNC WRITE OPERATIONS
761
+ // =============================================================================
762
+
763
+ /**
764
+ * Writes target positions to multiple servos synchronously.
765
+ * @param {Map<number, number> | object} servoPositions - A Map or object where keys are servo IDs (1-252) and values are target positions (0-4095).
766
+ * @returns {Promise<"success">} Resolves with "success".
767
+ * @throws {Error} If not connected, any position is out of range, transmission fails, or an exception occurs.
768
+ */
769
+ export async function syncWritePositions(servoPositions) {
770
+ checkConnection();
771
+
772
+ const groupSyncWrite = new GroupSyncWrite(portHandler, packetHandler, ADDR_SCS_GOAL_POSITION, 2);
773
+ let paramAdded = false;
774
+
775
+ const entries = servoPositions instanceof Map ? servoPositions.entries() : Object.entries(servoPositions);
776
+
777
+ for (const [idStr, position] of entries) {
778
+ const servoId = parseInt(idStr, 10);
779
+ if (isNaN(servoId) || servoId < 1 || servoId > 252) {
780
+ throw new Error(`Invalid servo ID "${idStr}" in syncWritePositions.`);
781
+ }
782
+ if (position < 0 || position > 4095) {
783
+ throw new Error(`Invalid position value ${position} for servo ${servoId} in syncWritePositions. Must be between 0 and 4095.`);
784
+ }
785
+ const targetPosition = Math.round(position);
786
+ const data = [SCS_LOBYTE(targetPosition), SCS_HIBYTE(targetPosition)];
787
+
788
+ if (groupSyncWrite.addParam(servoId, data)) {
789
+ paramAdded = true;
790
+ } else {
791
+ console.warn(`Failed to add servo ${servoId} to sync write group (possibly duplicate).`);
792
+ }
793
+ }
794
+
795
+ if (!paramAdded) {
796
+ debugLog("Sync Write: No valid servo positions provided or added.");
797
+ return "success";
798
+ }
799
+
800
+ try {
801
+ const result = await groupSyncWrite.txPacket();
802
+ if (result !== COMM_SUCCESS) {
803
+ throw new Error(`Sync Write txPacket failed: ${packetHandler.getTxRxResult(result)}`);
804
+ }
805
+ return "success";
806
+ } catch (err) {
807
+ console.error("Exception during syncWritePositions:", err);
808
+ throw new Error(`Sync Write failed: ${err.message}`);
809
+ }
810
+ }
811
+
812
+ /**
813
+ * Writes a target speed for a servo in wheel mode.
814
+ * @param {number} servoId - The ID of the servo
815
+ * @param {number} speed - The target speed value (-10000 to 10000). Negative values indicate reverse direction. 0 stops the wheel.
816
+ * @returns {Promise<"success">} Resolves with "success".
817
+ * @throws {Error} If not connected, either write fails, or an exception occurs.
818
+ */
819
+ export async function writeWheelSpeed(servoId, speed) {
820
+ checkConnection();
821
+ let unlocked = false;
822
+ try {
823
+ const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed)));
824
+ let speedValue = Math.abs(clampedSpeed) & 0x7fff;
825
+
826
+ if (clampedSpeed < 0) {
827
+ speedValue |= 0x8000;
828
+ }
829
+
830
+ debugLog(`Temporarily unlocking servo ${servoId} for wheel speed write...`);
831
+
832
+ // 1. Unlock servo configuration first
833
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
834
+ if (resUnlock !== COMM_SUCCESS) {
835
+ debugLog(`Warning: Failed to unlock servo ${servoId}, trying direct write anyway...`);
836
+ } else {
837
+ unlocked = true;
838
+ }
839
+
840
+ // 2. Write the speed
841
+ const [result, error] = await packetHandler.write2ByteTxRx(portHandler, servoId, ADDR_SCS_GOAL_SPEED, speedValue);
842
+ if (result !== COMM_SUCCESS) {
843
+ throw new Error(`Error writing wheel speed to servo ${servoId}: ${packetHandler.getTxRxResult(result)}, Error: ${error}`);
844
+ }
845
+
846
+ // 3. Lock servo configuration back
847
+ if (unlocked) {
848
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
849
+ if (resLock !== COMM_SUCCESS) {
850
+ console.warn(`Warning: Failed to re-lock servo ${servoId} after wheel speed write: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
851
+ } else {
852
+ unlocked = false;
853
+ }
854
+ }
855
+
856
+ return "success";
857
+ } catch (err) {
858
+ console.error(`Exception writing wheel speed to servo ${servoId}:`, err);
859
+ if (unlocked) {
860
+ await tryLockServo(servoId);
861
+ }
862
+ throw new Error(`Exception writing wheel speed to servo ${servoId}: ${err.message}`);
863
+ }
864
+ }
865
+
866
+ /**
867
+ * Writes target speeds to multiple servos in wheel mode synchronously.
868
+ * @param {Map<number, number> | object} servoSpeeds - A Map or object where keys are servo IDs (1-252) and values are target speeds (-10000 to 10000).
869
+ * @returns {Promise<"success">} Resolves with "success".
870
+ * @throws {Error} If not connected, any speed is out of range, transmission fails, or an exception occurs.
871
+ */
872
+ export async function syncWriteWheelSpeed(servoSpeeds) {
873
+ checkConnection();
874
+
875
+ const groupSyncWrite = new GroupSyncWrite(
876
+ portHandler,
877
+ packetHandler,
878
+ ADDR_SCS_GOAL_SPEED,
879
+ 2 // Data length for speed (2 bytes)
880
+ );
881
+ let paramAdded = false;
882
+
883
+ const entries = servoSpeeds instanceof Map ? servoSpeeds.entries() : Object.entries(servoSpeeds);
884
+
885
+ // Second pass: Add valid parameters
886
+ for (const [idStr, speed] of entries) {
887
+ const servoId = parseInt(idStr, 10); // Already validated
888
+
889
+ if (isNaN(servoId) || servoId < 1 || servoId > 252) {
890
+ throw new Error(`Invalid servo ID "${idStr}" in syncWriteWheelSpeed.`);
891
+ }
892
+ if (speed < -10000 || speed > 10000) {
893
+ throw new Error(
894
+ `Invalid speed value ${speed} for servo ${servoId} in syncWriteWheelSpeed. Must be between -10000 and 10000.`
895
+ );
896
+ }
897
+
898
+ const clampedSpeed = Math.max(-10000, Math.min(10000, Math.round(speed))); // Ensure integer, already validated range
899
+ let speedValue = Math.abs(clampedSpeed) & 0x7fff; // Get absolute value, ensure within 15 bits
900
+
901
+ // Set the direction bit (MSB of the 16-bit value) if speed is negative
902
+ if (clampedSpeed < 0) {
903
+ speedValue |= 0x8000; // Set the 16th bit for reverse direction
904
+ }
905
+
906
+ const data = [SCS_LOBYTE(speedValue), SCS_HIBYTE(speedValue)];
907
+
908
+ if (groupSyncWrite.addParam(servoId, data)) {
909
+ paramAdded = true;
910
+ } else {
911
+ // This should ideally not happen if IDs are unique, but handle defensively
912
+ console.warn(
913
+ `Failed to add servo ${servoId} to sync write speed group (possibly duplicate).`
914
+ );
915
+ }
916
+ }
917
+
918
+ if (!paramAdded) {
919
+ debugLog("Sync Write Speed: No valid servo speeds provided or added.");
920
+ return "success"; // Nothing to write is considered success
921
+ }
922
+
923
+ try {
924
+ // Send the Sync Write instruction
925
+ const result = await groupSyncWrite.txPacket();
926
+ if (result !== COMM_SUCCESS) {
927
+ throw new Error(`Sync Write Speed txPacket failed: ${packetHandler.getTxRxResult(result)}`);
928
+ }
929
+ return "success";
930
+ } catch (err) {
931
+ console.error("Exception during syncWriteWheelSpeed:", err);
932
+ // Re-throw the original error or a new one wrapping it
933
+ throw new Error(`Sync Write Speed failed: ${err.message}`);
934
+ }
935
+ }
936
+
937
+ /**
938
+ * Sets the Baud Rate of a servo.
939
+ * NOTE: After changing the baud rate, you might need to disconnect and reconnect
940
+ * at the new baud rate to communicate with the servo further.
941
+ * @param {number} servoId - The current ID of the servo to configure (1-252).
942
+ * @param {number} baudRateIndex - The index representing the new baud rate (0-7).
943
+ * @returns {Promise<"success">} Resolves with "success".
944
+ * @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs.
945
+ */
946
+ export async function setBaudRate(servoId, baudRateIndex) {
947
+ checkConnection();
948
+
949
+ // Validate inputs
950
+ if (servoId < 1 || servoId > 252) {
951
+ throw new Error(`Invalid servo ID provided: ${servoId}. Must be between 1 and 252.`);
952
+ }
953
+ if (baudRateIndex < 0 || baudRateIndex > 7) {
954
+ throw new Error(`Invalid baudRateIndex: ${baudRateIndex}. Must be between 0 and 7.`);
955
+ }
956
+
957
+ let unlocked = false;
958
+ try {
959
+ debugLog(`Setting baud rate for servo ${servoId}: Index=${baudRateIndex}`);
960
+
961
+ // 1. Unlock servo configuration
962
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(
963
+ portHandler,
964
+ servoId,
965
+ ADDR_SCS_LOCK,
966
+ 0 // 0 to unlock
967
+ );
968
+ if (resUnlock !== COMM_SUCCESS) {
969
+ throw new Error(
970
+ `Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(
971
+ resUnlock
972
+ )}, Error: ${errUnlock}`
973
+ );
974
+ }
975
+ unlocked = true;
976
+
977
+ // 2. Write new Baud Rate index
978
+ const [resBaud, errBaud] = await packetHandler.write1ByteTxRx(
979
+ portHandler,
980
+ servoId,
981
+ ADDR_SCS_BAUD_RATE,
982
+ baudRateIndex
983
+ );
984
+ if (resBaud !== COMM_SUCCESS) {
985
+ throw new Error(
986
+ `Failed to write baud rate index ${baudRateIndex} to servo ${servoId}: ${packetHandler.getTxRxResult(
987
+ resBaud
988
+ )}, Error: ${errBaud}`
989
+ );
990
+ }
991
+
992
+ // 3. Lock servo configuration
993
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(
994
+ portHandler,
995
+ servoId,
996
+ ADDR_SCS_LOCK,
997
+ 1
998
+ );
999
+ if (resLock !== COMM_SUCCESS) {
1000
+ throw new Error(
1001
+ `Failed to lock servo ${servoId} after setting baud rate: ${packetHandler.getTxRxResult(
1002
+ resLock
1003
+ )}, Error: ${errLock}.`
1004
+ );
1005
+ }
1006
+ unlocked = false; // Successfully locked
1007
+
1008
+ debugLog(
1009
+ `Successfully set baud rate for servo ${servoId}. Index: ${baudRateIndex}. Remember to potentially reconnect with the new baud rate.`
1010
+ );
1011
+ return "success";
1012
+ } catch (err) {
1013
+ console.error(`Exception during setBaudRate for servo ID ${servoId}:`, err);
1014
+ if (unlocked) {
1015
+ await tryLockServo(servoId);
1016
+ }
1017
+ throw new Error(`Failed to set baud rate for servo ${servoId}: ${err.message}`);
1018
+ }
1019
+ }
1020
+
1021
+ /**
1022
+ * Sets the ID of a servo.
1023
+ * NOTE: Changing the ID requires using the new ID for subsequent commands.
1024
+ * @param {number} currentServoId - The current ID of the servo to configure (1-252).
1025
+ * @param {number} newServoId - The new ID to set for the servo (1-252).
1026
+ * @returns {Promise<"success">} Resolves with "success".
1027
+ * @throws {Error} If not connected, input is invalid, any step fails, or an exception occurs.
1028
+ */
1029
+ export async function setServoId(currentServoId, newServoId) {
1030
+ checkConnection();
1031
+
1032
+ // Validate inputs
1033
+ if (currentServoId < 1 || currentServoId > 252 || newServoId < 1 || newServoId > 252) {
1034
+ throw new Error(
1035
+ `Invalid servo ID provided. Current: ${currentServoId}, New: ${newServoId}. Must be between 1 and 252.`
1036
+ );
1037
+ }
1038
+
1039
+ if (currentServoId === newServoId) {
1040
+ debugLog(`Servo ID is already ${newServoId}. No change needed.`);
1041
+ return "success";
1042
+ }
1043
+
1044
+ let unlocked = false;
1045
+ let idWritten = false;
1046
+ try {
1047
+ debugLog(`Setting servo ID: From ${currentServoId} to ${newServoId}`);
1048
+
1049
+ // 1. Unlock servo configuration (using current ID)
1050
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(
1051
+ portHandler,
1052
+ currentServoId,
1053
+ ADDR_SCS_LOCK,
1054
+ 0 // 0 to unlock
1055
+ );
1056
+ if (resUnlock !== COMM_SUCCESS) {
1057
+ throw new Error(
1058
+ `Failed to unlock servo ${currentServoId}: ${packetHandler.getTxRxResult(
1059
+ resUnlock
1060
+ )}, Error: ${errUnlock}`
1061
+ );
1062
+ }
1063
+ unlocked = true;
1064
+
1065
+ // 2. Write new Servo ID (using current ID)
1066
+ const [resId, errId] = await packetHandler.write1ByteTxRx(
1067
+ portHandler,
1068
+ currentServoId,
1069
+ ADDR_SCS_ID,
1070
+ newServoId
1071
+ );
1072
+ if (resId !== COMM_SUCCESS) {
1073
+ throw new Error(
1074
+ `Failed to write new ID ${newServoId} to servo ${currentServoId}: ${packetHandler.getTxRxResult(
1075
+ resId
1076
+ )}, Error: ${errId}`
1077
+ );
1078
+ }
1079
+ idWritten = true;
1080
+
1081
+ // 3. Lock servo configuration (using NEW ID)
1082
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(
1083
+ portHandler,
1084
+ newServoId, // Use NEW ID here
1085
+ ADDR_SCS_LOCK,
1086
+ 1 // 1 to lock
1087
+ );
1088
+ if (resLock !== COMM_SUCCESS) {
1089
+ // ID was likely changed, but lock failed. Critical state.
1090
+ throw new Error(
1091
+ `Failed to lock servo with new ID ${newServoId}: ${packetHandler.getTxRxResult(
1092
+ resLock
1093
+ )}, Error: ${errLock}. Configuration might be incomplete.`
1094
+ );
1095
+ }
1096
+ unlocked = false; // Successfully locked with new ID
1097
+
1098
+ debugLog(
1099
+ `Successfully set servo ID from ${currentServoId} to ${newServoId}. Remember to use the new ID for future commands.`
1100
+ );
1101
+ return "success";
1102
+ } catch (err) {
1103
+ console.error(`Exception during setServoId for current ID ${currentServoId}:`, err);
1104
+ if (unlocked) {
1105
+ // If unlock succeeded but subsequent steps failed, attempt to re-lock.
1106
+ // If ID write failed, use current ID. If ID write succeeded but lock failed, use new ID.
1107
+ const idToLock = idWritten ? newServoId : currentServoId;
1108
+ console.warn(`Attempting to re-lock servo using ID ${idToLock}...`);
1109
+ await tryLockServo(idToLock);
1110
+ }
1111
+ throw new Error(
1112
+ `Failed to set servo ID from ${currentServoId} to ${newServoId}: ${err.message}`
1113
+ );
1114
+ }
1115
+ }
1116
+
1117
+ // =============================================================================
1118
+ // LEGACY COMPATIBILITY FUNCTIONS (for backward compatibility)
1119
+ // =============================================================================
1120
+
1121
+ /**
1122
+ * Sets a servo to wheel mode (continuous rotation) with unlocking.
1123
+ * @param {number} servoId - The ID of the servo (1-252).
1124
+ * @returns {Promise<"success">} Resolves with "success".
1125
+ * @throws {Error} If not connected, any step fails, or an exception occurs.
1126
+ */
1127
+ export async function setWheelMode(servoId) {
1128
+ checkConnection();
1129
+ let unlocked = false;
1130
+ try {
1131
+ debugLog(`Setting servo ${servoId} to wheel mode...`);
1132
+
1133
+ // 1. Unlock servo configuration
1134
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
1135
+ if (resUnlock !== COMM_SUCCESS) {
1136
+ throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`);
1137
+ }
1138
+ unlocked = true;
1139
+
1140
+ // 2. Set mode to 1 (Wheel/Speed mode)
1141
+ const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 1);
1142
+ if (resMode !== COMM_SUCCESS) {
1143
+ throw new Error(`Failed to set wheel mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`);
1144
+ }
1145
+
1146
+ // 3. Lock servo configuration
1147
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
1148
+ if (resLock !== COMM_SUCCESS) {
1149
+ throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
1150
+ }
1151
+ unlocked = false;
1152
+
1153
+ debugLog(`Successfully set servo ${servoId} to wheel mode.`);
1154
+ return "success";
1155
+ } catch (err) {
1156
+ console.error(`Exception setting wheel mode for servo ${servoId}:`, err);
1157
+ if (unlocked) {
1158
+ await tryLockServo(servoId);
1159
+ }
1160
+ throw new Error(`Failed to set wheel mode for servo ${servoId}: ${err.message}`);
1161
+ }
1162
+ }
1163
+
1164
+ /**
1165
+ * Sets a servo back to position control mode from wheel mode.
1166
+ * @param {number} servoId - The ID of the servo (1-252).
1167
+ * @returns {Promise<"success">} Resolves with "success".
1168
+ * @throws {Error} If not connected, any step fails, or an exception occurs.
1169
+ */
1170
+ export async function setPositionMode(servoId) {
1171
+ checkConnection();
1172
+ let unlocked = false;
1173
+ try {
1174
+ debugLog(`Setting servo ${servoId} back to position mode...`);
1175
+
1176
+ // 1. Unlock servo configuration
1177
+ const [resUnlock, errUnlock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 0);
1178
+ if (resUnlock !== COMM_SUCCESS) {
1179
+ throw new Error(`Failed to unlock servo ${servoId}: ${packetHandler.getTxRxResult(resUnlock)}, Error: ${errUnlock}`);
1180
+ }
1181
+ unlocked = true;
1182
+
1183
+ // 2. Set mode to 0 (Position/Servo mode)
1184
+ const [resMode, errMode] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_MODE, 0);
1185
+ if (resMode !== COMM_SUCCESS) {
1186
+ throw new Error(`Failed to set position mode for servo ${servoId}: ${packetHandler.getTxRxResult(resMode)}, Error: ${errMode}`);
1187
+ }
1188
+
1189
+ // 3. Lock servo configuration
1190
+ const [resLock, errLock] = await packetHandler.write1ByteTxRx(portHandler, servoId, ADDR_SCS_LOCK, 1);
1191
+ if (resLock !== COMM_SUCCESS) {
1192
+ throw new Error(`Failed to lock servo ${servoId} after setting mode: ${packetHandler.getTxRxResult(resLock)}, Error: ${errLock}`);
1193
+ }
1194
+ unlocked = false;
1195
+
1196
+ debugLog(`Successfully set servo ${servoId} back to position mode.`);
1197
+ return "success";
1198
+ } catch (err) {
1199
+ console.error(`Exception setting position mode for servo ${servoId}:`, err);
1200
+ if (unlocked) {
1201
+ await tryLockServo(servoId);
1202
+ }
1203
+ throw new Error(`Failed to set position mode for servo ${servoId}: ${err.message}`);
1204
+ }
1205
+ }
packages/feetech.js/scsservo_constants.mjs ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Constants for FeetTech SCS servos
2
+
3
+ // Constants
4
+ export const BROADCAST_ID = 0xfe; // 254
5
+ export const MAX_ID = 0xfc; // 252
6
+
7
+ // Protocol instructions
8
+ export const INST_PING = 1;
9
+ export const INST_READ = 2;
10
+ export const INST_WRITE = 3;
11
+ export const INST_REG_WRITE = 4;
12
+ export const INST_ACTION = 5;
13
+ export const INST_SYNC_WRITE = 131; // 0x83
14
+ export const INST_SYNC_READ = 130; // 0x82
15
+ export const INST_STATUS = 85; // 0x55, status packet instruction (0x55)
16
+
17
+ // Communication results
18
+ export const COMM_SUCCESS = 0; // tx or rx packet communication success
19
+ export const COMM_PORT_BUSY = -1; // Port is busy (in use)
20
+ export const COMM_TX_FAIL = -2; // Failed transmit instruction packet
21
+ export const COMM_RX_FAIL = -3; // Failed get status packet
22
+ export const COMM_TX_ERROR = -4; // Incorrect instruction packet
23
+ export const COMM_RX_WAITING = -5; // Now receiving status packet
24
+ export const COMM_RX_TIMEOUT = -6; // There is no status packet
25
+ export const COMM_RX_CORRUPT = -7; // Incorrect status packet
26
+ export const COMM_NOT_AVAILABLE = -9;
27
+
28
+ // Packet constants
29
+ export const TXPACKET_MAX_LEN = 250;
30
+ export const RXPACKET_MAX_LEN = 250;
31
+
32
+ // Protocol Packet positions
33
+ export const PKT_HEADER0 = 0;
34
+ export const PKT_HEADER1 = 1;
35
+ export const PKT_ID = 2;
36
+ export const PKT_LENGTH = 3;
37
+ export const PKT_INSTRUCTION = 4;
38
+ export const PKT_ERROR = 4;
39
+ export const PKT_PARAMETER0 = 5;
40
+
41
+ // Protocol Error bits
42
+ export const ERRBIT_VOLTAGE = 1;
43
+ export const ERRBIT_ANGLE = 2;
44
+ export const ERRBIT_OVERHEAT = 4;
45
+ export const ERRBIT_OVERELE = 8;
46
+ export const ERRBIT_OVERLOAD = 32;
47
+
48
+ // Control table addresses (SCS servos)
49
+ export const ADDR_SCS_TORQUE_ENABLE = 40;
50
+ export const ADDR_SCS_GOAL_ACC = 41;
51
+ export const ADDR_SCS_GOAL_POSITION = 42;
52
+ export const ADDR_SCS_GOAL_SPEED = 46;
53
+ export const ADDR_SCS_PRESENT_POSITION = 56;
packages/feetech.js/test.html ADDED
@@ -0,0 +1,770 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Feetech Servo Test</title>
7
+ <style>
8
+ body {
9
+ font-family: sans-serif;
10
+ line-height: 1.6;
11
+ padding: 20px;
12
+ }
13
+ .container {
14
+ max-width: 800px;
15
+ margin: auto;
16
+ }
17
+ .section {
18
+ border: 1px solid #ccc;
19
+ padding: 15px;
20
+ margin-bottom: 20px;
21
+ border-radius: 5px;
22
+ }
23
+ h2 {
24
+ margin-top: 0;
25
+ }
26
+ label {
27
+ display: inline-block;
28
+ min-width: 100px;
29
+ margin-bottom: 5px;
30
+ }
31
+ input[type="number"],
32
+ input[type="text"] {
33
+ width: 100px;
34
+ padding: 5px;
35
+ margin-right: 10px;
36
+ margin-bottom: 10px;
37
+ }
38
+ button {
39
+ padding: 8px 15px;
40
+ margin-right: 10px;
41
+ cursor: pointer;
42
+ }
43
+ pre {
44
+ background-color: #f4f4f4;
45
+ padding: 10px;
46
+ border: 1px solid #ddd;
47
+ border-radius: 3px;
48
+ white-space: pre-wrap;
49
+ word-wrap: break-word;
50
+ }
51
+ .status {
52
+ font-weight: bold;
53
+ }
54
+ .success {
55
+ color: green;
56
+ }
57
+ .error {
58
+ color: red;
59
+ }
60
+ .log-area {
61
+ margin-top: 10px;
62
+ }
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <div class="container">
67
+ <h1>Feetech Servo Test Page</h1>
68
+
69
+ <details class="section">
70
+ <summary>Key Concepts</summary>
71
+ <p>Understanding these parameters is crucial for controlling Feetech servos:</p>
72
+ <ul>
73
+ <li>
74
+ <strong>Mode:</strong> Determines the servo's primary function.
75
+ <ul>
76
+ <li>
77
+ <code>Mode 0</code>: Position/Servo Mode. The servo moves to and holds a specific
78
+ angular position.
79
+ </li>
80
+ <li>
81
+ <code>Mode 1</code>: Wheel/Speed Mode. The servo rotates continuously at a specified
82
+ speed and direction, like a motor.
83
+ </li>
84
+ </ul>
85
+ Changing the mode requires unlocking, writing the mode value (0 or 1), and locking the
86
+ configuration.
87
+ </li>
88
+ <li>
89
+ <strong>Position:</strong> In Position Mode (Mode 0), this value represents the target
90
+ or current angular position of the servo's output shaft.
91
+ <ul>
92
+ <li>
93
+ Range: Typically <code>0</code> to <code>4095</code> (representing a 12-bit
94
+ resolution).
95
+ </li>
96
+ <li>
97
+ Meaning: Corresponds to the servo's rotational range (e.g., 0-360 degrees or 0-270
98
+ degrees, depending on the specific servo model). <code>0</code> is one end of the
99
+ range, <code>4095</code> is the other.
100
+ </li>
101
+ </ul>
102
+ </li>
103
+ <li>
104
+ <strong>Speed (Wheel Mode):</strong> In Wheel Mode (Mode 1), this value controls the
105
+ rotational speed and direction.
106
+ <ul>
107
+ <li>
108
+ Range: Typically <code>-2500</code> to <code>+2500</code>. (Note: Some documentation
109
+ might mention -1023 to +1023, but the SDK example uses a wider range).
110
+ </li>
111
+ <li>
112
+ Meaning: <code>0</code> stops the wheel. Positive values rotate in one direction
113
+ (e.g., clockwise), negative values rotate in the opposite direction (e.g.,
114
+ counter-clockwise). The magnitude determines the speed (larger absolute value means
115
+ faster rotation).
116
+ </li>
117
+ <li>Control Address: <code>ADDR_SCS_GOAL_SPEED</code> (Register 46/47).</li>
118
+ </ul>
119
+ </li>
120
+ <li>
121
+ <strong>Acceleration:</strong> Controls how quickly the servo changes speed to reach its
122
+ target position (in Position Mode) or target speed (in Wheel Mode).
123
+ <ul>
124
+ <li>Range: Typically <code>0</code> to <code>254</code>.</li>
125
+ <li>
126
+ Meaning: Defines the rate of change of speed. The unit is 100 steps/s².
127
+ <code>0</code> usually means instantaneous acceleration (or minimal delay). Higher
128
+ values result in slower, smoother acceleration and deceleration. For example, a
129
+ value of <code>10</code> means the speed changes by 10 * 100 = 1000 steps per
130
+ second, per second. This helps reduce jerky movements and mechanical stress.
131
+ </li>
132
+ <li>Control Address: <code>ADDR_SCS_GOAL_ACC</code> (Register 41).</li>
133
+ </ul>
134
+ </li>
135
+ <li>
136
+ <strong>Baud Rate:</strong> The speed of communication between the controller and the
137
+ servo. It must match on both ends. Servos often support multiple baud rates, selectable
138
+ via an index:
139
+ <ul>
140
+ <li>Index 0: 1,000,000 bps</li>
141
+ <li>Index 1: 500,000 bps</li>
142
+ <li>Index 2: 250,000 bps</li>
143
+ <li>Index 3: 128,000 bps</li>
144
+ <li>Index 4: 115,200 bps</li>
145
+ <li>Index 5: 76,800 bps</li>
146
+ <li>Index 6: 57,600 bps</li>
147
+ <li>Index 7: 38,400 bps</li>
148
+ </ul>
149
+ </li>
150
+ </ul>
151
+ </details>
152
+
153
+ <div class="section">
154
+ <h2>Connection</h2>
155
+ <button id="connectBtn">Connect</button>
156
+ <button id="disconnectBtn">Disconnect</button>
157
+ <p>Status: <span id="connectionStatus" class="status error">Disconnected</span></p>
158
+ <label for="baudRate">Baud Rate:</label>
159
+ <input type="number" id="baudRate" value="1000000" />
160
+ <label for="protocolEnd">Protocol End (0=STS/SMS, 1=SCS):</label>
161
+ <input type="number" id="protocolEnd" value="0" min="0" max="1" />
162
+ </div>
163
+
164
+ <div class="section">
165
+ <h2>Scan Servos</h2>
166
+ <label for="scanStartId">Start ID:</label>
167
+ <input type="number" id="scanStartId" value="1" min="1" max="252" />
168
+ <label for="scanEndId">End ID:</label>
169
+ <input type="number" id="scanEndId" value="15" min="1" max="252" />
170
+ <button id="scanServosBtn">Scan</button>
171
+ <p>Scan Results:</p>
172
+ <pre id="scanResultsOutput" style="max-height: 200px; overflow-y: auto"></pre>
173
+ <!-- Added element for results -->
174
+ </div>
175
+
176
+ <div class="section">
177
+ <h2>Single Servo Control</h2>
178
+ <label for="servoId">Servo ID:</label>
179
+ <input type="number" id="servoId" value="1" min="1" max="252" /><br />
180
+
181
+ <label for="idWrite">Change servo ID:</label>
182
+ <input type="number" id="idWrite" value="1" min="1" max="252" />
183
+ <button id="writeIdBtn">Write</button><br />
184
+
185
+ <label for="baudRead">Read Baud Rate:</label>
186
+ <button id="readBaudBtn">Read</button>
187
+ <span id="readBaudResult"></span><br />
188
+
189
+ <label for="baudWrite">Write Baud Rate Index:</label>
190
+ <input type="number" id="baudWrite" value="6" min="0" max="7" />
191
+ <!-- Assuming index 0-7 -->
192
+ <button id="writeBaudBtn">Write</button><br />
193
+
194
+ <label for="positionRead">Read Position:</label>
195
+ <button id="readPosBtn">Read</button>
196
+ <span id="readPosResult"></span><br />
197
+
198
+ <label for="positionWrite">Write Position:</label>
199
+ <input type="number" id="positionWrite" value="1000" min="0" max="4095" />
200
+ <button id="writePosBtn">Write</button><br />
201
+
202
+ <label for="torqueEnable">Torque:</label>
203
+ <button id="torqueEnableBtn">Enable</button>
204
+ <button id="torqueDisableBtn">Disable</button><br />
205
+
206
+ <label for="accelerationWrite">Write Acceleration:</label>
207
+ <input type="number" id="accelerationWrite" value="50" min="0" max="254" />
208
+ <button id="writeAccBtn">Write</button><br />
209
+
210
+ <label for="wheelMode">Wheel Mode:</label>
211
+ <button id="setWheelModeBtn">Set Wheel Mode</button>
212
+ <button id="removeWheelModeBtn">Set Position Mode</button><br />
213
+
214
+ <label for="wheelSpeedWrite">Write Wheel Speed:</label>
215
+ <input type="number" id="wheelSpeedWrite" value="0" min="-2500" max="2500" />
216
+ <button id="writeWheelSpeedBtn">Write Speed</button>
217
+ </div>
218
+
219
+ <div class="section">
220
+ <h2>Sync Operations</h2>
221
+ <label for="syncReadIds">Sync Read IDs (csv):</label>
222
+ <input type="text" id="syncReadIds" value="1,2,3" style="width: 150px" />
223
+ <button id="syncReadBtn">Sync Read Positions</button><br />
224
+
225
+ <label for="syncWriteData">Sync Write (id:pos,...):</label>
226
+ <input type="text" id="syncWriteData" value="1:1500,2:2500" style="width: 200px" />
227
+ <button id="syncWriteBtn">Sync Write Positions</button><br />
228
+
229
+ <label for="syncWriteSpeedData">Sync Write Speed (id:speed,...):</label>
230
+ <input type="text" id="syncWriteSpeedData" value="1:500,2:-1000" style="width: 200px" />
231
+ <button id="syncWriteSpeedBtn">Sync Write Speeds</button>
232
+ <!-- New Button -->
233
+ </div>
234
+
235
+ <div class="section">
236
+ <h2>Log Output</h2>
237
+ <pre id="logOutput"></pre>
238
+ </div>
239
+ </div>
240
+
241
+ <script type="module">
242
+ // Import the scsServoSDK object from index.mjs
243
+ import { scsServoSDK } from "./index.mjs";
244
+ // No longer need COMM_SUCCESS etc. here as errors are thrown
245
+
246
+ const connectBtn = document.getElementById("connectBtn");
247
+ const disconnectBtn = document.getElementById("disconnectBtn");
248
+ const connectionStatus = document.getElementById("connectionStatus");
249
+ const baudRateInput = document.getElementById("baudRate");
250
+ const protocolEndInput = document.getElementById("protocolEnd");
251
+
252
+ const servoIdInput = document.getElementById("servoId");
253
+ const readIdBtn = document.getElementById("readIdBtn"); // New
254
+ const readIdResult = document.getElementById("readIdResult"); // New
255
+ const idWriteInput = document.getElementById("idWrite"); // New
256
+ const writeIdBtn = document.getElementById("writeIdBtn"); // New
257
+ const readBaudBtn = document.getElementById("readBaudBtn"); // New
258
+ const readBaudResult = document.getElementById("readBaudResult"); // New
259
+ const baudWriteInput = document.getElementById("baudWrite"); // New
260
+ const writeBaudBtn = document.getElementById("writeBaudBtn"); // New
261
+ const readPosBtn = document.getElementById("readPosBtn");
262
+ const readPosResult = document.getElementById("readPosResult");
263
+ const positionWriteInput = document.getElementById("positionWrite");
264
+ const writePosBtn = document.getElementById("writePosBtn");
265
+ const torqueEnableBtn = document.getElementById("torqueEnableBtn");
266
+ const torqueDisableBtn = document.getElementById("torqueDisableBtn");
267
+ const accelerationWriteInput = document.getElementById("accelerationWrite");
268
+ const writeAccBtn = document.getElementById("writeAccBtn");
269
+ const setWheelModeBtn = document.getElementById("setWheelModeBtn");
270
+ const removeWheelModeBtn = document.getElementById("removeWheelModeBtn"); // Get reference to the new button
271
+ const wheelSpeedWriteInput = document.getElementById("wheelSpeedWrite");
272
+ const writeWheelSpeedBtn = document.getElementById("writeWheelSpeedBtn");
273
+
274
+ const syncReadIdsInput = document.getElementById("syncReadIds");
275
+ const syncReadBtn = document.getElementById("syncReadBtn");
276
+ const syncWriteDataInput = document.getElementById("syncWriteData");
277
+ const syncWriteBtn = document.getElementById("syncWriteBtn");
278
+ const syncWriteSpeedDataInput = document.getElementById("syncWriteSpeedData"); // New Input
279
+ const syncWriteSpeedBtn = document.getElementById("syncWriteSpeedBtn"); // New Button
280
+ const scanServosBtn = document.getElementById("scanServosBtn"); // Get reference to the scan button
281
+ const scanStartIdInput = document.getElementById("scanStartId"); // Get reference to start ID input
282
+ const scanEndIdInput = document.getElementById("scanEndId"); // Get reference to end ID input
283
+ const scanResultsOutput = document.getElementById("scanResultsOutput"); // Get reference to the new results area
284
+
285
+ const logOutput = document.getElementById("logOutput");
286
+
287
+ let isConnected = false;
288
+
289
+ function log(message) {
290
+ console.log(message);
291
+ const timestamp = new Date().toLocaleTimeString();
292
+ logOutput.textContent = `[${timestamp}] ${message}\n` + logOutput.textContent;
293
+ // Limit log size
294
+ const lines = logOutput.textContent.split("\n"); // Use '\n' instead of literal newline
295
+ if (lines.length > 50) {
296
+ logOutput.textContent = lines.slice(0, 50).join("\n"); // Use '\n' instead of literal newline
297
+ }
298
+ }
299
+
300
+ function updateConnectionStatus(connected, message) {
301
+ isConnected = connected;
302
+ connectionStatus.textContent = message || (connected ? "Connected" : "Disconnected");
303
+ connectionStatus.className = `status ${connected ? "success" : "error"}`;
304
+ log(`Connection status: ${connectionStatus.textContent}`);
305
+ }
306
+
307
+ connectBtn.onclick = async () => {
308
+ log("Attempting to connect...");
309
+ try {
310
+ const baudRate = parseInt(baudRateInput.value, 10);
311
+ const protocolEnd = parseInt(protocolEndInput.value, 10);
312
+ // Use scsServoSDK - throws on error
313
+ await scsServoSDK.connect({ baudRate, protocolEnd });
314
+ updateConnectionStatus(true, "Connected");
315
+ } catch (err) {
316
+ updateConnectionStatus(false, `Connection error: ${err.message}`);
317
+ console.error(err);
318
+ }
319
+ };
320
+
321
+ disconnectBtn.onclick = async () => {
322
+ log("Attempting to disconnect...");
323
+ try {
324
+ // Use scsServoSDK - throws on error
325
+ await scsServoSDK.disconnect();
326
+ updateConnectionStatus(false, "Disconnected"); // Success means disconnected
327
+ } catch (err) {
328
+ // Assuming disconnect might fail if already disconnected or other issues
329
+ updateConnectionStatus(false, `Disconnection error: ${err.message}`);
330
+ console.error(err);
331
+ }
332
+ };
333
+
334
+ writeIdBtn.onclick = async () => {
335
+ // New handler
336
+ if (!isConnected) {
337
+ log("Error: Not connected");
338
+ return;
339
+ }
340
+ const currentId = parseInt(servoIdInput.value, 10);
341
+ const newId = parseInt(idWriteInput.value, 10);
342
+ if (isNaN(newId) || newId < 1 || newId > 252) {
343
+ log(`Error: Invalid new ID ${newId}. Must be between 1 and 252.`);
344
+ return;
345
+ }
346
+ log(`Writing new ID ${newId} to servo ${currentId}...`);
347
+ try {
348
+ // Use scsServoSDK - throws on error
349
+ await scsServoSDK.setServoId(currentId, newId);
350
+ log(`Successfully wrote new ID ${newId} to servo (was ${currentId}).`);
351
+ // IMPORTANT: Update the main ID input to reflect the change
352
+ servoIdInput.value = newId;
353
+ log(`Servo ID input field updated to ${newId}.`);
354
+ } catch (err) {
355
+ log(`Error writing ID for servo ${currentId}: ${err.message}`);
356
+ console.error(err);
357
+ }
358
+ };
359
+
360
+ readBaudBtn.onclick = async () => {
361
+ // New handler
362
+ if (!isConnected) {
363
+ log("Error: Not connected");
364
+ return;
365
+ }
366
+ const id = parseInt(servoIdInput.value, 10);
367
+ log(`Reading Baud Rate Index for servo ${id}...`);
368
+ readBaudResult.textContent = "Reading...";
369
+ try {
370
+ // Use scsServoSDK - returns value directly or throws
371
+ const baudRateIndex = await scsServoSDK.readBaudRate(id);
372
+ readBaudResult.textContent = `Baud Index: ${baudRateIndex}`;
373
+ log(`Servo ${id} Baud Rate Index: ${baudRateIndex}`);
374
+ } catch (err) {
375
+ readBaudResult.textContent = `Error: ${err.message}`;
376
+ log(`Error reading Baud Rate Index for servo ${id}: ${err.message}`);
377
+ console.error(err);
378
+ }
379
+ };
380
+
381
+ writeBaudBtn.onclick = async () => {
382
+ // New handler
383
+ if (!isConnected) {
384
+ log("Error: Not connected");
385
+ return;
386
+ }
387
+ const id = parseInt(servoIdInput.value, 10);
388
+ const newBaudIndex = parseInt(baudWriteInput.value, 10);
389
+ if (isNaN(newBaudIndex) || newBaudIndex < 0 || newBaudIndex > 7) {
390
+ // Adjust max index if needed
391
+ log(`Error: Invalid new Baud Rate Index ${newBaudIndex}. Check valid range.`);
392
+ return;
393
+ }
394
+ log(`Writing new Baud Rate Index ${newBaudIndex} to servo ${id}...`);
395
+ try {
396
+ // Use scsServoSDK - throws on error
397
+ await scsServoSDK.setBaudRate(id, newBaudIndex);
398
+ log(`Successfully wrote new Baud Rate Index ${newBaudIndex} to servo ${id}.`);
399
+ log(
400
+ `IMPORTANT: You may need to disconnect and reconnect with the new baud rate if it differs from the current connection baud rate.`
401
+ );
402
+ } catch (err) {
403
+ log(`Error writing Baud Rate Index for servo ${id}: ${err.message}`);
404
+ console.error(err);
405
+ }
406
+ };
407
+
408
+ readPosBtn.onclick = async () => {
409
+ if (!isConnected) {
410
+ log("Error: Not connected");
411
+ return;
412
+ }
413
+ const id = parseInt(servoIdInput.value, 10);
414
+ log(`Reading position for servo ${id}...`);
415
+ readPosResult.textContent = "Reading...";
416
+ try {
417
+ // Use scsServoSDK - returns value directly or throws
418
+ const position = await scsServoSDK.readPosition(id);
419
+ readPosResult.textContent = `Position: ${position}`;
420
+ log(`Servo ${id} position: ${position}`);
421
+ } catch (err) {
422
+ readPosResult.textContent = `Error: ${err.message}`;
423
+ log(`Error reading position for servo ${id}: ${err.message}`);
424
+ console.error(err);
425
+ }
426
+ };
427
+
428
+ writePosBtn.onclick = async () => {
429
+ if (!isConnected) {
430
+ log("Error: Not connected");
431
+ return;
432
+ }
433
+ const id = parseInt(servoIdInput.value, 10);
434
+ const pos = parseInt(positionWriteInput.value, 10);
435
+ log(`Writing position ${pos} to servo ${id}...`);
436
+ try {
437
+ // Use scsServoSDK - throws on error
438
+ await scsServoSDK.writePosition(id, pos);
439
+ log(`Successfully wrote position ${pos} to servo ${id}.`);
440
+ } catch (err) {
441
+ log(`Error writing position for servo ${id}: ${err.message}`);
442
+ console.error(err);
443
+ }
444
+ };
445
+
446
+ torqueEnableBtn.onclick = async () => {
447
+ if (!isConnected) {
448
+ log("Error: Not connected");
449
+ return;
450
+ }
451
+ const id = parseInt(servoIdInput.value, 10);
452
+ log(`Enabling torque for servo ${id}...`);
453
+ try {
454
+ // Use scsServoSDK - throws on error
455
+ await scsServoSDK.writeTorqueEnable(id, true);
456
+ log(`Successfully enabled torque for servo ${id}.`);
457
+ } catch (err) {
458
+ log(`Error enabling torque for servo ${id}: ${err.message}`);
459
+ console.error(err);
460
+ }
461
+ };
462
+
463
+ torqueDisableBtn.onclick = async () => {
464
+ if (!isConnected) {
465
+ log("Error: Not connected");
466
+ return;
467
+ }
468
+ const id = parseInt(servoIdInput.value, 10);
469
+ log(`Disabling torque for servo ${id}...`);
470
+ try {
471
+ // Use scsServoSDK - throws on error
472
+ await scsServoSDK.writeTorqueEnable(id, false);
473
+ log(`Successfully disabled torque for servo ${id}.`);
474
+ } catch (err) {
475
+ log(`Error disabling torque for servo ${id}: ${err.message}`);
476
+ console.error(err);
477
+ }
478
+ };
479
+
480
+ writeAccBtn.onclick = async () => {
481
+ if (!isConnected) {
482
+ log("Error: Not connected");
483
+ return;
484
+ }
485
+ const id = parseInt(servoIdInput.value, 10);
486
+ const acc = parseInt(accelerationWriteInput.value, 10);
487
+ log(`Writing acceleration ${acc} to servo ${id}...`);
488
+ try {
489
+ // Use scsServoSDK - throws on error
490
+ await scsServoSDK.writeAcceleration(id, acc);
491
+ log(`Successfully wrote acceleration ${acc} to servo ${id}.`);
492
+ } catch (err) {
493
+ log(`Error writing acceleration for servo ${id}: ${err.message}`);
494
+ console.error(err);
495
+ }
496
+ };
497
+
498
+ setWheelModeBtn.onclick = async () => {
499
+ if (!isConnected) {
500
+ log("Error: Not connected");
501
+ return;
502
+ }
503
+ const id = parseInt(servoIdInput.value, 10);
504
+ log(`Setting servo ${id} to wheel mode...`);
505
+ try {
506
+ // Use scsServoSDK - throws on error
507
+ await scsServoSDK.setWheelMode(id);
508
+ log(`Successfully set servo ${id} to wheel mode.`);
509
+ } catch (err) {
510
+ log(`Error setting wheel mode for servo ${id}: ${err.message}`);
511
+ console.error(err);
512
+ }
513
+ };
514
+
515
+ // Add event listener for the new button
516
+ removeWheelModeBtn.onclick = async () => {
517
+ if (!isConnected) {
518
+ log("Error: Not connected");
519
+ return;
520
+ }
521
+ const id = parseInt(servoIdInput.value, 10);
522
+ log(`Setting servo ${id} back to position mode...`);
523
+ try {
524
+ // Use scsServoSDK - throws on error
525
+ await scsServoSDK.setPositionMode(id);
526
+ log(`Successfully set servo ${id} back to position mode.`);
527
+ } catch (err) {
528
+ log(`Error setting position mode for servo ${id}: ${err.message}`);
529
+ console.error(err);
530
+ }
531
+ };
532
+
533
+ writeWheelSpeedBtn.onclick = async () => {
534
+ if (!isConnected) {
535
+ log("Error: Not connected");
536
+ return;
537
+ }
538
+ const id = parseInt(servoIdInput.value, 10);
539
+ const speed = parseInt(wheelSpeedWriteInput.value, 10);
540
+ log(`Writing wheel speed ${speed} to servo ${id}...`);
541
+ try {
542
+ // Use scsServoSDK - throws on error
543
+ await scsServoSDK.writeWheelSpeed(id, speed);
544
+ log(`Successfully wrote wheel speed ${speed} to servo ${id}.`);
545
+ } catch (err) {
546
+ log(`Error writing wheel speed for servo ${id}: ${err.message}`);
547
+ console.error(err);
548
+ }
549
+ };
550
+
551
+ syncReadBtn.onclick = async () => {
552
+ if (!isConnected) {
553
+ log("Error: Not connected");
554
+ return;
555
+ }
556
+ const idsString = syncReadIdsInput.value;
557
+ const ids = idsString
558
+ .split(",")
559
+ .map((s) => parseInt(s.trim(), 10))
560
+ .filter((id) => !isNaN(id) && id > 0 && id < 253);
561
+ if (ids.length === 0) {
562
+ log("Sync Read: No valid servo IDs provided.");
563
+ return;
564
+ }
565
+ log(`Sync reading positions for servos: ${ids.join(", ")}...`);
566
+ try {
567
+ // Use scsServoSDK - returns Map or throws
568
+ const positions = await scsServoSDK.syncReadPositions(ids);
569
+ let logMsg = "Sync Read Successful:\n";
570
+ positions.forEach((pos, id) => {
571
+ logMsg += ` Servo ${id}: Position=${pos}\n`;
572
+ });
573
+ log(logMsg.trim());
574
+ } catch (err) {
575
+ log(`Sync Read Failed: ${err.message}`);
576
+ console.error(err);
577
+ }
578
+ };
579
+
580
+ syncWriteBtn.onclick = async () => {
581
+ if (!isConnected) {
582
+ log("Error: Not connected");
583
+ return;
584
+ }
585
+ const dataString = syncWriteDataInput.value;
586
+ const positionMap = new Map();
587
+ const pairs = dataString.split(",");
588
+ let validData = false;
589
+
590
+ pairs.forEach((pair) => {
591
+ const parts = pair.split(":");
592
+ if (parts.length === 2) {
593
+ const id = parseInt(parts[0].trim(), 10);
594
+ const pos = parseInt(parts[1].trim(), 10);
595
+ // Position validation (0-4095)
596
+ if (!isNaN(id) && id > 0 && id < 253 && !isNaN(pos) && pos >= 0 && pos <= 4095) {
597
+ positionMap.set(id, pos);
598
+ validData = true;
599
+ } else {
600
+ log(
601
+ `Sync Write Position: Invalid data pair "${pair}". ID (1-252), Pos (0-4095). Skipping.`
602
+ );
603
+ }
604
+ } else {
605
+ log(`Sync Write Position: Invalid format "${pair}". Skipping.`);
606
+ }
607
+ });
608
+
609
+ if (!validData) {
610
+ log("Sync Write Position: No valid servo position data provided.");
611
+ return;
612
+ }
613
+
614
+ log(
615
+ `Sync writing positions: ${Array.from(positionMap.entries())
616
+ .map(([id, pos]) => `${id}:${pos}`)
617
+ .join(", ")}...`
618
+ );
619
+ try {
620
+ // Use scsServoSDK - throws on error
621
+ await scsServoSDK.syncWritePositions(positionMap);
622
+ log(`Sync write position command sent successfully.`);
623
+ } catch (err) {
624
+ log(`Sync Write Position Failed: ${err.message}`);
625
+ console.error(err);
626
+ }
627
+ };
628
+
629
+ // New handler for Sync Write Speed
630
+ syncWriteSpeedBtn.onclick = async () => {
631
+ if (!isConnected) {
632
+ log("Error: Not connected");
633
+ return;
634
+ }
635
+ const dataString = syncWriteSpeedDataInput.value;
636
+ const speedMap = new Map();
637
+ const pairs = dataString.split(",");
638
+ let validData = false;
639
+
640
+ pairs.forEach((pair) => {
641
+ const parts = pair.split(":");
642
+ if (parts.length === 2) {
643
+ const id = parseInt(parts[0].trim(), 10);
644
+ const speed = parseInt(parts[1].trim(), 10);
645
+ // Speed validation (-10000 to 10000)
646
+ if (
647
+ !isNaN(id) &&
648
+ id > 0 &&
649
+ id < 253 &&
650
+ !isNaN(speed) &&
651
+ speed >= -10000 &&
652
+ speed <= 10000
653
+ ) {
654
+ speedMap.set(id, speed);
655
+ validData = true;
656
+ } else {
657
+ log(
658
+ `Sync Write Speed: Invalid data pair "${pair}". ID (1-252), Speed (-10000 to 10000). Skipping.`
659
+ );
660
+ }
661
+ } else {
662
+ log(`Sync Write Speed: Invalid format "${pair}". Skipping.`);
663
+ }
664
+ });
665
+
666
+ if (!validData) {
667
+ log("Sync Write Speed: No valid servo speed data provided.");
668
+ return;
669
+ }
670
+
671
+ log(
672
+ `Sync writing speeds: ${Array.from(speedMap.entries())
673
+ .map(([id, speed]) => `${id}:${speed}`)
674
+ .join(", ")}...`
675
+ );
676
+ try {
677
+ // Use scsServoSDK - throws on error
678
+ await scsServoSDK.syncWriteWheelSpeed(speedMap);
679
+ log(`Sync write speed command sent successfully.`);
680
+ } catch (err) {
681
+ log(`Sync Write Speed Failed: ${err.message}`);
682
+ console.error(err);
683
+ }
684
+ };
685
+
686
+ scanServosBtn.onclick = async () => {
687
+ if (!isConnected) {
688
+ log("Error: Not connected");
689
+ return;
690
+ }
691
+
692
+ const startId = parseInt(scanStartIdInput.value, 10);
693
+ const endId = parseInt(scanEndIdInput.value, 10);
694
+
695
+ if (isNaN(startId) || isNaN(endId) || startId < 1 || endId > 252 || startId > endId) {
696
+ const errorMsg =
697
+ "Error: Invalid scan ID range. Please enter values between 1 and 252, with Start ID <= End ID.";
698
+ log(errorMsg);
699
+ scanResultsOutput.textContent = errorMsg; // Show error in results area too
700
+ return;
701
+ }
702
+
703
+ const startMsg = `Starting servo scan (IDs ${startId}-${endId})...`;
704
+ log(startMsg);
705
+ scanResultsOutput.textContent = startMsg + "\n"; // Clear and start results area
706
+ scanServosBtn.disabled = true; // Disable button during scan
707
+
708
+ let foundCount = 0;
709
+
710
+ for (let id = startId; id <= endId; id++) {
711
+ let resultMsg = `Scanning ID ${id}... `;
712
+ try {
713
+ // Attempt to read position. If it succeeds, the servo exists.
714
+ // If it throws, the servo likely doesn't exist or there's another issue.
715
+ const position = await scsServoSDK.readPosition(id);
716
+ foundCount++;
717
+
718
+ // Servo found, now try to read mode and baud rate
719
+ let mode = "ReadError";
720
+ let baudRateIndex = "ReadError";
721
+ try {
722
+ mode = await scsServoSDK.readMode(id);
723
+ } catch (modeErr) {
724
+ log(` Servo ${id}: Error reading mode: ${modeErr.message}`);
725
+ }
726
+ try {
727
+ baudRateIndex = await scsServoSDK.readBaudRate(id);
728
+ } catch (baudErr) {
729
+ log(` Servo ${id}: Error reading baud rate: ${baudErr.message}`);
730
+ }
731
+
732
+ resultMsg += `FOUND: Pos=${position}, Mode=${mode}, BaudIdx=${baudRateIndex}`;
733
+ log(
734
+ ` Servo ${id} FOUND: Position=${position}, Mode=${mode}, BaudIndex=${baudRateIndex}`
735
+ );
736
+ } catch (err) {
737
+ // Check if the error message indicates a timeout or non-response, which is expected for non-existent IDs
738
+ // This check might need refinement based on the exact error messages thrown by readPosition
739
+ if (
740
+ err.message.includes("timeout") ||
741
+ err.message.includes("No response") ||
742
+ err.message.includes("failed: RX")
743
+ ) {
744
+ resultMsg += `No response`;
745
+ // log(` Servo ${id}: No response`); // Optional: reduce log noise
746
+ } else {
747
+ // Log other unexpected errors
748
+ resultMsg += `Error: ${err.message}`;
749
+ log(` Servo ${id}: Error during scan: ${err.message}`);
750
+ console.error(`Error scanning servo ${id}:`, err);
751
+ }
752
+ }
753
+ scanResultsOutput.textContent += resultMsg + "\n"; // Append result to the results area
754
+ scanResultsOutput.scrollTop = scanResultsOutput.scrollHeight; // Auto-scroll
755
+ // Optional small delay between scans if needed
756
+ // await new Promise(resolve => setTimeout(resolve, 10));
757
+ }
758
+
759
+ const finishMsg = `Servo scan finished. Found ${foundCount} servo(s).`;
760
+ log(finishMsg);
761
+ scanResultsOutput.textContent += finishMsg + "\n"; // Add finish message to results area
762
+ scanResultsOutput.scrollTop = scanResultsOutput.scrollHeight; // Auto-scroll
763
+ scanServosBtn.disabled = false; // Re-enable button
764
+ };
765
+
766
+ // Initial log
767
+ log("Test page loaded. Please connect to a servo controller.");
768
+ </script>
769
+ </body>
770
+ </html>
src/app.css ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @import "tw-animate-css";
4
+ @plugin "@iconify/tailwind4";
5
+
6
+ @custom-variant dark (&:is(.dark *));
7
+
8
+ :root {
9
+ --radius: 0.625rem;
10
+ --background: oklch(1 0 0);
11
+ --foreground: oklch(0.147 0.004 49.25);
12
+ --card: oklch(1 0 0);
13
+ --card-foreground: oklch(0.147 0.004 49.25);
14
+ --popover: oklch(1 0 0);
15
+ --popover-foreground: oklch(0.147 0.004 49.25);
16
+ --primary: oklch(0.216 0.006 56.043);
17
+ --primary-foreground: oklch(0.985 0.001 106.423);
18
+ --secondary: oklch(0.97 0.001 106.424);
19
+ --secondary-foreground: oklch(0.216 0.006 56.043);
20
+ --muted: oklch(0.97 0.001 106.424);
21
+ --muted-foreground: oklch(0.553 0.013 58.071);
22
+ --accent: oklch(0.97 0.001 106.424);
23
+ --accent-foreground: oklch(0.216 0.006 56.043);
24
+ --destructive: oklch(0.577 0.245 27.325);
25
+ --border: oklch(0.923 0.003 48.717);
26
+ --input: oklch(0.923 0.003 48.717);
27
+ --ring: oklch(0.709 0.01 56.259);
28
+ --chart-1: oklch(0.646 0.222 41.116);
29
+ --chart-2: oklch(0.6 0.118 184.704);
30
+ --chart-3: oklch(0.398 0.07 227.392);
31
+ --chart-4: oklch(0.828 0.189 84.429);
32
+ --chart-5: oklch(0.769 0.188 70.08);
33
+ --sidebar: oklch(0.985 0.001 106.423);
34
+ --sidebar-foreground: oklch(0.147 0.004 49.25);
35
+ --sidebar-primary: oklch(0.216 0.006 56.043);
36
+ --sidebar-primary-foreground: oklch(0.985 0.001 106.423);
37
+ --sidebar-accent: oklch(0.97 0.001 106.424);
38
+ --sidebar-accent-foreground: oklch(0.216 0.006 56.043);
39
+ --sidebar-border: oklch(0.923 0.003 48.717);
40
+ --sidebar-ring: oklch(0.709 0.01 56.259);
41
+ }
42
+
43
+ .dark {
44
+ --background: oklch(0.147 0.004 49.25);
45
+ --foreground: oklch(0.985 0.001 106.423);
46
+ --card: oklch(0.216 0.006 56.043);
47
+ --card-foreground: oklch(0.985 0.001 106.423);
48
+ --popover: oklch(0.216 0.006 56.043);
49
+ --popover-foreground: oklch(0.985 0.001 106.423);
50
+ --primary: oklch(0.923 0.003 48.717);
51
+ --primary-foreground: oklch(0.216 0.006 56.043);
52
+ --secondary: oklch(0.268 0.007 34.298);
53
+ --secondary-foreground: oklch(0.985 0.001 106.423);
54
+ --muted: oklch(0.268 0.007 34.298);
55
+ --muted-foreground: oklch(0.709 0.01 56.259);
56
+ --accent: oklch(0.268 0.007 34.298);
57
+ --accent-foreground: oklch(0.985 0.001 106.423);
58
+ --destructive: oklch(0.704 0.191 22.216);
59
+ --border: oklch(1 0 0 / 10%);
60
+ --input: oklch(1 0 0 / 15%);
61
+ --ring: oklch(0.553 0.013 58.071);
62
+ --chart-1: oklch(0.488 0.243 264.376);
63
+ --chart-2: oklch(0.696 0.17 162.48);
64
+ --chart-3: oklch(0.769 0.188 70.08);
65
+ --chart-4: oklch(0.627 0.265 303.9);
66
+ --chart-5: oklch(0.645 0.246 16.439);
67
+ --sidebar: oklch(0.216 0.006 56.043);
68
+ --sidebar-foreground: oklch(0.985 0.001 106.423);
69
+ --sidebar-primary: oklch(0.488 0.243 264.376);
70
+ --sidebar-primary-foreground: oklch(0.985 0.001 106.423);
71
+ --sidebar-accent: oklch(0.268 0.007 34.298);
72
+ --sidebar-accent-foreground: oklch(0.985 0.001 106.423);
73
+ --sidebar-border: oklch(1 0 0 / 10%);
74
+ --sidebar-ring: oklch(0.553 0.013 58.071);
75
+ }
76
+
77
+ @theme inline {
78
+ --radius-sm: calc(var(--radius) - 4px);
79
+ --radius-md: calc(var(--radius) - 2px);
80
+ --radius-lg: var(--radius);
81
+ --radius-xl: calc(var(--radius) + 4px);
82
+ --color-background: var(--background);
83
+ --color-foreground: var(--foreground);
84
+ --color-card: var(--card);
85
+ --color-card-foreground: var(--card-foreground);
86
+ --color-popover: var(--popover);
87
+ --color-popover-foreground: var(--popover-foreground);
88
+ --color-primary: var(--primary);
89
+ --color-primary-foreground: var(--primary-foreground);
90
+ --color-secondary: var(--secondary);
91
+ --color-secondary-foreground: var(--secondary-foreground);
92
+ --color-muted: var(--muted);
93
+ --color-muted-foreground: var(--muted-foreground);
94
+ --color-accent: var(--accent);
95
+ --color-accent-foreground: var(--accent-foreground);
96
+ --color-destructive: var(--destructive);
97
+ --color-border: var(--border);
98
+ --color-input: var(--input);
99
+ --color-ring: var(--ring);
100
+ --color-chart-1: var(--chart-1);
101
+ --color-chart-2: var(--chart-2);
102
+ --color-chart-3: var(--chart-3);
103
+ --color-chart-4: var(--chart-4);
104
+ --color-chart-5: var(--chart-5);
105
+ --color-sidebar: var(--sidebar);
106
+ --color-sidebar-foreground: var(--sidebar-foreground);
107
+ --color-sidebar-primary: var(--sidebar-primary);
108
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
109
+ --color-sidebar-accent: var(--sidebar-accent);
110
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
111
+ --color-sidebar-border: var(--sidebar-border);
112
+ --color-sidebar-ring: var(--sidebar-ring);
113
+ }
114
+
115
+ @layer base {
116
+ * {
117
+ @apply border-border outline-ring/50;
118
+ }
119
+ body {
120
+ @apply bg-background text-foreground;
121
+ }
122
+ }
src/app.d.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { InteractivityProps } from '@threlte/extras'
2
+
3
+ // See https://svelte.dev/docs/kit/types#app.d.ts
4
+ // for information about these interfaces
5
+ declare global {
6
+ namespace App {
7
+ // interface Error {}
8
+ // interface Locals {}
9
+ // interface PageData {}
10
+ // interface PageState {}
11
+ // interface Platform {}
12
+ }
13
+ namespace Threlte {
14
+ interface UserProps extends InteractivityProps {
15
+ interactivity?: boolean;
16
+ }
17
+ }
18
+ }
19
+
20
+ export {};
src/app.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ %sveltekit.head%
8
+ </head>
9
+ <body data-sveltekit-preload-data="hover">
10
+ <div style="display: contents">%sveltekit.body%</div>
11
+ </body>
12
+ </html>
src/lib/components/3d/Floor.svelte ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { T } from "@threlte/core";
3
+ import { PlaneGeometry } from 'three';
4
+ import { Grid } from '@threlte/extras'
5
+ const floorGeometry = new PlaneGeometry(20, 20);
6
+ </script>
7
+
8
+ <T.Mesh
9
+ receiveShadow
10
+ position.y={0}
11
+ rotation.x={-Math.PI / 2}
12
+ frustumCulled={false}
13
+ >
14
+ <T is={floorGeometry} />
15
+ <T.ShadowMaterial
16
+ opacity={0.3}
17
+ transparent={true}
18
+ polygonOffset={true}
19
+ polygonOffsetFactor={1}
20
+ polygonOffsetUnits={1}
21
+ />
22
+ </T.Mesh>
23
+ <Grid/>
24
+
src/lib/components/3d/elements/compute/ComputeGridItem.svelte ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { T } from "@threlte/core";
3
+ import GPU from "./GPU.svelte";
4
+ import ComputeStatusBillboard from "./status/ComputeStatusBillboard.svelte";
5
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
6
+ import { interactivity, type IntersectionEvent, useCursor } from "@threlte/extras";
7
+
8
+ interface Props {
9
+ compute: RemoteCompute;
10
+ onVideoInputBoxClick: (compute: RemoteCompute) => void;
11
+ onRobotInputBoxClick: (compute: RemoteCompute) => void;
12
+ onRobotOutputBoxClick: (compute: RemoteCompute) => void;
13
+ }
14
+
15
+ let { compute, onVideoInputBoxClick, onRobotInputBoxClick, onRobotOutputBoxClick }: Props = $props();
16
+
17
+ const { onPointerEnter, onPointerLeave, hovering } = useCursor();
18
+ interactivity();
19
+
20
+ let isToggled = $state(false);
21
+
22
+ function handleClick(event: IntersectionEvent<MouseEvent>) {
23
+ event.stopPropagation();
24
+ isToggled = !isToggled;
25
+ }
26
+ </script>
27
+
28
+ <T.Group
29
+ position.x={compute.position.x}
30
+ position.y={compute.position.y}
31
+ position.z={compute.position.z}
32
+ scale={[1, 1, 1]}
33
+ >
34
+ <T.Group
35
+ onpointerenter={onPointerEnter}
36
+ onpointerleave={onPointerLeave}
37
+ onclick={handleClick}
38
+ >
39
+ <GPU rotating={$hovering} />
40
+ </T.Group>
41
+ <T.Group scale={[8, 8, 8]} rotation={[-Math.PI / 2, 0, 0]}>
42
+ <ComputeStatusBillboard
43
+ {compute}
44
+ offset={0.8}
45
+ {onVideoInputBoxClick}
46
+ {onRobotInputBoxClick}
47
+ {onRobotOutputBoxClick}
48
+ visible={isToggled}
49
+ />
50
+ </T.Group>
51
+ </T.Group>
src/lib/components/3d/elements/compute/Computes.svelte ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ import { remoteComputeManager } from "$lib/elements/compute/RemoteComputeManager.svelte";
4
+ import AISessionConnectionModal from "@/components/3d/elements/compute/modal/AISessionConnectionModal.svelte";
5
+ import VideoInputConnectionModal from "@/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte";
6
+ import RobotInputConnectionModal from "@/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte";
7
+ import RobotOutputConnectionModal from "@/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte";
8
+ import ComputeGridItem from "@/components/3d/elements/compute/ComputeGridItem.svelte";
9
+ import type { RemoteCompute } from "$lib/elements/compute/RemoteCompute.svelte";
10
+
11
+ interface Props {
12
+ workspaceId: string;
13
+ }
14
+ let { workspaceId }: Props = $props();
15
+
16
+ let isAISessionModalOpen = $state(false);
17
+ let isVideoInputModalOpen = $state(false);
18
+ let isRobotInputModalOpen = $state(false);
19
+ let isRobotOutputModalOpen = $state(false);
20
+ let selectedCompute = $state<RemoteCompute | null>(null);
21
+
22
+ function handleVideoInputBoxClick(compute: RemoteCompute) {
23
+ selectedCompute = compute;
24
+ if (!compute.hasSession) {
25
+ // If no session exists, open the session creation modal
26
+ isAISessionModalOpen = true;
27
+ } else {
28
+ // If session exists, open video connection modal
29
+ isVideoInputModalOpen = true;
30
+ }
31
+ }
32
+
33
+ function handleRobotInputBoxClick(compute: RemoteCompute) {
34
+ selectedCompute = compute;
35
+ if (!compute.hasSession) {
36
+ // If no session exists, open the session creation modal
37
+ isAISessionModalOpen = true;
38
+ } else {
39
+ // If session exists, open robot input connection modal
40
+ isRobotInputModalOpen = true;
41
+ }
42
+ }
43
+
44
+ function handleRobotOutputBoxClick(compute: RemoteCompute) {
45
+ selectedCompute = compute;
46
+ if (!compute.hasSession) {
47
+ // If no session exists, open the session creation modal
48
+ isAISessionModalOpen = true;
49
+ } else {
50
+ // If session exists, open robot output connection modal
51
+ isRobotOutputModalOpen = true;
52
+ }
53
+ }
54
+
55
+ // Auto-refresh compute statuses periodically
56
+ onMount(() => {
57
+ const interval = setInterval(async () => {
58
+ for (const compute of remoteComputeManager.computes) {
59
+ if (compute.hasSession) {
60
+ await remoteComputeManager.getSessionStatus(compute.id);
61
+ }
62
+ }
63
+ }, 5000); // Refresh every 5 seconds
64
+
65
+ return () => clearInterval(interval);
66
+ });
67
+ </script>
68
+
69
+ {#each remoteComputeManager.computes as compute (compute.id)}
70
+ <ComputeGridItem
71
+ {compute}
72
+ onVideoInputBoxClick={handleVideoInputBoxClick}
73
+ onRobotInputBoxClick={handleRobotInputBoxClick}
74
+ onRobotOutputBoxClick={handleRobotOutputBoxClick}
75
+ />
76
+ {/each}
77
+
78
+ {#if selectedCompute}
79
+ <!-- AI Session Creation Modal -->
80
+ <AISessionConnectionModal bind:open={isAISessionModalOpen} compute={selectedCompute} {workspaceId} />
81
+ <!-- Video Input Connection Modal -->
82
+ <VideoInputConnectionModal bind:open={isVideoInputModalOpen} compute={selectedCompute} {workspaceId} />
83
+ <!-- Robot Input Connection Modal -->
84
+ <RobotInputConnectionModal bind:open={isRobotInputModalOpen} compute={selectedCompute} {workspaceId} />
85
+ <!-- Robot Output Connection Modal -->
86
+ <RobotOutputConnectionModal bind:open={isRobotOutputModalOpen} compute={selectedCompute} {workspaceId} />
87
+ {/if}
src/lib/components/3d/elements/compute/GPU.svelte ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { useCursor } from '@threlte/extras'
3
+ import { T } from "@threlte/core";
4
+ import { HTML, type IntersectionEvent } from "@threlte/extras";
5
+ import { GLTF, useGltf } from "@threlte/extras";
6
+ import Model from "./GPUModel.svelte";
7
+ import { Shape, Path, ExtrudeGeometry, BoxGeometry } from "three";
8
+ import { onMount } from "svelte";
9
+ import type { VideoInstance } from "$lib/elements/video//VideoManager.svelte";
10
+ import { videoManager } from "$lib/elements/video//VideoManager.svelte";
11
+
12
+ // Props interface
13
+ interface Props {
14
+ // Transform props
15
+ position?: [number, number, number];
16
+ rotation?: [number, number, number];
17
+ scale?: [number, number, number];
18
+ rotating?: boolean;
19
+ }
20
+
21
+ // Props with defaults
22
+ let { position = [0, 0, 0], rotation = [0, 0, 0], scale = [1, 1, 1], rotating = false }: Props = $props();
23
+
24
+ // Create the TV frame geometry (outer rounded rectangle)
25
+ function createTVFrame(
26
+ tvWidth: number,
27
+ tvHeight: number,
28
+ tvDepth: number,
29
+ tvFrameThickness: number,
30
+ tvCornerRadius: number
31
+ ) {
32
+ const shape = new Shape();
33
+ const x = -tvWidth / 2;
34
+ const y = -tvHeight / 2;
35
+ const w = tvWidth;
36
+ const h = tvHeight;
37
+ const radius = tvCornerRadius;
38
+
39
+ shape.moveTo(x, y + radius);
40
+ shape.lineTo(x, y + h - radius);
41
+ shape.quadraticCurveTo(x, y + h, x + radius, y + h);
42
+ shape.lineTo(x + w - radius, y + h);
43
+ shape.quadraticCurveTo(x + w, y + h, x + w, y + h - radius);
44
+ shape.lineTo(x + w, y + radius);
45
+ shape.quadraticCurveTo(x + w, y, x + w - radius, y);
46
+ shape.lineTo(x + radius, y);
47
+ shape.quadraticCurveTo(x, y, x, y + radius);
48
+
49
+ // Create hole for screen (inner rectangle)
50
+ const hole = new Path();
51
+ const hx = x + tvFrameThickness;
52
+ const hy = y + tvFrameThickness;
53
+ const hwidth = w - tvFrameThickness * 2;
54
+ const hheight = h - tvFrameThickness * 2;
55
+ const hradius = tvCornerRadius * 0.5;
56
+
57
+ hole.moveTo(hx, hy + hradius);
58
+ hole.lineTo(hx, hy + hheight - hradius);
59
+ hole.quadraticCurveTo(hx, hy + hheight, hx + hradius, hy + hheight);
60
+ hole.lineTo(hx + hwidth - hradius, hy + hheight);
61
+ hole.quadraticCurveTo(hx + hwidth, hy + hheight, hx + hwidth, hy + hheight - hradius);
62
+ hole.lineTo(hx + hwidth, hy + hradius);
63
+ hole.quadraticCurveTo(hx + hwidth, hy, hx + hwidth - hradius, hy);
64
+ hole.lineTo(hx + hradius, hy);
65
+ hole.quadraticCurveTo(hx, hy, hx, hy + hradius);
66
+
67
+ shape.holes.push(hole);
68
+
69
+ return new ExtrudeGeometry(shape, {
70
+ depth: tvDepth,
71
+ bevelEnabled: true,
72
+ bevelThickness: 0.02,
73
+ bevelSize: 0.02,
74
+ bevelSegments: 8
75
+ });
76
+ }
77
+
78
+ // Create the screen (video display area)
79
+ function createScreen(tvWidth: number, tvHeight: number, tvFrameThickness: number) {
80
+ const w = tvWidth - tvFrameThickness * 2;
81
+ const h = tvHeight - tvFrameThickness * 2;
82
+
83
+ // Create a very thin box for the screen area (only visible from front)
84
+ return new BoxGeometry(w, h, 0.02);
85
+ }
86
+
87
+ const frameGeometry = createTVFrame(1, 1, 1, 0.2, 0.15);
88
+ const screenGeometry = createScreen(1, 1, 0.2);
89
+
90
+ const gltf = useGltf("/gpu/scene.gltf");
91
+
92
+ let fan_rotation = $state(0);
93
+ let rotationPerSeconds = $state(1); // 1 rotation per second by default
94
+
95
+ onMount(() => {
96
+ const interval = setInterval(() => {
97
+ // Calculate angle increment per frame for desired rotations per second
98
+ if (rotating) {
99
+ const angleIncrement = (Math.PI * 2 * rotationPerSeconds) / 60;
100
+ fan_rotation = fan_rotation + angleIncrement;
101
+ }
102
+ }, 1000/60); // Run at ~60fps
103
+
104
+ return () => {
105
+ clearInterval(interval);
106
+ };
107
+ });
108
+
109
+
110
+ </script>
111
+
112
+ <T.Group
113
+ {position}
114
+ {rotation}
115
+ {scale}
116
+ >
117
+ <!-- TV Frame -->
118
+ <!-- <T.Mesh geometry={frameGeometry}>
119
+ <T.MeshStandardMaterial
120
+ color={"#374151"}
121
+ metalness={0.05}
122
+ roughness={0.4}
123
+ envMapIntensity={0.3}
124
+ />
125
+ </T.Mesh> -->
126
+ <T.Group
127
+ scale={[1, 1, 1]}
128
+ >
129
+ <Model fan_rotation={fan_rotation} />
130
+ </T.Group>
131
+ <!-- <GLTF castShadow receiveShadow gltf={$gltf} position={{ y: 1 }} scale={3} /> -->
132
+
133
+ <!-- <T.Group scale={[1,1,1]}>
134
+ {#if $gltf}
135
+ <T is={$gltf.nodes['Sketchfab_model']} />
136
+ {/if}
137
+ </T.Group> -->
138
+ </T.Group>
src/lib/components/3d/elements/compute/GPUModel.svelte ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--
2
+ Auto-generated by: https://github.com/threlte/threlte/tree/main/packages/gltf
3
+ Command: npx @threlte/gltf@3.0.1 /Users/julienblanchon/Downloads/gpu/scene.gltf --output ./src/lib/components/3d/elements/gpu/ --types
4
+ Author: Cem Gürbüz (https://sketchfab.com/cemgurbuzz)
5
+ License: CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)
6
+ Source: https://sketchfab.com/3d-models/nvidia-geforce-rtx-3090-9b7cd73fefd5435f99f891567f5a9c2e
7
+ Title: Nvidia GeForce RTX 3090
8
+ -->
9
+
10
+ <script lang="ts">
11
+ import type * as THREE from 'three'
12
+
13
+ import type { Snippet } from 'svelte'
14
+ import { T, type Props } from '@threlte/core'
15
+ import { useGltf } from '@threlte/extras'
16
+
17
+ let {
18
+ fan_rotation = 0,
19
+ fallback,
20
+ error,
21
+ children,
22
+ ref = $bindable(),
23
+ ...props
24
+ }: Props<THREE.Group<THREE.Object3DEventMap>> & {
25
+ fan_rotation?: number
26
+ ref?: THREE.Group<THREE.Object3DEventMap> | undefined
27
+ children?: Snippet<[{ ref: THREE.Group<THREE.Object3DEventMap> | undefined }]>
28
+ fallback?: Snippet
29
+ error?: Snippet<[{ error: Error }]>
30
+ } = $props()
31
+
32
+ type GLTFResult = {
33
+ nodes: {
34
+ Metal_Frame_Metal_0: THREE.Mesh
35
+ Front_Cover_Black_0: THREE.Mesh
36
+ Fan_Circle_Black_Fan_0: THREE.Mesh
37
+ Fan_F_Black_Fan_0: THREE.Mesh
38
+ Fan_F_Slot1_0: THREE.Mesh
39
+ Front_Cover_U_Black_0: THREE.Mesh
40
+ Front_Cover_T_Black_0: THREE.Mesh
41
+ Fan_Circle_B_Black_Fan_0: THREE.Mesh
42
+ Grills_U_Metal_Black_0: THREE.Mesh
43
+ Grills_T_Metal_Black_0: THREE.Mesh
44
+ Plane010_Black001_0: THREE.Mesh
45
+ Socket_Slot_0: THREE.Mesh
46
+ Side_Metal_Part_Metal_S_0: THREE.Mesh
47
+ Grills_F003_Metal_Black_0: THREE.Mesh
48
+ Grills_F002_Metal_Black_0: THREE.Mesh
49
+ Fan_B_Black_Fan_0: THREE.Mesh
50
+ Fan_B_Slot1_0: THREE.Mesh
51
+ }
52
+ materials: {
53
+ Metal: THREE.MeshStandardMaterial
54
+ Black: THREE.MeshStandardMaterial
55
+ Black_Fan: THREE.MeshStandardMaterial
56
+ ['Slot.1']: THREE.MeshStandardMaterial
57
+ Metal_Black: THREE.MeshStandardMaterial
58
+ ['Black.001']: THREE.MeshStandardMaterial
59
+ Slot: THREE.MeshStandardMaterial
60
+ Metal_S: THREE.MeshStandardMaterial
61
+ }
62
+ }
63
+
64
+ const gltf = useGltf<GLTFResult>('/gpu/scene.gltf')
65
+ </script>
66
+
67
+ <T.Group
68
+ bind:ref
69
+ dispose={false}
70
+ {...props as any}
71
+ >
72
+ {#await gltf}
73
+ {@render fallback?.()}
74
+ {:then gltf}
75
+ <T.Group scale={0.01}>
76
+ <T.Group
77
+ position={[127.5, 88.51, 10.29]}
78
+ rotation={[Math.PI / 2, 0.05, 0]}
79
+ scale={0.3}
80
+ >
81
+ <T.Mesh
82
+ geometry={gltf.nodes.Fan_F_Black_Fan_0.geometry}
83
+ material={gltf.materials.Black_Fan}
84
+ rotation={[0, fan_rotation, 0]}
85
+ />
86
+ <T.Mesh
87
+ geometry={gltf.nodes.Fan_F_Slot1_0.geometry}
88
+ material={gltf.materials['Slot.1']}
89
+ />
90
+ </T.Group>
91
+ <T.Group
92
+ position={[-123.9, 88.51, -37.82]}
93
+ rotation={[Math.PI / 2, -0.05, Math.PI]}
94
+ scale={0.3}
95
+ >
96
+ <T.Mesh
97
+ geometry={gltf.nodes.Fan_B_Black_Fan_0.geometry}
98
+ material={gltf.materials.Black_Fan}
99
+ rotation={[0, fan_rotation, 0]}
100
+ />
101
+ <T.Mesh
102
+ geometry={gltf.nodes.Fan_B_Slot1_0.geometry}
103
+ material={gltf.materials['Slot.1']}
104
+
105
+ />
106
+ </T.Group>
107
+ <T.Mesh
108
+ geometry={gltf.nodes.Metal_Frame_Metal_0.geometry}
109
+ material={gltf.materials.Metal}
110
+ position={[0, 88.3, -8.47]}
111
+ rotation={[Math.PI / 2, 0, 0]}
112
+ />
113
+ <T.Mesh
114
+ geometry={gltf.nodes.Front_Cover_Black_0.geometry}
115
+ material={gltf.materials.Black}
116
+ position={[-122.3, 89.69, 12.11]}
117
+ rotation={[Math.PI / 2, 0, 0]}
118
+ scale={[1, 1, 0.84]}
119
+ />
120
+ <T.Mesh
121
+ geometry={gltf.nodes.Fan_Circle_Black_Fan_0.geometry}
122
+ material={gltf.materials.Black_Fan}
123
+ position={[127.5, 88.51, 10.29]}
124
+ rotation={[Math.PI / 2, 0, 0]}
125
+ scale={0.79}
126
+ />
127
+ <T.Mesh
128
+ geometry={gltf.nodes.Front_Cover_U_Black_0.geometry}
129
+ material={gltf.materials.Black}
130
+ position={[0.02, 26.08, 14.09]}
131
+ rotation={[Math.PI / 2, 0, 0]}
132
+ />
133
+ <T.Mesh
134
+ geometry={gltf.nodes.Front_Cover_T_Black_0.geometry}
135
+ material={gltf.materials.Black}
136
+ position={[-4.75, 163.4, 14.09]}
137
+ rotation={[-Math.PI / 2 , 0, -Math.PI]}
138
+ />
139
+ <T.Mesh
140
+ geometry={gltf.nodes.Fan_Circle_B_Black_Fan_0.geometry}
141
+ material={gltf.materials.Black_Fan}
142
+ position={[-124.15, 88.51, -40.18]}
143
+ rotation={[Math.PI / 2, 0, Math.PI]}
144
+ scale={0.79}
145
+ />
146
+ <T.Mesh
147
+ geometry={gltf.nodes.Grills_U_Metal_Black_0.geometry}
148
+ material={gltf.materials.Metal_Black}
149
+ position={[-0.12, 3.16, 3.09]}
150
+ rotation={[Math.PI / 2, -Math.PI / 4, 0]}
151
+ scale={[0.55, 11.75, 0.55]}
152
+ />
153
+ <T.Mesh
154
+ geometry={gltf.nodes.Grills_T_Metal_Black_0.geometry}
155
+ material={gltf.materials.Metal_Black}
156
+ position={[0.8, 174.49, 3.09]}
157
+ rotation={[-Math.PI / 2, Math.PI / 4, -Math.PI]}
158
+ scale={[0.55, 11.75, 0.55]}
159
+ />
160
+ <T.Mesh
161
+ geometry={gltf.nodes.Plane010_Black001_0.geometry}
162
+ material={gltf.materials['Black.001']}
163
+ position={[121.84, 88.42, -34.24]}
164
+ rotation={[-Math.PI / 2, 0, -Math.PI]}
165
+ scale={[1, 1, 0.84]}
166
+ />
167
+ <T.Mesh
168
+ geometry={gltf.nodes.Socket_Slot_0.geometry}
169
+ material={gltf.materials.Slot}
170
+ position={[-149.71, 187.47, -39.01]}
171
+ rotation={[Math.PI / 2, 0, 0]}
172
+ scale={[1, 1.93, 1]}
173
+ />
174
+ <T.Mesh
175
+ geometry={gltf.nodes.Side_Metal_Part_Metal_S_0.geometry}
176
+ material={gltf.materials.Metal_S}
177
+ position={[-225.87, 118.09, -12.54]}
178
+ rotation={[Math.PI / 2, 0, 0]}
179
+ />
180
+ <T.Mesh
181
+ geometry={gltf.nodes.Grills_F003_Metal_Black_0.geometry}
182
+ material={gltf.materials.Metal_Black}
183
+ position={[131.49, 88.84, -23.02]}
184
+ rotation={[Math.PI / 2, 0, 0]}
185
+ scale={[1, 1, 1.02]}
186
+ />
187
+ <T.Mesh
188
+ geometry={gltf.nodes.Grills_F002_Metal_Black_0.geometry}
189
+ material={gltf.materials.Metal_Black}
190
+ position={[-128.18, 88.84, -4.17]}
191
+ rotation={[Math.PI / 2, 0, Math.PI]}
192
+ scale={[1, 0.97, 1.02]}
193
+ />
194
+ </T.Group>
195
+ {:catch err}
196
+ {@render error?.({ error: err })}
197
+ {/await}
198
+
199
+ {@render children?.({ ref })}
200
+ </T.Group>
src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import * as Dialog from "@/components/ui/dialog";
3
+ import { Button } from "@/components/ui/button";
4
+ import * as Card from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import * as Alert from "@/components/ui/alert";
9
+ import { remoteComputeManager } from "$lib/elements/compute//RemoteComputeManager.svelte";
10
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
11
+ import type { AISessionConfig } from "$lib/elements/compute//RemoteComputeManager.svelte";
12
+ import { settings } from "$lib/runes/settings.svelte";
13
+ import { toast } from "svelte-sonner";
14
+
15
+ interface Props {
16
+ workspaceId: string;
17
+ open: boolean;
18
+ compute: RemoteCompute;
19
+ }
20
+
21
+ let { open = $bindable(), compute, workspaceId }: Props = $props();
22
+
23
+ let isConnecting = $state(false);
24
+ let sessionId = $state('');
25
+ let policyPath = $state('./checkpoints/act_so101_beyond');
26
+ let cameraNames = $state('front');
27
+ let useProvidedWorkspace = $state(false);
28
+
29
+ // Auto-generate session ID when modal opens
30
+ $effect(() => {
31
+ if (open && compute && !sessionId) {
32
+ sessionId = `${compute.id}-session-${Date.now()}`;
33
+ }
34
+ });
35
+
36
+ async function handleCreateSession() {
37
+ if (!compute) return;
38
+
39
+ if (!sessionId.trim() || !policyPath.trim()) {
40
+ toast.error('Please fill in all required fields');
41
+ return;
42
+ }
43
+
44
+ isConnecting = true;
45
+ try {
46
+ const cameras = cameraNames.split(',').map(name => name.trim()).filter(name => name);
47
+ if (cameras.length === 0) {
48
+ cameras.push('front');
49
+ }
50
+
51
+ const config: AISessionConfig = {
52
+ sessionId: sessionId.trim(),
53
+ policyPath: policyPath.trim(),
54
+ cameraNames: cameras,
55
+ transportServerUrl: settings.transportServerUrl,
56
+ workspaceId: useProvidedWorkspace ? workspaceId : undefined
57
+ };
58
+
59
+ const result = await remoteComputeManager.createSession(compute.id, config);
60
+ if (result.success) {
61
+ toast.success(`AI session created: ${sessionId}`);
62
+ open = false;
63
+ } else {
64
+ toast.error(`Failed to create session: ${result.error}`);
65
+ }
66
+ } catch (error) {
67
+ console.error('Session creation error:', error);
68
+ toast.error('Failed to create session');
69
+ } finally {
70
+ isConnecting = false;
71
+ }
72
+ }
73
+
74
+ async function handleStartSession() {
75
+ if (!compute) return;
76
+
77
+ isConnecting = true;
78
+ try {
79
+ const result = await remoteComputeManager.startSession(compute.id);
80
+ if (result.success) {
81
+ toast.success('AI session started');
82
+ } else {
83
+ toast.error(`Failed to start session: ${result.error}`);
84
+ }
85
+ } catch (error) {
86
+ console.error('Session start error:', error);
87
+ toast.error('Failed to start session');
88
+ } finally {
89
+ isConnecting = false;
90
+ }
91
+ }
92
+
93
+ async function handleStopSession() {
94
+ if (!compute) return;
95
+
96
+ isConnecting = true;
97
+ try {
98
+ const result = await remoteComputeManager.stopSession(compute.id);
99
+ if (result.success) {
100
+ toast.success('AI session stopped');
101
+ } else {
102
+ toast.error(`Failed to stop session: ${result.error}`);
103
+ }
104
+ } catch (error) {
105
+ console.error('Session stop error:', error);
106
+ toast.error('Failed to stop session');
107
+ } finally {
108
+ isConnecting = false;
109
+ }
110
+ }
111
+
112
+ async function handleDeleteSession() {
113
+ if (!compute) return;
114
+
115
+ isConnecting = true;
116
+ try {
117
+ const result = await remoteComputeManager.deleteSession(compute.id);
118
+ if (result.success) {
119
+ toast.success('AI session deleted');
120
+ } else {
121
+ toast.error(`Failed to delete session: ${result.error}`);
122
+ }
123
+ } catch (error) {
124
+ console.error('Session delete error:', error);
125
+ toast.error('Failed to delete session');
126
+ } finally {
127
+ isConnecting = false;
128
+ }
129
+ }
130
+ </script>
131
+
132
+ <Dialog.Root bind:open>
133
+ <Dialog.Content
134
+ class="max-h-[80vh] max-w-2xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
135
+ >
136
+ <Dialog.Header class="pb-3">
137
+ <Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
138
+ <span class="icon-[mdi--robot-outline] size-5 text-purple-400"></span>
139
+ AI Compute Session - {compute.name || 'No Compute Selected'}
140
+ </Dialog.Title>
141
+ <Dialog.Description class="text-sm text-slate-400">
142
+ Configure and manage ACT model inference sessions for robot control
143
+ </Dialog.Description>
144
+ </Dialog.Header>
145
+
146
+ <div class="space-y-4">
147
+ <!-- Current Session Status -->
148
+ <div
149
+ class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
150
+ >
151
+ <div class="flex items-center gap-2">
152
+ <span class="icon-[mdi--brain] size-4 text-purple-400"></span>
153
+ <span class="text-sm font-medium text-purple-300">Session Status</span>
154
+ </div>
155
+ {#if compute.hasSession}
156
+ <Badge variant="default" class="bg-purple-600 text-xs">
157
+ {compute.statusInfo.statusText}
158
+ </Badge>
159
+ {:else}
160
+ <Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
161
+ {/if}
162
+ </div>
163
+
164
+ <!-- Current Session Details -->
165
+ {#if compute.hasSession && compute.sessionData}
166
+ <Card.Root class="border-purple-500/30 bg-purple-500/5">
167
+ <Card.Header>
168
+ <Card.Title class="flex items-center gap-2 text-base text-purple-200">
169
+ <span class="icon-[mdi--cog] size-4"></span>
170
+ Current Session
171
+ </Card.Title>
172
+ </Card.Header>
173
+ <Card.Content>
174
+ <div class="space-y-3">
175
+ <div class="rounded-lg border border-purple-500/30 bg-purple-900/20 p-3">
176
+ <div class="grid grid-cols-2 gap-2 text-xs">
177
+ <div>
178
+ <span class="text-purple-300 font-medium">Session ID:</span>
179
+ <span class="text-purple-100 block">{compute.sessionId}</span>
180
+ </div>
181
+ <div>
182
+ <span class="text-purple-300 font-medium">Status:</span>
183
+ <span class="text-purple-100 block">{compute.statusInfo.emoji} {compute.statusInfo.statusText}</span>
184
+ </div>
185
+ <div>
186
+ <span class="text-purple-300 font-medium">Policy:</span>
187
+ <span class="text-purple-100 block">{compute.sessionConfig?.policyPath}</span>
188
+ </div>
189
+ <div>
190
+ <span class="text-purple-300 font-medium">Cameras:</span>
191
+ <span class="text-purple-100 block">{compute.sessionConfig?.cameraNames.join(', ')}</span>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Connection Details -->
197
+ <div class="rounded-lg border border-green-500/30 bg-green-900/20 p-3">
198
+ <div class="text-sm font-medium text-green-300 mb-2">📡 Inference Server Connections</div>
199
+ <div class="space-y-1 text-xs">
200
+ <div>
201
+ <span class="text-green-400">Workspace:</span>
202
+ <span class="text-green-200 font-mono ml-2">{compute.sessionData.workspace_id}</span>
203
+ </div>
204
+ {#each Object.entries(compute.sessionData.camera_room_ids) as [camera, roomId]}
205
+ <div>
206
+ <span class="text-green-400">📹 {camera}:</span>
207
+ <span class="text-green-200 font-mono ml-2">{roomId}</span>
208
+ </div>
209
+ {/each}
210
+ <div>
211
+ <span class="text-green-400">📥 Joint Input:</span>
212
+ <span class="text-green-200 font-mono ml-2">{compute.sessionData.joint_input_room_id}</span>
213
+ </div>
214
+ <div>
215
+ <span class="text-green-400">📤 Joint Output:</span>
216
+ <span class="text-green-200 font-mono ml-2">{compute.sessionData.joint_output_room_id}</span>
217
+ </div>
218
+ </div>
219
+ </div>
220
+
221
+ <!-- Session Controls -->
222
+ <div class="flex gap-2">
223
+ {#if compute.canStart}
224
+ <Button
225
+ variant="default"
226
+ size="sm"
227
+ onclick={handleStartSession}
228
+ disabled={isConnecting}
229
+ class="bg-green-600 hover:bg-green-700 text-xs disabled:opacity-50"
230
+ >
231
+ {#if isConnecting}
232
+ <span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
233
+ Starting...
234
+ {:else}
235
+ <span class="icon-[mdi--play] mr-1 size-3"></span>
236
+ Start Inference
237
+ {/if}
238
+ </Button>
239
+ {/if}
240
+ {#if compute.canStop}
241
+ <Button
242
+ variant="secondary"
243
+ size="sm"
244
+ onclick={handleStopSession}
245
+ disabled={isConnecting}
246
+ class="text-xs disabled:opacity-50"
247
+ >
248
+ {#if isConnecting}
249
+ <span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
250
+ Stopping...
251
+ {:else}
252
+ <span class="icon-[mdi--stop] mr-1 size-3"></span>
253
+ Stop Inference
254
+ {/if}
255
+ </Button>
256
+ {/if}
257
+ <Button
258
+ variant="destructive"
259
+ size="sm"
260
+ onclick={handleDeleteSession}
261
+ disabled={isConnecting}
262
+ class="text-xs disabled:opacity-50"
263
+ >
264
+ {#if isConnecting}
265
+ <span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
266
+ Deleting...
267
+ {:else}
268
+ <span class="icon-[mdi--delete] mr-1 size-3"></span>
269
+ Delete Session
270
+ {/if}
271
+ </Button>
272
+ </div>
273
+ </div>
274
+ </Card.Content>
275
+ </Card.Root>
276
+ {/if}
277
+
278
+ <!-- Create New Session -->
279
+ {#if !compute.hasSession}
280
+ <Card.Root class="border-purple-500/30 bg-purple-500/5">
281
+ <Card.Header>
282
+ <Card.Title class="flex items-center gap-2 text-base text-purple-200">
283
+ <span class="icon-[mdi--plus-circle] size-4"></span>
284
+ Create AI Session
285
+ </Card.Title>
286
+ </Card.Header>
287
+ <Card.Content>
288
+ <div class="space-y-4">
289
+ <div class="grid grid-cols-2 gap-4">
290
+ <div class="space-y-2">
291
+ <Label for="sessionId" class="text-purple-300">Session ID</Label>
292
+ <Input
293
+ id="sessionId"
294
+ bind:value={sessionId}
295
+ placeholder="my-session-01"
296
+ class="bg-slate-800 border-slate-600 text-slate-100"
297
+ />
298
+ </div>
299
+ <div class="space-y-2">
300
+ <Label for="policyPath" class="text-purple-300">Policy Path</Label>
301
+ <Input
302
+ id="policyPath"
303
+ bind:value={policyPath}
304
+ placeholder="./checkpoints/act_so101_beyond"
305
+ class="bg-slate-800 border-slate-600 text-slate-100"
306
+ />
307
+ </div>
308
+ </div>
309
+
310
+ <div class="grid grid-cols-2 gap-4">
311
+ <div class="space-y-2">
312
+ <Label for="cameraNames" class="text-purple-300">Camera Names</Label>
313
+ <Input
314
+ id="cameraNames"
315
+ bind:value={cameraNames}
316
+ placeholder="front, wrist, overhead"
317
+ class="bg-slate-800 border-slate-600 text-slate-100"
318
+ />
319
+ <p class="text-xs text-slate-400">Comma-separated camera names</p>
320
+ </div>
321
+ <div class="space-y-2">
322
+ <Label for="transportServerUrl" class="text-purple-300">Transport Server URL</Label>
323
+ <Input
324
+ id="transportServerUrl"
325
+ value={settings.transportServerUrl}
326
+ disabled
327
+ placeholder="http://localhost:8000"
328
+ class="bg-slate-800 border-slate-600 text-slate-100 opacity-60 cursor-not-allowed"
329
+ title="Change this value in the settings panel"
330
+ />
331
+ <p class="text-xs text-slate-400">Configure in settings panel</p>
332
+ </div>
333
+ </div>
334
+
335
+ <div class="flex items-center space-x-2">
336
+ <input
337
+ type="checkbox"
338
+ id="useWorkspace"
339
+ bind:checked={useProvidedWorkspace}
340
+ class="rounded border-slate-600 bg-slate-800"
341
+ />
342
+ <Label for="useWorkspace" class="text-purple-300 text-sm">
343
+ Use current workspace ({workspaceId})
344
+ </Label>
345
+ </div>
346
+
347
+ <Alert.Root>
348
+ <span class="icon-[mdi--information] size-4"></span>
349
+ <Alert.Description>
350
+ This will create a new ACT inference session with dedicated rooms for camera inputs,
351
+ joint inputs, and joint outputs in the inference server communication system.
352
+ </Alert.Description>
353
+ </Alert.Root>
354
+
355
+ <Button
356
+ variant="default"
357
+ onclick={handleCreateSession}
358
+ disabled={isConnecting || !sessionId.trim() || !policyPath.trim()}
359
+ class="w-full bg-purple-600 hover:bg-purple-700 disabled:opacity-50"
360
+ >
361
+ {#if isConnecting}
362
+ <span class="icon-[mdi--loading] animate-spin mr-2 size-4"></span>
363
+ Creating Session...
364
+ {:else}
365
+ <span class="icon-[mdi--rocket-launch] mr-2 size-4"></span>
366
+ Create AI Session
367
+ {/if}
368
+ </Button>
369
+ </div>
370
+ </Card.Content>
371
+ </Card.Root>
372
+ {/if}
373
+
374
+ <!-- Quick Info -->
375
+ <div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
376
+ <span class="icon-[mdi--information] mr-1 size-3"></span>
377
+ AI sessions require a trained ACT model and create dedicated communication rooms for video inputs,
378
+ robot joint states, and control outputs in the inference server system.
379
+ </div>
380
+ </div>
381
+ </Dialog.Content>
382
+ </Dialog.Root>
src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import * as Dialog from "@/components/ui/dialog";
3
+ import { Button } from "@/components/ui/button";
4
+ import * as Card from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { toast } from "svelte-sonner";
7
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
8
+ import { robotManager } from "$lib/elements/robot/RobotManager.svelte";
9
+
10
+ interface Props {
11
+ workspaceId: string;
12
+ open: boolean;
13
+ compute: RemoteCompute;
14
+ }
15
+
16
+ let { open = $bindable(), compute, workspaceId }: Props = $props();
17
+
18
+ let isConnecting = $state(false);
19
+ let selectedRobotId = $state('');
20
+ let robotProducer: any = null;
21
+ let connectedRobotId = $state<string | null>(null);
22
+
23
+ // Get available robots from robot manager
24
+ const robots = $derived(robotManager.robots);
25
+
26
+ async function handleConnectRobotInput() {
27
+ if (!compute.hasSession) {
28
+ toast.error('No AI session available. Create a session first.');
29
+ return;
30
+ }
31
+
32
+ if (!selectedRobotId) {
33
+ toast.error('Please select a robot to connect.');
34
+ return;
35
+ }
36
+
37
+ isConnecting = true;
38
+ try {
39
+ // Get the joint input room ID from the AI session
40
+ const jointInputRoomId = compute.sessionData?.joint_input_room_id;
41
+ if (!jointInputRoomId) {
42
+ throw new Error('No joint input room found in AI session');
43
+ }
44
+
45
+ // Find the selected robot
46
+ const robot = robotManager.robots.find(r => r.id === selectedRobotId);
47
+ if (!robot) {
48
+ throw new Error(`Robot ${selectedRobotId} not found`);
49
+ }
50
+
51
+ // Connect robot as PRODUCER to the joint input room (robot sends joint states TO AI)
52
+ await robotManager.connectProducerToRoom(workspaceId, selectedRobotId, jointInputRoomId);
53
+
54
+ connectedRobotId = selectedRobotId;
55
+
56
+ toast.success('Robot input connected to AI session', {
57
+ description: `Robot ${selectedRobotId} now sends joint data to AI`
58
+ });
59
+
60
+ } catch (error) {
61
+ console.error('Robot input connection error:', error);
62
+ toast.error('Failed to connect robot input', {
63
+ description: error instanceof Error ? error.message : 'Unknown error'
64
+ });
65
+ } finally {
66
+ isConnecting = false;
67
+ }
68
+ }
69
+
70
+ async function handleDisconnectRobotInput() {
71
+ if (!connectedRobotId) return;
72
+
73
+ try {
74
+ // Find the connected robot
75
+ const robot = robotManager.robots.find(r => r.id === connectedRobotId);
76
+ if (robot) {
77
+ // Disconnect producer from the joint input room
78
+ for (const producer of robot.producers) {
79
+ await robot.removeProducer(producer.id);
80
+ }
81
+ }
82
+
83
+ connectedRobotId = null;
84
+ toast.success('Robot input disconnected');
85
+ } catch (error) {
86
+ console.error('Disconnect error:', error);
87
+ toast.error('Error disconnecting robot input');
88
+ }
89
+ }
90
+
91
+ // Cleanup on modal close
92
+ $effect(() => {
93
+ if (!open) {
94
+ // Don't auto-disconnect when modal closes, user might want to keep connection
95
+ }
96
+ });
97
+ </script>
98
+
99
+ <Dialog.Root bind:open>
100
+ <Dialog.Content
101
+ class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
102
+ >
103
+ <Dialog.Header class="pb-3">
104
+ <Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
105
+ <span class="icon-[mdi--robot-industrial] size-5 text-amber-400"></span>
106
+ Robot Input - {compute.name || 'No Compute Selected'}
107
+ </Dialog.Title>
108
+ <Dialog.Description class="text-sm text-slate-400">
109
+ Connect robot joint data as input for AI inference
110
+ </Dialog.Description>
111
+ </Dialog.Header>
112
+
113
+ <div class="space-y-4">
114
+ <!-- AI Session Status -->
115
+ <div
116
+ class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
117
+ >
118
+ <div class="flex items-center gap-2">
119
+ <span class="icon-[mdi--brain] size-4 text-purple-400"></span>
120
+ <span class="text-sm font-medium text-purple-300">AI Session</span>
121
+ </div>
122
+ {#if compute.hasSession}
123
+ <Badge variant="default" class="bg-purple-600 text-xs">
124
+ {compute.statusInfo.statusText}
125
+ </Badge>
126
+ {:else}
127
+ <Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
128
+ {/if}
129
+ </div>
130
+
131
+ {#if !compute.hasSession}
132
+ <Card.Root class="border-yellow-500/30 bg-yellow-500/5">
133
+ <Card.Header>
134
+ <Card.Title class="flex items-center gap-2 text-base text-yellow-200">
135
+ <span class="icon-[mdi--alert] size-4"></span>
136
+ AI Session Required
137
+ </Card.Title>
138
+ </Card.Header>
139
+ <Card.Content class="text-sm text-yellow-300">
140
+ You need to create an AI session before connecting robot inputs.
141
+ The session provides a joint input room for receiving robot data.
142
+ </Card.Content>
143
+ </Card.Root>
144
+ {:else}
145
+ <!-- Robot Selection and Connection -->
146
+ <Card.Root class="border-amber-500/30 bg-amber-500/5">
147
+ <Card.Header>
148
+ <Card.Title class="flex items-center gap-2 text-base text-amber-200">
149
+ <span class="icon-[mdi--robot-industrial] size-4"></span>
150
+ Robot Input Connection
151
+ </Card.Title>
152
+ </Card.Header>
153
+ <Card.Content class="space-y-4">
154
+ <!-- Available Robots -->
155
+ <div class="space-y-2">
156
+ <div class="text-sm font-medium text-amber-300">Available Robots:</div>
157
+ <div class="max-h-40 overflow-y-auto space-y-2">
158
+ {#if robots.length === 0}
159
+ <div class="text-center py-4 text-sm text-slate-400">
160
+ No robots available. Add robots first.
161
+ </div>
162
+ {:else}
163
+ {#each robots as robot}
164
+ <button
165
+ onclick={() => selectedRobotId = robot.id}
166
+ class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
167
+ ? 'border-amber-500 bg-amber-500/20'
168
+ : 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
169
+ >
170
+ <div class="flex items-center justify-between">
171
+ <div>
172
+ <div class="text-xs text-slate-400">
173
+ ID: {robot.id}
174
+ </div>
175
+ <div class="text-xs text-slate-400">
176
+ Producers: {robot.producers.length}
177
+ </div>
178
+ </div>
179
+ <div class="flex items-center gap-2">
180
+ {#if robot.producers.length > 0}
181
+ <Badge variant="default" class="bg-green-600 text-xs">
182
+ Active
183
+ </Badge>
184
+ {:else}
185
+ <Badge variant="secondary" class="text-xs">
186
+ Available
187
+ </Badge>
188
+ {/if}
189
+ </div>
190
+ </div>
191
+ </button>
192
+ {/each}
193
+ {/if}
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Connection Status -->
198
+ {#if selectedRobotId}
199
+ <div class="rounded-lg border border-amber-500/30 bg-amber-900/20 p-3">
200
+ <div class="flex items-center justify-between">
201
+ <div>
202
+ <p class="text-sm font-medium text-amber-300">
203
+ Selected Robot: {selectedRobotId}
204
+ </p>
205
+ <p class="text-xs text-amber-400/70">
206
+ {connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
207
+ </p>
208
+ </div>
209
+ {#if connectedRobotId !== selectedRobotId}
210
+ <Button
211
+ variant="default"
212
+ size="sm"
213
+ onclick={handleConnectRobotInput}
214
+ disabled={isConnecting}
215
+ class="bg-amber-600 hover:bg-amber-700 text-xs disabled:opacity-50"
216
+ >
217
+ {#if isConnecting}
218
+ <span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
219
+ Connecting...
220
+ {:else}
221
+ <span class="icon-[mdi--link] mr-1 size-3"></span>
222
+ Connect Input
223
+ {/if}
224
+ </Button>
225
+ {:else}
226
+ <Button
227
+ variant="destructive"
228
+ size="sm"
229
+ onclick={handleDisconnectRobotInput}
230
+ class="text-xs"
231
+ >
232
+ <span class="icon-[mdi--close-circle] mr-1 size-3"></span>
233
+ Disconnect
234
+ </Button>
235
+ {/if}
236
+ </div>
237
+ </div>
238
+ {/if}
239
+ </Card.Content>
240
+ </Card.Root>
241
+
242
+ <!-- Session Joint Input Details -->
243
+ <Card.Root class="border-blue-500/30 bg-blue-500/5">
244
+ <Card.Header>
245
+ <Card.Title class="flex items-center gap-2 text-base text-blue-200">
246
+ <span class="icon-[mdi--information] size-4"></span>
247
+ Data Flow: Robot → AI Session
248
+ </Card.Title>
249
+ </Card.Header>
250
+ <Card.Content>
251
+ <div class="space-y-2 text-xs">
252
+ <div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
253
+ <span class="text-blue-300 font-medium">Joint Input Room:</span>
254
+ <span class="text-blue-200 font-mono">{compute.sessionData?.joint_input_room_id}</span>
255
+ </div>
256
+ <div class="text-slate-400 text-xs">
257
+ The robot will act as a <strong>PRODUCER</strong> and send its current joint positions to this room for AI processing.
258
+ The inference server receives this data as a CONSUMER.
259
+ All joint values should be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
260
+ </div>
261
+ </div>
262
+ </Card.Content>
263
+ </Card.Root>
264
+
265
+ <!-- Connection Status -->
266
+ {#if connectedRobotId}
267
+ <Card.Root class="border-green-500/30 bg-green-500/5">
268
+ <Card.Header>
269
+ <Card.Title class="flex items-center gap-2 text-base text-green-200">
270
+ <span class="icon-[mdi--check-circle] size-4"></span>
271
+ Active Connection
272
+ </Card.Title>
273
+ </Card.Header>
274
+ <Card.Content>
275
+ <div class="text-sm text-green-300">
276
+ Robot <span class="font-mono">{connectedRobotId}</span> is now sending joint data to the AI session as a producer.
277
+ The AI model will use this data along with camera inputs for inference.
278
+ </div>
279
+ </Card.Content>
280
+ </Card.Root>
281
+ {/if}
282
+ {/if}
283
+
284
+ <!-- Quick Info -->
285
+ <div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
286
+ <span class="icon-[mdi--information] mr-1 size-3"></span>
287
+ Robot input: Robot acts as PRODUCER sending joint positions → Inference server acts as CONSUMER receiving data for processing.
288
+ </div>
289
+ </div>
290
+ </Dialog.Content>
291
+ </Dialog.Root>
src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import * as Dialog from "@/components/ui/dialog";
3
+ import { Button } from "@/components/ui/button";
4
+ import * as Card from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { toast } from "svelte-sonner";
7
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
8
+ import { robotManager } from "$lib/elements/robot/RobotManager.svelte";
9
+
10
+ interface Props {
11
+ workspaceId: string;
12
+ open: boolean;
13
+ compute: RemoteCompute;
14
+ }
15
+
16
+ let { open = $bindable(), compute, workspaceId }: Props = $props();
17
+
18
+ let isConnecting = $state(false);
19
+ let selectedRobotId = $state('');
20
+ let robotConsumer: any = null;
21
+ let connectedRobotId = $state<string | null>(null);
22
+
23
+ // Get available robots from robot manager
24
+ const robots = $derived(robotManager.robots);
25
+
26
+ async function handleConnectRobotOutput() {
27
+ if (!compute.hasSession) {
28
+ toast.error('No AI session available. Create a session first.');
29
+ return;
30
+ }
31
+
32
+ if (!selectedRobotId) {
33
+ toast.error('Please select a robot to connect.');
34
+ return;
35
+ }
36
+
37
+ isConnecting = true;
38
+ try {
39
+ // Get the joint output room ID from the AI session
40
+ const jointOutputRoomId = compute.sessionData?.joint_output_room_id;
41
+ if (!jointOutputRoomId) {
42
+ throw new Error('No joint output room found in AI session');
43
+ }
44
+
45
+ // Find the selected robot
46
+ const robot = robotManager.robots.find(r => r.id === selectedRobotId);
47
+ if (!robot) {
48
+ throw new Error(`Robot ${selectedRobotId} not found`);
49
+ }
50
+
51
+ // Connect robot as CONSUMER to the joint output room (robot receives commands FROM AI)
52
+ await robotManager.connectConsumerToRoom(workspaceId, selectedRobotId, jointOutputRoomId);
53
+
54
+ connectedRobotId = selectedRobotId;
55
+
56
+ toast.success('Robot output connected to AI session', {
57
+ description: `Robot ${selectedRobotId} now receives AI commands`
58
+ });
59
+
60
+ } catch (error) {
61
+ console.error('Robot output connection error:', error);
62
+ toast.error('Failed to connect robot output', {
63
+ description: error instanceof Error ? error.message : 'Unknown error'
64
+ });
65
+ } finally {
66
+ isConnecting = false;
67
+ }
68
+ }
69
+
70
+ async function handleDisconnectRobotOutput() {
71
+ if (!connectedRobotId) return;
72
+
73
+ try {
74
+ // Find the connected robot
75
+ const robot = robotManager.robots.find(r => r.id === connectedRobotId);
76
+ if (robot) {
77
+ await robot.removeConsumer();
78
+ }
79
+
80
+ connectedRobotId = null;
81
+ toast.success('Robot output disconnected');
82
+ } catch (error) {
83
+ console.error('Disconnect error:', error);
84
+ toast.error('Error disconnecting robot output');
85
+ }
86
+ }
87
+
88
+ // Cleanup on modal close
89
+ $effect(() => {
90
+ if (!open) {
91
+ // Don't auto-disconnect when modal closes, user might want to keep connection
92
+ }
93
+ });
94
+ </script>
95
+
96
+ <Dialog.Root bind:open>
97
+ <Dialog.Content
98
+ class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
99
+ >
100
+ <Dialog.Header class="pb-3">
101
+ <Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
102
+ <span class="icon-[mdi--robot-outline] size-5 text-blue-400"></span>
103
+ Robot Output - {compute.name || 'No Compute Selected'}
104
+ </Dialog.Title>
105
+ <Dialog.Description class="text-sm text-slate-400">
106
+ Connect AI command output to control robot actuators
107
+ </Dialog.Description>
108
+ </Dialog.Header>
109
+
110
+ <div class="space-y-4">
111
+ <!-- AI Session Status -->
112
+ <div
113
+ class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
114
+ >
115
+ <div class="flex items-center gap-2">
116
+ <span class="icon-[mdi--brain] size-4 text-purple-400"></span>
117
+ <span class="text-sm font-medium text-purple-300">AI Session</span>
118
+ </div>
119
+ {#if compute.hasSession}
120
+ <Badge variant="default" class="bg-purple-600 text-xs">
121
+ {compute.statusInfo.statusText}
122
+ </Badge>
123
+ {:else}
124
+ <Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
125
+ {/if}
126
+ </div>
127
+
128
+ {#if !compute.hasSession}
129
+ <Card.Root class="border-yellow-500/30 bg-yellow-500/5">
130
+ <Card.Header>
131
+ <Card.Title class="flex items-center gap-2 text-base text-yellow-200">
132
+ <span class="icon-[mdi--alert] size-4"></span>
133
+ AI Session Required
134
+ </Card.Title>
135
+ </Card.Header>
136
+ <Card.Content class="text-sm text-yellow-300">
137
+ You need to create an AI session before connecting robot outputs.
138
+ The session provides a joint output room for sending AI commands.
139
+ </Card.Content>
140
+ </Card.Root>
141
+ {:else}
142
+ <!-- Robot Selection and Connection -->
143
+ <Card.Root class="border-blue-500/30 bg-blue-500/5">
144
+ <Card.Header>
145
+ <Card.Title class="flex items-center gap-2 text-base text-blue-200">
146
+ <span class="icon-[mdi--robot-outline] size-4"></span>
147
+ Robot Output Connection
148
+ </Card.Title>
149
+ </Card.Header>
150
+ <Card.Content class="space-y-4">
151
+ <!-- Available Robots -->
152
+ <div class="space-y-2">
153
+ <div class="text-sm font-medium text-blue-300">Available Robots:</div>
154
+ <div class="max-h-40 overflow-y-auto space-y-2">
155
+ {#if robots.length === 0}
156
+ <div class="text-center py-4 text-sm text-slate-400">
157
+ No robots available. Add robots first.
158
+ </div>
159
+ {:else}
160
+ {#each robots as robot}
161
+ <button
162
+ onclick={() => selectedRobotId = robot.id}
163
+ class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
164
+ ? 'border-blue-500 bg-blue-500/20'
165
+ : 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
166
+ >
167
+ <div class="flex items-center justify-between">
168
+ <div>
169
+ <div class="text-xs text-slate-400">
170
+ ID: {robot.id}
171
+ </div>
172
+ <div class="text-xs text-slate-400">
173
+ Consumer: {robot.hasConsumer ? 'Connected' : 'None'}
174
+ </div>
175
+ </div>
176
+ <div class="flex items-center gap-2">
177
+ {#if robot.hasConsumer}
178
+ <Badge variant="default" class="bg-green-600 text-xs">
179
+ Active
180
+ </Badge>
181
+ {:else}
182
+ <Badge variant="secondary" class="text-xs">
183
+ Available
184
+ </Badge>
185
+ {/if}
186
+ </div>
187
+ </div>
188
+ </button>
189
+ {/each}
190
+ {/if}
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Connection Status -->
195
+ {#if selectedRobotId}
196
+ <div class="rounded-lg border border-blue-500/30 bg-blue-900/20 p-3">
197
+ <div class="flex items-center justify-between">
198
+ <div>
199
+ <p class="text-sm font-medium text-blue-300">
200
+ Selected Robot: {selectedRobotId}
201
+ </p>
202
+ <p class="text-xs text-blue-400/70">
203
+ {connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
204
+ </p>
205
+ </div>
206
+ {#if connectedRobotId !== selectedRobotId}
207
+ <Button
208
+ variant="default"
209
+ size="sm"
210
+ onclick={handleConnectRobotOutput}
211
+ disabled={isConnecting}
212
+ class="bg-blue-600 hover:bg-blue-700 text-xs disabled:opacity-50"
213
+ >
214
+ {#if isConnecting}
215
+ <span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
216
+ Connecting...
217
+ {:else}
218
+ <span class="icon-[mdi--link] mr-1 size-3"></span>
219
+ Connect Output
220
+ {/if}
221
+ </Button>
222
+ {:else}
223
+ <Button
224
+ variant="destructive"
225
+ size="sm"
226
+ onclick={handleDisconnectRobotOutput}
227
+ class="text-xs"
228
+ >
229
+ <span class="icon-[mdi--close-circle] mr-1 size-3"></span>
230
+ Disconnect
231
+ </Button>
232
+ {/if}
233
+ </div>
234
+ </div>
235
+ {/if}
236
+ </Card.Content>
237
+ </Card.Root>
238
+
239
+ <!-- Session Joint Output Details -->
240
+ <Card.Root class="border-orange-500/30 bg-orange-500/5">
241
+ <Card.Header>
242
+ <Card.Title class="flex items-center gap-2 text-base text-orange-200">
243
+ <span class="icon-[mdi--information] size-4"></span>
244
+ Data Flow: AI Session → Robot
245
+ </Card.Title>
246
+ </Card.Header>
247
+ <Card.Content>
248
+ <div class="space-y-2 text-xs">
249
+ <div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
250
+ <span class="text-orange-300 font-medium">Joint Output Room:</span>
251
+ <span class="text-orange-200 font-mono">{compute.sessionData?.joint_output_room_id}</span>
252
+ </div>
253
+ <div class="text-slate-400 text-xs">
254
+ The inference server will act as a <strong>PRODUCER</strong> and send predicted joint commands to this room for robot execution.
255
+ The robot receives this data as a CONSUMER.
256
+ All joint values will be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
257
+ </div>
258
+ </div>
259
+ </Card.Content>
260
+ </Card.Root>
261
+
262
+ <!-- Connection Status -->
263
+ {#if connectedRobotId}
264
+ <Card.Root class="border-green-500/30 bg-green-500/5">
265
+ <Card.Header>
266
+ <Card.Title class="flex items-center gap-2 text-base text-green-200">
267
+ <span class="icon-[mdi--check-circle] size-4"></span>
268
+ Active Connection
269
+ </Card.Title>
270
+ </Card.Header>
271
+ <Card.Content>
272
+ <div class="text-sm text-green-300">
273
+ Robot <span class="font-mono">{connectedRobotId}</span> is now receiving AI commands as a consumer.
274
+ The robot will execute joint movements based on AI inference results.
275
+ </div>
276
+ </Card.Content>
277
+ </Card.Root>
278
+ {/if}
279
+ {/if}
280
+
281
+ <!-- Quick Info -->
282
+ <div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
283
+ <span class="icon-[mdi--information] mr-1 size-3"></span>
284
+ Robot output: Inference server acts as PRODUCER sending commands → Robot acts as CONSUMER receiving and executing movements.
285
+ </div>
286
+ </div>
287
+ </Dialog.Content>
288
+ </Dialog.Root>
src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import * as Dialog from "@/components/ui/dialog";
3
+ import { Button } from "@/components/ui/button";
4
+ import * as Card from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { toast } from "svelte-sonner";
7
+ import { settings } from "$lib/runes/settings.svelte";
8
+ import { videoManager } from "$lib/elements/video//VideoManager.svelte";
9
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
10
+
11
+ interface Props {
12
+ workspaceId: string;
13
+ open: boolean;
14
+ compute: RemoteCompute;
15
+ }
16
+
17
+ let { open = $bindable(), compute, workspaceId }: Props = $props();
18
+
19
+ let isConnecting = $state(false);
20
+ let selectedCameraName = $state('front');
21
+ let localStream: MediaStream | null = $state(null);
22
+ let videoProducer: any = null;
23
+
24
+ // Auto-refresh rooms when modal opens
25
+ $effect(() => {
26
+ if (open) {
27
+ videoManager.refreshRooms(workspaceId);
28
+ }
29
+ });
30
+
31
+ async function handleConnectLocalCamera() {
32
+ if (!compute.hasSession) {
33
+ toast.error('No AI session available. Create a session first.');
34
+ return;
35
+ }
36
+
37
+ isConnecting = true;
38
+ try {
39
+ // Get user media
40
+ const stream = await navigator.mediaDevices.getUserMedia({
41
+ video: true,
42
+ audio: false
43
+ });
44
+ localStream = stream;
45
+
46
+ // Get the camera room ID for the selected camera
47
+ const cameraRoomId = compute.sessionData?.camera_room_ids[selectedCameraName];
48
+ if (!cameraRoomId) {
49
+ throw new Error(`No room found for camera: ${selectedCameraName}`);
50
+ }
51
+
52
+ // Create video producer and connect to the camera room
53
+ const { VideoProducer } = await import("@robohub/transport-server-client/video");
54
+ videoProducer = new VideoProducer(settings.transportServerUrl);
55
+
56
+ // Connect to the EXISTING camera room (don't create new one)
57
+ const participantId = `frontend-camera-${selectedCameraName}-${Date.now()}`;
58
+ const success = await videoProducer.connect(workspaceId, cameraRoomId, participantId);
59
+
60
+ if (!success) {
61
+ throw new Error('Failed to connect to camera room');
62
+ }
63
+
64
+ // Start streaming
65
+ await videoProducer.startCamera();
66
+
67
+ toast.success(`Camera connected to AI session`, {
68
+ description: `Local camera streaming to ${selectedCameraName} input`
69
+ });
70
+
71
+ } catch (error) {
72
+ console.error('Camera connection error:', error);
73
+ toast.error('Failed to connect camera', {
74
+ description: error instanceof Error ? error.message : 'Unknown error'
75
+ });
76
+ } finally {
77
+ isConnecting = false;
78
+ }
79
+ }
80
+
81
+ async function handleDisconnectCamera() {
82
+ try {
83
+ if (videoProducer) {
84
+ await videoProducer.stopStreaming();
85
+ await videoProducer.disconnect();
86
+ videoProducer = null;
87
+ }
88
+
89
+ if (localStream) {
90
+ localStream.getTracks().forEach(track => track.stop());
91
+ localStream = null;
92
+ }
93
+
94
+ toast.success('Camera disconnected');
95
+ } catch (error) {
96
+ console.error('Disconnect error:', error);
97
+ toast.error('Error disconnecting camera');
98
+ }
99
+ }
100
+
101
+ // Cleanup on modal close
102
+ $effect(() => {
103
+ return () => {
104
+ if (!open) {
105
+ handleDisconnectCamera();
106
+ }
107
+ };
108
+ });
109
+ </script>
110
+
111
+ <Dialog.Root bind:open>
112
+ <Dialog.Content
113
+ class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
114
+ >
115
+ <Dialog.Header class="pb-3">
116
+ <Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
117
+ <span class="icon-[mdi--video-input-component] size-5 text-green-400"></span>
118
+ Video Input - {compute.name || 'No Compute Selected'}
119
+ </Dialog.Title>
120
+ <Dialog.Description class="text-sm text-slate-400">
121
+ Connect camera streams to provide visual input for AI inference
122
+ </Dialog.Description>
123
+ </Dialog.Header>
124
+
125
+ <div class="space-y-4">
126
+ <!-- AI Session Status -->
127
+ <div
128
+ class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
129
+ >
130
+ <div class="flex items-center gap-2">
131
+ <span class="icon-[mdi--brain] size-4 text-purple-400"></span>
132
+ <span class="text-sm font-medium text-purple-300">AI Session</span>
133
+ </div>
134
+ {#if compute.hasSession}
135
+ <Badge variant="default" class="bg-purple-600 text-xs">
136
+ {compute.statusInfo.statusText}
137
+ </Badge>
138
+ {:else}
139
+ <Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
140
+ {/if}
141
+ </div>
142
+
143
+ {#if !compute.hasSession}
144
+ <Card.Root class="border-yellow-500/30 bg-yellow-500/5">
145
+ <Card.Header>
146
+ <Card.Title class="flex items-center gap-2 text-base text-yellow-200">
147
+ <span class="icon-[mdi--alert] size-4"></span>
148
+ AI Session Required
149
+ </Card.Title>
150
+ </Card.Header>
151
+ <Card.Content class="text-sm text-yellow-300">
152
+ You need to create an AI session before connecting video inputs.
153
+ The session defines which camera names are available for connection.
154
+ </Card.Content>
155
+ </Card.Root>
156
+ {:else}
157
+ <!-- Camera Selection and Connection -->
158
+ <Card.Root class="border-green-500/30 bg-green-500/5">
159
+ <Card.Header>
160
+ <Card.Title class="flex items-center gap-2 text-base text-green-200">
161
+ <span class="icon-[mdi--camera] size-4"></span>
162
+ Camera Connection
163
+ </Card.Title>
164
+ </Card.Header>
165
+ <Card.Content class="space-y-4">
166
+ <!-- Available Cameras -->
167
+ <div class="space-y-2">
168
+ <div class="text-sm font-medium text-green-300">Available Camera Inputs:</div>
169
+ <div class="grid grid-cols-2 gap-2">
170
+ {#each compute.sessionConfig?.cameraNames || [] as cameraName}
171
+ <button
172
+ onclick={() => selectedCameraName = cameraName}
173
+ class="p-2 rounded border text-left {selectedCameraName === cameraName
174
+ ? 'border-green-500 bg-green-500/20'
175
+ : 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
176
+ >
177
+ <div class="text-sm font-medium">{cameraName}</div>
178
+ <div class="text-xs text-slate-400">
179
+ Room: {compute.sessionData?.camera_room_ids[cameraName]?.slice(-8)}
180
+ </div>
181
+ </button>
182
+ {/each}
183
+ </div>
184
+ </div>
185
+
186
+ <!-- Connection Status -->
187
+ <div class="rounded-lg border border-green-500/30 bg-green-900/20 p-3">
188
+ <div class="flex items-center justify-between">
189
+ <div>
190
+ <p class="text-sm font-medium text-green-300">
191
+ Selected Camera: {selectedCameraName}
192
+ </p>
193
+ <p class="text-xs text-green-400/70">
194
+ {localStream ? 'Connected' : 'Not Connected'}
195
+ </p>
196
+ </div>
197
+ {#if !localStream}
198
+ <Button
199
+ variant="default"
200
+ size="sm"
201
+ onclick={handleConnectLocalCamera}
202
+ disabled={isConnecting}
203
+ class="bg-green-600 hover:bg-green-700 text-xs disabled:opacity-50"
204
+ >
205
+ {#if isConnecting}
206
+ <span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
207
+ Connecting...
208
+ {:else}
209
+ <span class="icon-[mdi--camera] mr-1 size-3"></span>
210
+ Connect Camera
211
+ {/if}
212
+ </Button>
213
+ {:else}
214
+ <Button
215
+ variant="destructive"
216
+ size="sm"
217
+ onclick={handleDisconnectCamera}
218
+ class="text-xs"
219
+ >
220
+ <span class="icon-[mdi--close-circle] mr-1 size-3"></span>
221
+ Disconnect
222
+ </Button>
223
+ {/if}
224
+ </div>
225
+ </div>
226
+
227
+ <!-- Live Preview -->
228
+ {#if localStream}
229
+ <div class="space-y-2">
230
+ <div class="text-sm font-medium text-green-300">Live Preview:</div>
231
+ <div class="rounded border border-green-500/30 bg-black/50 aspect-video overflow-hidden">
232
+ <video
233
+ autoplay
234
+ muted
235
+ playsinline
236
+ class="w-full h-full object-cover"
237
+ onloadedmetadata={(e) => {
238
+ const video = e.target as HTMLVideoElement;
239
+ video.srcObject = localStream;
240
+ }}
241
+ ></video>
242
+ </div>
243
+ </div>
244
+ {/if}
245
+ </Card.Content>
246
+ </Card.Root>
247
+
248
+ <!-- Session Camera Details -->
249
+ <Card.Root class="border-blue-500/30 bg-blue-500/5">
250
+ <Card.Header>
251
+ <Card.Title class="flex items-center gap-2 text-base text-blue-200">
252
+ <span class="icon-[mdi--information] size-4"></span>
253
+ Session Camera Details
254
+ </Card.Title>
255
+ </Card.Header>
256
+ <Card.Content>
257
+ <div class="space-y-2 text-xs">
258
+ {#each Object.entries(compute.sessionData?.camera_room_ids || {}) as [camera, roomId]}
259
+ <div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
260
+ <span class="text-blue-300 font-medium">{camera}</span>
261
+ <span class="text-blue-200 font-mono">{roomId}</span>
262
+ </div>
263
+ {/each}
264
+ </div>
265
+ </Card.Content>
266
+ </Card.Root>
267
+ {/if}
268
+
269
+ <!-- Quick Info -->
270
+ <div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
271
+ <span class="icon-[mdi--information] mr-1 size-3"></span>
272
+ Video inputs stream camera data to the AI model for visual processing. Each camera connects to a dedicated room in the session.
273
+ </div>
274
+ </div>
275
+ </Dialog.Content>
276
+ </Dialog.Root>
src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
3
+ import { ICON } from "$lib/utils/icon";
4
+ import {
5
+ BaseStatusBox,
6
+ StatusHeader,
7
+ StatusContent,
8
+ StatusIndicator
9
+ } from "$lib/components/3d/ui";
10
+ import { Text } from "threlte-uikit";
11
+
12
+ interface Props {
13
+ compute: RemoteCompute;
14
+ }
15
+
16
+ let { compute }: Props = $props();
17
+
18
+ // Compute theme color
19
+ const computeColor = "rgb(139, 69, 219)";
20
+ </script>
21
+
22
+
23
+ <BaseStatusBox
24
+ minWidth={110}
25
+ minHeight={135}
26
+ color={computeColor}
27
+ borderOpacity={0.6}
28
+ backgroundOpacity={0.2}
29
+ clickable={false}
30
+ >
31
+ <!-- Header -->
32
+ <StatusHeader
33
+ icon={ICON["icon-[mdi--brain]"].svg}
34
+ text="AI COMPUTE"
35
+ color={computeColor}
36
+ opacity={0.9}
37
+ fontSize={12}
38
+ />
39
+
40
+ <!-- Compute Info -->
41
+ <StatusContent
42
+ title={compute.name}
43
+ subtitle={compute.statusInfo.statusText}
44
+ color="rgb(221, 214, 254)"
45
+ variant="primary"
46
+ />
47
+ </BaseStatusBox>
48
+
src/lib/components/3d/elements/compute/status/ComputeConnectionFlowBoxUIKit.svelte ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import VideoInputBoxUIKit from "./VideoInputBoxUIKit.svelte";
3
+ import RobotInputBoxUIKit from "./RobotInputBoxUIKit.svelte";
4
+ import ComputeOutputBoxUIKit from "./ComputeOutputBoxUIKit.svelte";
5
+ import ComputeBoxUIKit from "./ComputeBoxUIKit.svelte";
6
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
7
+ import { Container } from "threlte-uikit";
8
+ import { StatusArrow } from "$lib/components/3d/ui";
9
+
10
+ interface Props {
11
+ compute: RemoteCompute;
12
+ onVideoInputBoxClick: (compute: RemoteCompute) => void;
13
+ onRobotInputBoxClick: (compute: RemoteCompute) => void;
14
+ onRobotOutputBoxClick: (compute: RemoteCompute) => void;
15
+ }
16
+
17
+ let { compute, onVideoInputBoxClick, onRobotInputBoxClick, onRobotOutputBoxClick }: Props = $props();
18
+
19
+ // Colors
20
+ const inputColor = "rgb(34, 197, 94)";
21
+ const outputColor = "rgb(59, 130, 246)";
22
+ </script>
23
+
24
+ <!--
25
+ @component
26
+ Elegant 2->1->1 connection flow layout for AI compute processing.
27
+ Clean vertical stacking of inputs that merge into compute, then flow to output.
28
+ -->
29
+
30
+ <Container flexDirection="row" alignItems="center" gap={12}>
31
+ <!-- Left: Stacked Inputs -->
32
+ <Container flexDirection="column" alignItems="center" gap={6}>
33
+ <VideoInputBoxUIKit {compute} handleClick={() => onVideoInputBoxClick(compute)} />
34
+ <RobotInputBoxUIKit {compute} handleClick={() => onRobotInputBoxClick(compute)} />
35
+ </Container>
36
+
37
+ <!-- Arrow: Inputs to Compute -->
38
+ <StatusArrow
39
+ direction="right"
40
+ color={inputColor}
41
+ opacity={compute.hasSession ? 1 : 0.5}
42
+ />
43
+
44
+ <!-- Center: Compute -->
45
+ <ComputeBoxUIKit {compute} />
46
+
47
+ <!-- Arrow: Compute to Output -->
48
+ <StatusArrow
49
+ direction="right"
50
+ color={outputColor}
51
+ opacity={compute.hasSession && compute.isRunning ? 1 : compute.hasSession ? 0.7 : 0.5}
52
+ />
53
+
54
+ <!-- Right: Output -->
55
+ <ComputeOutputBoxUIKit {compute} handleClick={() => onRobotOutputBoxClick(compute)} />
56
+ </Container>
src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
3
+ import { ICON } from "$lib/utils/icon";
4
+ import {
5
+ BaseStatusBox,
6
+ StatusHeader,
7
+ StatusContent,
8
+ StatusIndicator,
9
+ StatusButton
10
+ } from "$lib/components/3d/ui";
11
+ import { Container, SVG, Text } from "threlte-uikit";
12
+
13
+ interface Props {
14
+ compute: RemoteCompute;
15
+ handleClick?: () => void;
16
+ }
17
+
18
+ let { compute, handleClick }: Props = $props();
19
+
20
+ // Input theme color (green)
21
+ const inputColor = "rgb(34, 197, 94)";
22
+ </script>
23
+
24
+ <!--
25
+ @component
26
+ Compact input box showing the status of video and robot inputs for AI sessions.
27
+ Displays input connection information when session exists or connection prompt when disconnected.
28
+ -->
29
+
30
+ <BaseStatusBox
31
+ minWidth={120}
32
+ minHeight={80}
33
+ color={inputColor}
34
+ borderOpacity={compute.hasSession ? 0.8 : 0.4}
35
+ backgroundOpacity={0.2}
36
+ opacity={compute.hasSession ? 1 : 0.6}
37
+ onclick={handleClick}
38
+ >
39
+ {#if compute.hasSession && compute.inputConnections}
40
+ <!-- Active Input State -->
41
+ <StatusHeader
42
+ icon={ICON["icon-[material-symbols--download]"].svg}
43
+ text="INPUTS"
44
+ color={inputColor}
45
+ opacity={0.9}
46
+ fontSize={12}
47
+ />
48
+
49
+ <!-- Camera Inputs -->
50
+ <StatusContent
51
+ title={`${Object.keys(compute.inputConnections.cameras).length} Cameras`}
52
+ subtitle="Joint States"
53
+ color="rgb(187, 247, 208)"
54
+ variant="primary"
55
+ />
56
+
57
+ <!-- Active indicator -->
58
+ <StatusIndicator color={inputColor} />
59
+ {:else}
60
+ <!-- No Session State -->
61
+ <StatusHeader
62
+ icon={ICON["icon-[material-symbols--download]"].svg}
63
+ text="NO INPUTS"
64
+ color={inputColor}
65
+ opacity={0.7}
66
+ iconSize={12}
67
+ fontSize={12}
68
+ />
69
+
70
+ <StatusContent
71
+ title="Setup Required"
72
+ color="rgb(134, 239, 172)"
73
+ variant="secondary"
74
+ />
75
+
76
+ <StatusButton
77
+ text="Add Session"
78
+ icon={ICON["icon-[mdi--plus]"].svg}
79
+ color={inputColor}
80
+ backgroundOpacity={0.1}
81
+ textOpacity={0.7}
82
+ />
83
+ {/if}
84
+ </BaseStatusBox>
src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
3
+ import { ICON } from "$lib/utils/icon";
4
+ import {
5
+ BaseStatusBox,
6
+ StatusHeader,
7
+ StatusContent,
8
+ StatusIndicator,
9
+ StatusButton
10
+ } from "$lib/components/3d/ui";
11
+ import { Container, SVG, Text } from "threlte-uikit";
12
+
13
+ interface Props {
14
+ compute: RemoteCompute;
15
+ handleClick?: () => void;
16
+ }
17
+
18
+ let { compute, handleClick }: Props = $props();
19
+
20
+ // Output theme color (blue)
21
+ const outputColor = "rgb(59, 130, 246)";
22
+
23
+ // Icons
24
+ // const exportIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTJsLTcgN3YtNEg1di02aDd2LTR6Ii8+PC9zdmc+";
25
+ // const robotIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTIgMTJjMC0yLjIxIDEuNzktNCA0LTRzNCAxLjc5IDQgNC0xLjc5IDQtNCA0LTQtMS43OS00LTR6TTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTAgMThDNy4zIDIwIDMuOCAxNi42IDMuOCAxMlM3LjMgNCA5IDRzNS4yIDMuNCA1LjIgOC02IDgtNSA4eiIvPjwvc3ZnPg==";
26
+ // const plusIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTNoLTZ2NmgtMnYtNkg1di0yaDZWNWgydjZoNnoiLz48L3N2Zz4=";
27
+ </script>
28
+
29
+ <!--
30
+ @component
31
+ Compact output box showing the status of robot outputs for AI sessions.
32
+ Displays output connection information when session exists or connection prompt when disconnected.
33
+ -->
34
+
35
+ <BaseStatusBox
36
+ minWidth={110}
37
+ minHeight={135}
38
+ color={outputColor}
39
+ borderOpacity={compute.hasSession && compute.isRunning ? 0.8 : 0.4}
40
+ backgroundOpacity={0.2}
41
+ opacity={compute.hasSession && compute.isRunning ? 1 : !compute.hasSession ? 0.4 : 0.6}
42
+ onclick={handleClick}
43
+ >
44
+ {#if compute.hasSession && compute.outputConnections}
45
+ <!-- Active Output State -->
46
+ <StatusHeader
47
+ icon={ICON["icon-[material-symbols--upload]"].svg}
48
+ text="OUTPUT"
49
+ color={outputColor}
50
+ opacity={0.9}
51
+ fontSize={12}
52
+ />
53
+
54
+ <StatusContent
55
+ title={compute.isRunning ? "Active" : "Ready"}
56
+ subtitle="Commands"
57
+ color="rgb(191, 219, 254)"
58
+ variant="primary"
59
+ />
60
+
61
+ <!-- Status indicator based on running state -->
62
+ <StatusIndicator
63
+ color={compute.isRunning ? outputColor : "rgb(245, 158, 11)"}
64
+ type={compute.isRunning ? "pulse" : "dot"}
65
+ />
66
+ {:else}
67
+ <!-- No Session State -->
68
+ <StatusHeader
69
+ icon={ICON["icon-[material-symbols--upload]"].svg}
70
+ text="NO OUTPUT"
71
+ color={outputColor}
72
+ opacity={0.7}
73
+ iconSize={12}
74
+ fontSize={12}
75
+ />
76
+
77
+ <StatusContent
78
+ title={!compute.hasSession ? 'Need Session' : 'Configure'}
79
+ color="rgb(147, 197, 253)"
80
+ variant="secondary"
81
+ />
82
+
83
+ <StatusButton
84
+ text="Setup"
85
+ icon={ICON["icon-[mdi--plus]"].svg}
86
+ color={outputColor}
87
+ backgroundOpacity={0.1}
88
+ textOpacity={0.7}
89
+ />
90
+ {/if}
91
+ </BaseStatusBox>
src/lib/components/3d/elements/compute/status/ComputeStatusBillboard.svelte ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { T } from "@threlte/core";
3
+ import { Billboard, interactivity } from "@threlte/extras";
4
+ import { Root, Container } from "threlte-uikit";
5
+ import ComputeConnectionFlowBoxUIKit from "./ComputeConnectionFlowBoxUIKit.svelte";
6
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
7
+
8
+ interface Props {
9
+ compute: RemoteCompute;
10
+ offset?: number;
11
+ visible?: boolean;
12
+ onVideoInputBoxClick: (compute: RemoteCompute) => void;
13
+ onRobotInputBoxClick: (compute: RemoteCompute) => void;
14
+ onRobotOutputBoxClick: (compute: RemoteCompute) => void;
15
+ }
16
+
17
+ let {
18
+ compute,
19
+ offset = 10,
20
+ visible = true,
21
+ onVideoInputBoxClick,
22
+ onRobotInputBoxClick,
23
+ onRobotOutputBoxClick
24
+ }: Props = $props();
25
+
26
+ interactivity();
27
+ </script>
28
+
29
+ <T.Group
30
+ onclick={(e) => e.stopPropagation()}
31
+ position.z={0.4}
32
+ padding={10}
33
+ rotation={[-Math.PI / 2, 0, 0]}
34
+ scale={[0.1, 0.1, 0.1]}
35
+ pointerEvents="listener"
36
+ {visible}
37
+ >
38
+ <Billboard>
39
+ <Root name={`compute-status-billboard-${compute.id}`}>
40
+ <Container
41
+ width="100%"
42
+ height="100%"
43
+ alignItems="center"
44
+ justifyContent="center"
45
+ padding={20}
46
+ >
47
+ <ComputeConnectionFlowBoxUIKit
48
+ {compute}
49
+ {onVideoInputBoxClick}
50
+ {onRobotInputBoxClick}
51
+ {onRobotOutputBoxClick}
52
+ />
53
+ </Container>
54
+ </Root>
55
+ </Billboard>
56
+ </T.Group>
src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { ICON } from "$lib/utils/icon";
3
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
4
+ import { BaseStatusBox, StatusHeader, StatusContent, StatusIndicator, StatusButton } from "$lib/components/3d/ui";
5
+
6
+ interface Props {
7
+ compute: RemoteCompute;
8
+ handleClick?: () => void;
9
+ }
10
+
11
+ let { compute, handleClick }: Props = $props();
12
+
13
+ // Robot theme color (orange - consistent with robot system)
14
+ const robotColor = "rgb(245, 158, 11)";
15
+
16
+ // Icons
17
+ // const robotIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTIgMTJjMC0yLjIxIDEuNzktNCA0LTRzNCAxLjc5IDQgNC0xLjc5IDQtNCA0LTQtMS43OS00LTR6TTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTAgMThDNy4zIDIwIDMuOCAxNi42IDMuOCAxMlM3LjMgNCA5IDRzNS4yIDMuNCA1LjIgOC02IDgtNSA4eiIvPjwvc3ZnPg==";
18
+ // const robotOffIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMjAuNzMgMTgsNjYgMy4yN0wyIDQuNzNsMTQuMTQgMTQuMTRDMTcuOSAxNS42MiAxNiAxNy40OSAxNiAxOS44VjIxaDJWMTMuNWgzdjIuNWgydjE1LjJDMjIgMTkuNCAyMi44IDE5IDE0LjE4IDEwLjE4TDEwIDZIMTJWNEgxMlY1aDNjMC0yLjc2IDEuMjQtNSA0LTVzNCAxLjI0IDQgNS0yLjI0IDUtNSA1SDEzdjFIOFY3SDZ2Mm0xIDIuNzNMMjAgMjAuNzNsLS4wMSAwIi8+PC9zdmc+";
19
+ // const robotOutlineIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTIgMmMtNS41NyAwLTEwIDQuNDMtMTAgMTBzNC40MyAxMCAxMCAxMCAxMC00LjQzIDEwLTEwUzE3LjU3IDIgMTIgMnptMCAxOGMtNC40MSAwLTgtMy41OS04LThzMy41OS04IDgtOCA4IDMuNTkgOCA4LTMuNTkgOC04IDh6bTAgLTEyYy0yLjIxIDAtNCAuNzktNCA0czEuNzkgNCA0IDQgNC0xLjc5IDQtNC0xLjc5LTQtNC00em0wIDZjLTEuMTEgMC0yLS44OS0yLTJzLjg5LTIgMi0yIDIgLjg5IDIgMi0uODkgMi0yIDJ6Ii8+PC9zdmc+";
20
+ // const formatListNumberedIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMiAxN2gybC0xLTFjLjU1IDAgMS0uNDUgMS0xcy0uNDUtMS0xLTEtMSAuNDUtMSAxaC0yYzAtMS4xLjktMiAyLTJzMiAuOSAyIDItLjkgMi0yIDJ6bS0yIDJoNHYySDJabTUtMTJoMTVoMlY5SDdabTAgNGgxNXYySHdtNS0xMEgyTDZ2MmgxdjJIN3ptMC00aDJsLTEtMXYtMmgxVjFINHYyaDJabTAtMUgxWiIvPjwvc3ZnPg==";
21
+ </script>
22
+
23
+ <!--
24
+ @component
25
+ Compact robot input box showing the status of robot joint states input for AI sessions.
26
+ Displays robot connection information when session exists or connection prompt when disconnected.
27
+ -->
28
+
29
+ <BaseStatusBox
30
+ minWidth={100}
31
+ minHeight={65}
32
+ color={robotColor}
33
+ borderOpacity={0.6}
34
+ backgroundOpacity={0.2}
35
+ opacity={compute.hasSession ? 1 : 0.6}
36
+ onclick={handleClick}
37
+ >
38
+ {#if compute.hasSession && compute.inputConnections}
39
+ <!-- Active Robot Input State -->
40
+ <StatusHeader
41
+ icon={ICON["icon-[ix--robotic-arm]"].svg}
42
+ text="ROBOT"
43
+ color={robotColor}
44
+ opacity={0.9}
45
+ fontSize={11}
46
+ />
47
+
48
+ <StatusContent
49
+ title="Joint States"
50
+ subtitle="6 DOF Robot"
51
+ color="rgb(254, 215, 170)"
52
+ variant="primary"
53
+ />
54
+
55
+ <!-- Connected status -->
56
+ <StatusIndicator color={robotColor} />
57
+ {:else}
58
+ <!-- No Session State -->
59
+ <StatusHeader
60
+ icon={ICON["icon-[ix--robotic-arm]"].svg}
61
+ text="NO ROBOT"
62
+ color={robotColor}
63
+ opacity={0.7}
64
+ fontSize={11}
65
+ />
66
+
67
+ <StatusContent
68
+ title="Setup Robot"
69
+ color="rgb(254, 215, 170)"
70
+ variant="secondary"
71
+ />
72
+
73
+ <StatusButton
74
+ icon={ICON["icon-[mdi--plus]"].svg}
75
+ text="Add"
76
+ color={robotColor}
77
+ backgroundOpacity={0.1}
78
+ textOpacity={0.7}
79
+ />
80
+ {/if}
81
+ </BaseStatusBox>
src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
3
+ import { ICON } from "$lib/utils/icon";
4
+ import { BaseStatusBox, StatusHeader, StatusContent, StatusIndicator, StatusButton } from "$lib/components/3d/ui";
5
+
6
+ interface Props {
7
+ compute: RemoteCompute;
8
+ handleClick?: () => void;
9
+ }
10
+
11
+ let { compute, handleClick }: Props = $props();
12
+
13
+ // Output theme color (blue)
14
+ const outputColor = "rgb(59, 130, 246)";
15
+ </script>
16
+
17
+ <!--
18
+ @component
19
+ Robot output box showing the status of robot joint commands output from AI sessions.
20
+ Displays robot command output information when session exists or connection prompt when disconnected.
21
+ -->
22
+
23
+ <BaseStatusBox
24
+ color={outputColor}
25
+ borderOpacity={0.6}
26
+ backgroundOpacity={0.2}
27
+ opacity={compute.hasSession && compute.isRunning ? 1 : !compute.hasSession ? 0.4 : 0.6}
28
+ onclick={handleClick}
29
+ >
30
+ {#if compute.hasSession && compute.outputConnections}
31
+ <!-- Active Robot Output State -->
32
+ <StatusHeader
33
+ icon={ICON["icon-[ix--robotic-arm]"].svg}
34
+ text="COMMANDS"
35
+ color={outputColor}
36
+ opacity={0.9}
37
+ />
38
+
39
+ <StatusContent
40
+ title={compute.isRunning ? "AI Commands Active" : "Session Ready"}
41
+ subtitle="Motor Control"
42
+ color="rgb(191, 219, 254)"
43
+ variant="primary"
44
+ />
45
+
46
+ <!-- Status indicator based on running state -->
47
+ {#if compute.isRunning}
48
+ <!-- Active pulse indicator -->
49
+ <StatusIndicator color={outputColor} type="pulse" />
50
+ {:else}
51
+ <!-- Ready but not running indicator -->
52
+ <StatusIndicator color="rgb(245, 158, 11)" />
53
+ {/if}
54
+ {:else}
55
+ <!-- No Session State -->
56
+ <StatusHeader
57
+ icon={ICON["icon-[ix--robotic-arm]"].svg}
58
+ text="NO OUTPUT"
59
+ color={outputColor}
60
+ opacity={0.7}
61
+ />
62
+
63
+ <StatusContent
64
+ title={!compute.hasSession ? 'Need Session' : 'Click to Configure'}
65
+ color="rgb(147, 197, 253)"
66
+ variant="secondary"
67
+ />
68
+
69
+ <StatusButton
70
+ icon={ICON["icon-[ix--robotic-arm]"].svg}
71
+ text="Setup Output"
72
+ color={outputColor}
73
+ backgroundOpacity={0.1}
74
+ textOpacity={0.7}
75
+ />
76
+ {/if}
77
+ </BaseStatusBox>
src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Text } from "threlte-uikit";
3
+ import { ICON } from "$lib/utils/icon";
4
+ import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
5
+ import { BaseStatusBox, StatusHeader, StatusContent, StatusIndicator, StatusButton } from "$lib/components/3d/ui";
6
+
7
+ interface Props {
8
+ compute: RemoteCompute;
9
+ handleClick?: () => void;
10
+ }
11
+
12
+ let { compute, handleClick }: Props = $props();
13
+
14
+ // Input theme color (green)
15
+ const inputColor = "rgb(34, 197, 94)";
16
+
17
+ // Icons
18
+ // const videoIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6Ii8+PC9zdmc+";
19
+ // const videoOffIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMy4yNyAyTDIgMy4yN2wxLjY4IDEuNjhDMy4yNiA1LjMgMyA1LjY0IDMgNnYxMmMwIC41NS40NSAxIDEgMWg4Yy4zNiAwIC42OC0uMTUuOTItLjM5bDEuNzMgMS43M0wxNiAyMC43M0wxOC43MyAxOGwtLjg5LS44OUwyMCAxNXYtMWwtNC05VjVjMC0uNTUtLjQ1LTEtMS0xSDlsLTEuMTYtMS4xNkMxMS41NSAyIDEwIDQuMjcgMTAgNHYxTDMuMjcgMnoiLz48L3N2Zz4=";
20
+ // const videoPlusIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTMgMTRoMXYzaDN2MWgtM3YzaC0xdi0zaC0zdi0xaDN2LTN6bTUgMGwtNS0zdjZsNS0zek0xMSAzSDNDMS45IDMgMSAzLjkgMSA1djEwYzAgMS4xLjkgMiAyIDJoOGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yeiIvPjwvc3ZnPg==";
21
+ // const cameraMultipleIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNCAyaDZsMS41IDEuNUg2djJoOGwtLjUtLjVIMThsMS41IDEuNWgzYzEuMSAwIDIgLjkgMiAydjEwYzAgMS4xLS45IDItMiAySDZjLTEuMSAwLTItLjktMi0yVjRjMC0xLjEuOS0yIDItMnptNSA2Yy0xLjExIDAtMi4wOC45LTIuMDggMnMuOTcgMiAyLjA4IDIgMi4wOC0uOSAyLjA4LTItLjk3LTItMi4wOC0yeiIvPjwvc3ZnPg==";
22
+ </script>
23
+
24
+ <!--
25
+ @component
26
+ Compact video input box showing the status of camera video streams for AI sessions.
27
+ Displays video connection information when session exists or connection prompt when disconnected.
28
+ -->
29
+
30
+ <BaseStatusBox
31
+ minWidth={100}
32
+ minHeight={65}
33
+ color={inputColor}
34
+ borderOpacity={0.6}
35
+ backgroundOpacity={0.2}
36
+ opacity={compute.hasSession ? 1 : 0.6}
37
+ onclick={handleClick}
38
+ >
39
+ {#if compute.hasSession && compute.inputConnections}
40
+ <!-- Active Video Input State -->
41
+ <StatusHeader
42
+ icon={ICON["icon-[mdi--video]"].svg}
43
+ text="VIDEO"
44
+ color={inputColor}
45
+ opacity={0.9}
46
+ fontSize={11}
47
+ />
48
+
49
+ <!-- Camera Streams -->
50
+ <StatusContent
51
+ title={`${Object.keys(compute.inputConnections.cameras).length} Cameras`}
52
+ color="rgb(187, 247, 208)"
53
+ variant="primary"
54
+ />
55
+
56
+ <!-- Connected status -->
57
+ <StatusIndicator color={inputColor} />
58
+ {:else}
59
+ <!-- No Session State -->
60
+ <StatusHeader
61
+ icon={ICON["icon-[mdi--video-off]"].svg}
62
+ text="NO VIDEO"
63
+ color={inputColor}
64
+ opacity={0.7}
65
+ fontSize={11}
66
+ />
67
+
68
+ <StatusContent
69
+ title="Setup Video"
70
+ color="rgb(134, 239, 172)"
71
+ variant="secondary"
72
+ />
73
+
74
+ <StatusButton
75
+ icon={ICON["icon-[mdi--plus]"].svg}
76
+ text="Add"
77
+ color={inputColor}
78
+ backgroundOpacity={0.1}
79
+ textOpacity={0.7}
80
+ />
81
+ {/if}
82
+ </BaseStatusBox>
src/lib/components/3d/elements/robot/RobotGridItem.svelte ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { T } from "@threlte/core";
3
+ import { Group } from "three";
4
+ import { getRootLinks } from "@/components/3d/elements/robot/URDF/utils/UrdfParser";
5
+ import UrdfLink from "@/components/3d/elements/robot/URDF/primitives/UrdfLink.svelte";
6
+ import RobotStatusBillboard from "@/components/3d/elements/robot/status/RobotStatusBillboard.svelte";
7
+ import type { Robot } from "$lib/elements/robot/Robot.svelte.js";
8
+ import { createUrdfRobot } from "$lib/elements/robot/createRobot.svelte";
9
+ import { interactivity, type IntersectionEvent, useCursor } from "@threlte/extras";
10
+ import type { RobotUrdfConfig } from "$lib/types/urdf";
11
+ import { onMount } from 'svelte';
12
+ import type IUrdfRobot from '@/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.js';
13
+ import { ROBOT_CONFIG } from '$lib/elements/robot/config.js';
14
+
15
+ interface Props {
16
+ robot: Robot;
17
+ onCameraMove: (ref: any) => void;
18
+ onInputBoxClick: (robot: Robot) => void;
19
+ onRobotBoxClick: (robot: Robot) => void;
20
+ onOutputBoxClick: (robot: Robot) => void;
21
+ }
22
+
23
+ let ref = $state<Group | undefined>(undefined);
24
+
25
+ let { robot = $bindable(), onCameraMove, onInputBoxClick, onRobotBoxClick, onOutputBoxClick }: Props = $props();
26
+
27
+ let urdfRobotState = $state<IUrdfRobot | null>(null);
28
+ let lastJointValues = $state<Record<string, number>>({});
29
+
30
+ onMount(async () => {
31
+ const urdfConfig: RobotUrdfConfig = {
32
+ urdfUrl: "/robots/so-100/so_arm100.urdf"
33
+ };
34
+
35
+ try {
36
+ const UrdfRobotState = await createUrdfRobot(urdfConfig);
37
+ urdfRobotState = UrdfRobotState.urdfRobot;
38
+ } catch (error) {
39
+ console.error('Failed to load URDF robot:', error);
40
+ }
41
+ });
42
+
43
+ // Sync joint values from Robot to URDF joints with optimized updates
44
+ $effect(() => {
45
+ if (!urdfRobotState) return;
46
+ if (robot.jointArray.length === 0) return;
47
+
48
+ // Check if this is the initial sync (no previous values recorded)
49
+ const isInitialSync = Object.keys(lastJointValues).length === 0;
50
+
51
+ // Check if any joint values have actually changed (using config threshold)
52
+ const threshold = isInitialSync ? 0 : ROBOT_CONFIG.performance.uiUpdateThreshold;
53
+ const hasSignificantChanges = isInitialSync || robot.jointArray.some(joint =>
54
+ Math.abs((lastJointValues[joint.name] || 0) - joint.value) > threshold
55
+ );
56
+ if (!hasSignificantChanges) return;
57
+
58
+ // Batch update all joints that have changed (or all joints on initial sync)
59
+ let updatedCount = 0;
60
+ robot.jointArray.forEach(joint => {
61
+ if (isInitialSync || Math.abs((lastJointValues[joint.name] || 0) - joint.value) > threshold) {
62
+ lastJointValues[joint.name] = joint.value;
63
+ const urdfJoint = findUrdfJoint(urdfRobotState, joint.name);
64
+ if (urdfJoint) {
65
+ // Initialize rotation array if it doesn't exist
66
+ if (!urdfJoint.rotation) {
67
+ urdfJoint.rotation = [0, 0, 0];
68
+ }
69
+
70
+ // Use the Robot's conversion method for proper coordinate mapping
71
+ const radians = robot.convertNormalizedToUrdfRadians(joint.name, joint.value);
72
+ const axis = urdfJoint.axis_xyz || [0, 0, 1];
73
+
74
+ // Reset rotation and apply to the appropriate axis
75
+ urdfJoint.rotation = [0, 0, 0];
76
+ for (let i = 0; i < 3; i++) {
77
+ if (Math.abs(axis[i]) > 0.001) {
78
+ urdfJoint.rotation[i] = radians * axis[i];
79
+ }
80
+ }
81
+ updatedCount++;
82
+ }
83
+ }
84
+ });
85
+ });
86
+
87
+ function findUrdfJoint(robot: Robot, jointName: string): any {
88
+ // Search through the robot's joints array
89
+ if (robot.joints && Array.isArray(robot.joints)) {
90
+ for (const joint of robot.joints) {
91
+ if (joint.name === jointName) {
92
+ return joint;
93
+ }
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+
99
+ const { onPointerEnter, onPointerLeave, hovering } = useCursor();
100
+ interactivity();
101
+
102
+ let isToggled = $state(false);
103
+
104
+ function handleClick(event: IntersectionEvent<MouseEvent>) {
105
+ event.stopPropagation();
106
+ isToggled = !isToggled;
107
+ }
108
+
109
+ </script>
110
+
111
+ <T.Group
112
+ bind:ref
113
+ position.x={robot.position.x}
114
+ position.y={robot.position.y}
115
+ position.z={robot.position.z}
116
+ scale={[10, 10, 10]}
117
+ rotation={[-Math.PI / 2, 0, 0]}
118
+ >
119
+ <T.Group
120
+ onpointerenter={onPointerEnter}
121
+ onpointerleave={onPointerLeave}
122
+ onclick={handleClick}
123
+ >
124
+ {#if urdfRobotState}
125
+ {#each getRootLinks(urdfRobotState) as link}
126
+ <UrdfLink
127
+ robot={urdfRobotState}
128
+ {link}
129
+ textScale={0.2}
130
+ showName={$hovering || isToggled}
131
+ showVisual={true}
132
+ showCollision={false}
133
+ visualColor={robot.connectionStatus.isConnected ? "#10b981" : "#6b7280"}
134
+ visualOpacity={$hovering || isToggled ? 0.4 : 1.0}
135
+ collisionOpacity={1.0}
136
+ collisionColor="#813d9c"
137
+ jointNames={$hovering}
138
+ joints={$hovering}
139
+ jointColor="#62a0ea"
140
+ jointIndicatorColor="#f66151"
141
+ nameHeight={0.1}
142
+ showLine={$hovering || isToggled}
143
+ opacity={1}
144
+ isInteractive={false}
145
+ />
146
+ {/each}
147
+ {:else}
148
+ <!-- Fallback simple representation while URDF loads -->
149
+ <T.Mesh>
150
+ <T.BoxGeometry args={[0.1, 0.1, 0.1]} />
151
+ <T.MeshStandardMaterial
152
+ color={robot.connectionStatus.isConnected ? "#10b981" : "#6b7280"}
153
+ opacity={$hovering ? 0.8 : 1.0}
154
+ transparent
155
+ />
156
+ </T.Mesh>
157
+ {/if}
158
+ </T.Group>
159
+
160
+
161
+ <RobotStatusBillboard
162
+ {robot}
163
+ onInputBoxClick={onInputBoxClick}
164
+ onRobotBoxClick={onRobotBoxClick}
165
+ onOutputBoxClick={onOutputBoxClick}
166
+ visible={isToggled}
167
+ />
168
+
169
+ </T.Group>
src/lib/components/3d/elements/robot/Robots.svelte ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { robotManager } from "$lib/elements/robot/RobotManager.svelte.js";
3
+ import { onMount, onDestroy } from "svelte";
4
+ import InputConnectionModal from "@/components/3d/elements/robot/modal/InputConnectionModal.svelte";
5
+ import OutputConnectionModal from "@/components/3d/elements/robot/modal/OutputConnectionModal.svelte";
6
+ import ManualControlSheet from "@/components/3d/elements/robot/modal/ManualControlSheet.svelte";
7
+ import type { Robot } from "$lib/elements/robot/Robot.svelte.js";
8
+ import { generateName } from "$lib/utils/generateName";
9
+ import RobotGridItem from "@/components/3d/elements/robot/RobotGridItem.svelte";
10
+
11
+ interface Props {
12
+ workspaceId: string;
13
+ }
14
+ let {workspaceId}: Props = $props();
15
+
16
+ let isInputModalOpen = $state(false);
17
+ let isOutputModalOpen = $state(false);
18
+ let isManualControlSheetOpen = $state(false);
19
+ let selectedRobot = $state<Robot | null>(null);
20
+
21
+ function onInputBoxClick(robot: Robot) {
22
+ selectedRobot = robot;
23
+ isInputModalOpen = true;
24
+ }
25
+
26
+ function onRobotBoxClick(robot: Robot) {
27
+ selectedRobot = robot;
28
+ isManualControlSheetOpen = true;
29
+ }
30
+
31
+ function onOutputBoxClick(robot: Robot) {
32
+ selectedRobot = robot;
33
+ isOutputModalOpen = true;
34
+ }
35
+
36
+ onMount(async () => {
37
+ async function createRobot() {
38
+ try {
39
+ const robotId = generateName();
40
+ await robotManager.createSO100Robot(robotId, {
41
+ x: 0,
42
+ y: 0,
43
+ z: 0
44
+ });
45
+ } catch (error) {
46
+ console.error('Failed to create robot:', error);
47
+ }
48
+ }
49
+
50
+ if (robotManager.robots.length === 0) {
51
+ await createRobot();
52
+ }
53
+ });
54
+
55
+ onDestroy(() => {
56
+ // Clean up robots and unlock servos for safety
57
+ console.log('🧹 Cleaning up robots and unlocking servos...');
58
+ robotManager.destroy().then(() => {
59
+ console.log('✅ Cleanup completed successfully');
60
+ }).catch((error) => {
61
+ console.error('❌ Error during cleanup:', error);
62
+ });
63
+ });
64
+ </script>
65
+
66
+ {#each robotManager.robots as robot (robot.id)}
67
+ <RobotGridItem
68
+ {robot}
69
+ onCameraMove={() => {}}
70
+ onInputBoxClick={onInputBoxClick}
71
+ onRobotBoxClick={onRobotBoxClick}
72
+ onOutputBoxClick={onOutputBoxClick}
73
+ />
74
+ {/each}
75
+
76
+ <!-- Connection Modals -->
77
+ {#if selectedRobot}
78
+ <InputConnectionModal bind:open={isInputModalOpen} robot={selectedRobot} {workspaceId} />
79
+ <OutputConnectionModal bind:open={isOutputModalOpen} robot={selectedRobot} {workspaceId} />
80
+ <ManualControlSheet bind:open={isManualControlSheetOpen} robot={selectedRobot} {workspaceId} />
81
+ {/if}
src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfBox.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export default interface IUrdfBox {
2
+ size: [x: number, y: number, z: number];
3
+ }
src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfCylinder.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export default interface IUrdfCylinder {
2
+ radius: number;
3
+ length: number;
4
+ }