Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	Commit 
							
							·
						
						6ce4ca6
	
0
								Parent(s):
							
							
Update
Browse filesThis view is limited to 50 files because it contains too many changes.  
							See raw diff
- .dockerignore +56 -0
 - .gitattributes +4 -0
 - .gitignore +23 -0
 - .gitmodules +6 -0
 - .npmrc +1 -0
 - .prettierignore +12 -0
 - .prettierrc +16 -0
 - Dockerfile +48 -0
 - README.md +297 -0
 - bun.lock +0 -0
 - components.json +16 -0
 - eslint.config.js +36 -0
 - external/.gitkeep +0 -0
 - external/RobotHub-InferenceServer +1 -0
 - external/RobotHub-TransportServer +1 -0
 - log.txt +1 -0
 - package.json +63 -0
 - packages/feetech.js/README.md +24 -0
 - packages/feetech.js/debug.mjs +15 -0
 - packages/feetech.js/index.d.ts +50 -0
 - packages/feetech.js/index.mjs +65 -0
 - packages/feetech.js/lowLevelSDK.mjs +1235 -0
 - packages/feetech.js/package.json +38 -0
 - packages/feetech.js/scsServoSDK.mjs +1205 -0
 - packages/feetech.js/scsservo_constants.mjs +53 -0
 - packages/feetech.js/test.html +770 -0
 - src/app.css +122 -0
 - src/app.d.ts +20 -0
 - src/app.html +12 -0
 - src/lib/components/3d/Floor.svelte +24 -0
 - src/lib/components/3d/elements/compute/ComputeGridItem.svelte +51 -0
 - src/lib/components/3d/elements/compute/Computes.svelte +87 -0
 - src/lib/components/3d/elements/compute/GPU.svelte +138 -0
 - src/lib/components/3d/elements/compute/GPUModel.svelte +200 -0
 - src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte +382 -0
 - src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte +291 -0
 - src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte +288 -0
 - src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte +276 -0
 - src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte +48 -0
 - src/lib/components/3d/elements/compute/status/ComputeConnectionFlowBoxUIKit.svelte +56 -0
 - src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte +84 -0
 - src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte +91 -0
 - src/lib/components/3d/elements/compute/status/ComputeStatusBillboard.svelte +56 -0
 - src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte +81 -0
 - src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte +77 -0
 - src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte +82 -0
 - src/lib/components/3d/elements/robot/RobotGridItem.svelte +169 -0
 - src/lib/components/3d/elements/robot/Robots.svelte +81 -0
 - src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfBox.ts +3 -0
 - 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 | 
         
            +
            [](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/[email protected] /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 | 
         
            +
            }
         
     |