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 |
+
}
|