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/gltf@3.0.1 /Users/julienblanchon/Downloads/gpu/scene.gltf --output ./src/lib/components/3d/elements/gpu/ --types
|
4 |
+
Author: Cem Gürbüz (https://sketchfab.com/cemgurbuzz)
|
5 |
+
License: CC-BY-NC-4.0 (http://creativecommons.org/licenses/by-nc/4.0/)
|
6 |
+
Source: https://sketchfab.com/3d-models/nvidia-geforce-rtx-3090-9b7cd73fefd5435f99f891567f5a9c2e
|
7 |
+
Title: Nvidia GeForce RTX 3090
|
8 |
+
-->
|
9 |
+
|
10 |
+
<script lang="ts">
|
11 |
+
import type * as THREE from 'three'
|
12 |
+
|
13 |
+
import type { Snippet } from 'svelte'
|
14 |
+
import { T, type Props } from '@threlte/core'
|
15 |
+
import { useGltf } from '@threlte/extras'
|
16 |
+
|
17 |
+
let {
|
18 |
+
fan_rotation = 0,
|
19 |
+
fallback,
|
20 |
+
error,
|
21 |
+
children,
|
22 |
+
ref = $bindable(),
|
23 |
+
...props
|
24 |
+
}: Props<THREE.Group<THREE.Object3DEventMap>> & {
|
25 |
+
fan_rotation?: number
|
26 |
+
ref?: THREE.Group<THREE.Object3DEventMap> | undefined
|
27 |
+
children?: Snippet<[{ ref: THREE.Group<THREE.Object3DEventMap> | undefined }]>
|
28 |
+
fallback?: Snippet
|
29 |
+
error?: Snippet<[{ error: Error }]>
|
30 |
+
} = $props()
|
31 |
+
|
32 |
+
type GLTFResult = {
|
33 |
+
nodes: {
|
34 |
+
Metal_Frame_Metal_0: THREE.Mesh
|
35 |
+
Front_Cover_Black_0: THREE.Mesh
|
36 |
+
Fan_Circle_Black_Fan_0: THREE.Mesh
|
37 |
+
Fan_F_Black_Fan_0: THREE.Mesh
|
38 |
+
Fan_F_Slot1_0: THREE.Mesh
|
39 |
+
Front_Cover_U_Black_0: THREE.Mesh
|
40 |
+
Front_Cover_T_Black_0: THREE.Mesh
|
41 |
+
Fan_Circle_B_Black_Fan_0: THREE.Mesh
|
42 |
+
Grills_U_Metal_Black_0: THREE.Mesh
|
43 |
+
Grills_T_Metal_Black_0: THREE.Mesh
|
44 |
+
Plane010_Black001_0: THREE.Mesh
|
45 |
+
Socket_Slot_0: THREE.Mesh
|
46 |
+
Side_Metal_Part_Metal_S_0: THREE.Mesh
|
47 |
+
Grills_F003_Metal_Black_0: THREE.Mesh
|
48 |
+
Grills_F002_Metal_Black_0: THREE.Mesh
|
49 |
+
Fan_B_Black_Fan_0: THREE.Mesh
|
50 |
+
Fan_B_Slot1_0: THREE.Mesh
|
51 |
+
}
|
52 |
+
materials: {
|
53 |
+
Metal: THREE.MeshStandardMaterial
|
54 |
+
Black: THREE.MeshStandardMaterial
|
55 |
+
Black_Fan: THREE.MeshStandardMaterial
|
56 |
+
['Slot.1']: THREE.MeshStandardMaterial
|
57 |
+
Metal_Black: THREE.MeshStandardMaterial
|
58 |
+
['Black.001']: THREE.MeshStandardMaterial
|
59 |
+
Slot: THREE.MeshStandardMaterial
|
60 |
+
Metal_S: THREE.MeshStandardMaterial
|
61 |
+
}
|
62 |
+
}
|
63 |
+
|
64 |
+
const gltf = useGltf<GLTFResult>('/gpu/scene.gltf')
|
65 |
+
</script>
|
66 |
+
|
67 |
+
<T.Group
|
68 |
+
bind:ref
|
69 |
+
dispose={false}
|
70 |
+
{...props as any}
|
71 |
+
>
|
72 |
+
{#await gltf}
|
73 |
+
{@render fallback?.()}
|
74 |
+
{:then gltf}
|
75 |
+
<T.Group scale={0.01}>
|
76 |
+
<T.Group
|
77 |
+
position={[127.5, 88.51, 10.29]}
|
78 |
+
rotation={[Math.PI / 2, 0.05, 0]}
|
79 |
+
scale={0.3}
|
80 |
+
>
|
81 |
+
<T.Mesh
|
82 |
+
geometry={gltf.nodes.Fan_F_Black_Fan_0.geometry}
|
83 |
+
material={gltf.materials.Black_Fan}
|
84 |
+
rotation={[0, fan_rotation, 0]}
|
85 |
+
/>
|
86 |
+
<T.Mesh
|
87 |
+
geometry={gltf.nodes.Fan_F_Slot1_0.geometry}
|
88 |
+
material={gltf.materials['Slot.1']}
|
89 |
+
/>
|
90 |
+
</T.Group>
|
91 |
+
<T.Group
|
92 |
+
position={[-123.9, 88.51, -37.82]}
|
93 |
+
rotation={[Math.PI / 2, -0.05, Math.PI]}
|
94 |
+
scale={0.3}
|
95 |
+
>
|
96 |
+
<T.Mesh
|
97 |
+
geometry={gltf.nodes.Fan_B_Black_Fan_0.geometry}
|
98 |
+
material={gltf.materials.Black_Fan}
|
99 |
+
rotation={[0, fan_rotation, 0]}
|
100 |
+
/>
|
101 |
+
<T.Mesh
|
102 |
+
geometry={gltf.nodes.Fan_B_Slot1_0.geometry}
|
103 |
+
material={gltf.materials['Slot.1']}
|
104 |
+
|
105 |
+
/>
|
106 |
+
</T.Group>
|
107 |
+
<T.Mesh
|
108 |
+
geometry={gltf.nodes.Metal_Frame_Metal_0.geometry}
|
109 |
+
material={gltf.materials.Metal}
|
110 |
+
position={[0, 88.3, -8.47]}
|
111 |
+
rotation={[Math.PI / 2, 0, 0]}
|
112 |
+
/>
|
113 |
+
<T.Mesh
|
114 |
+
geometry={gltf.nodes.Front_Cover_Black_0.geometry}
|
115 |
+
material={gltf.materials.Black}
|
116 |
+
position={[-122.3, 89.69, 12.11]}
|
117 |
+
rotation={[Math.PI / 2, 0, 0]}
|
118 |
+
scale={[1, 1, 0.84]}
|
119 |
+
/>
|
120 |
+
<T.Mesh
|
121 |
+
geometry={gltf.nodes.Fan_Circle_Black_Fan_0.geometry}
|
122 |
+
material={gltf.materials.Black_Fan}
|
123 |
+
position={[127.5, 88.51, 10.29]}
|
124 |
+
rotation={[Math.PI / 2, 0, 0]}
|
125 |
+
scale={0.79}
|
126 |
+
/>
|
127 |
+
<T.Mesh
|
128 |
+
geometry={gltf.nodes.Front_Cover_U_Black_0.geometry}
|
129 |
+
material={gltf.materials.Black}
|
130 |
+
position={[0.02, 26.08, 14.09]}
|
131 |
+
rotation={[Math.PI / 2, 0, 0]}
|
132 |
+
/>
|
133 |
+
<T.Mesh
|
134 |
+
geometry={gltf.nodes.Front_Cover_T_Black_0.geometry}
|
135 |
+
material={gltf.materials.Black}
|
136 |
+
position={[-4.75, 163.4, 14.09]}
|
137 |
+
rotation={[-Math.PI / 2 , 0, -Math.PI]}
|
138 |
+
/>
|
139 |
+
<T.Mesh
|
140 |
+
geometry={gltf.nodes.Fan_Circle_B_Black_Fan_0.geometry}
|
141 |
+
material={gltf.materials.Black_Fan}
|
142 |
+
position={[-124.15, 88.51, -40.18]}
|
143 |
+
rotation={[Math.PI / 2, 0, Math.PI]}
|
144 |
+
scale={0.79}
|
145 |
+
/>
|
146 |
+
<T.Mesh
|
147 |
+
geometry={gltf.nodes.Grills_U_Metal_Black_0.geometry}
|
148 |
+
material={gltf.materials.Metal_Black}
|
149 |
+
position={[-0.12, 3.16, 3.09]}
|
150 |
+
rotation={[Math.PI / 2, -Math.PI / 4, 0]}
|
151 |
+
scale={[0.55, 11.75, 0.55]}
|
152 |
+
/>
|
153 |
+
<T.Mesh
|
154 |
+
geometry={gltf.nodes.Grills_T_Metal_Black_0.geometry}
|
155 |
+
material={gltf.materials.Metal_Black}
|
156 |
+
position={[0.8, 174.49, 3.09]}
|
157 |
+
rotation={[-Math.PI / 2, Math.PI / 4, -Math.PI]}
|
158 |
+
scale={[0.55, 11.75, 0.55]}
|
159 |
+
/>
|
160 |
+
<T.Mesh
|
161 |
+
geometry={gltf.nodes.Plane010_Black001_0.geometry}
|
162 |
+
material={gltf.materials['Black.001']}
|
163 |
+
position={[121.84, 88.42, -34.24]}
|
164 |
+
rotation={[-Math.PI / 2, 0, -Math.PI]}
|
165 |
+
scale={[1, 1, 0.84]}
|
166 |
+
/>
|
167 |
+
<T.Mesh
|
168 |
+
geometry={gltf.nodes.Socket_Slot_0.geometry}
|
169 |
+
material={gltf.materials.Slot}
|
170 |
+
position={[-149.71, 187.47, -39.01]}
|
171 |
+
rotation={[Math.PI / 2, 0, 0]}
|
172 |
+
scale={[1, 1.93, 1]}
|
173 |
+
/>
|
174 |
+
<T.Mesh
|
175 |
+
geometry={gltf.nodes.Side_Metal_Part_Metal_S_0.geometry}
|
176 |
+
material={gltf.materials.Metal_S}
|
177 |
+
position={[-225.87, 118.09, -12.54]}
|
178 |
+
rotation={[Math.PI / 2, 0, 0]}
|
179 |
+
/>
|
180 |
+
<T.Mesh
|
181 |
+
geometry={gltf.nodes.Grills_F003_Metal_Black_0.geometry}
|
182 |
+
material={gltf.materials.Metal_Black}
|
183 |
+
position={[131.49, 88.84, -23.02]}
|
184 |
+
rotation={[Math.PI / 2, 0, 0]}
|
185 |
+
scale={[1, 1, 1.02]}
|
186 |
+
/>
|
187 |
+
<T.Mesh
|
188 |
+
geometry={gltf.nodes.Grills_F002_Metal_Black_0.geometry}
|
189 |
+
material={gltf.materials.Metal_Black}
|
190 |
+
position={[-128.18, 88.84, -4.17]}
|
191 |
+
rotation={[Math.PI / 2, 0, Math.PI]}
|
192 |
+
scale={[1, 0.97, 1.02]}
|
193 |
+
/>
|
194 |
+
</T.Group>
|
195 |
+
{:catch err}
|
196 |
+
{@render error?.({ error: err })}
|
197 |
+
{/await}
|
198 |
+
|
199 |
+
{@render children?.({ ref })}
|
200 |
+
</T.Group>
|
src/lib/components/3d/elements/compute/modal/AISessionConnectionModal.svelte
ADDED
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import * as Dialog from "@/components/ui/dialog";
|
3 |
+
import { Button } from "@/components/ui/button";
|
4 |
+
import * as Card from "@/components/ui/card";
|
5 |
+
import { Badge } from "@/components/ui/badge";
|
6 |
+
import { Input } from "@/components/ui/input";
|
7 |
+
import { Label } from "@/components/ui/label";
|
8 |
+
import * as Alert from "@/components/ui/alert";
|
9 |
+
import { remoteComputeManager } from "$lib/elements/compute//RemoteComputeManager.svelte";
|
10 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
11 |
+
import type { AISessionConfig } from "$lib/elements/compute//RemoteComputeManager.svelte";
|
12 |
+
import { settings } from "$lib/runes/settings.svelte";
|
13 |
+
import { toast } from "svelte-sonner";
|
14 |
+
|
15 |
+
interface Props {
|
16 |
+
workspaceId: string;
|
17 |
+
open: boolean;
|
18 |
+
compute: RemoteCompute;
|
19 |
+
}
|
20 |
+
|
21 |
+
let { open = $bindable(), compute, workspaceId }: Props = $props();
|
22 |
+
|
23 |
+
let isConnecting = $state(false);
|
24 |
+
let sessionId = $state('');
|
25 |
+
let policyPath = $state('./checkpoints/act_so101_beyond');
|
26 |
+
let cameraNames = $state('front');
|
27 |
+
let useProvidedWorkspace = $state(false);
|
28 |
+
|
29 |
+
// Auto-generate session ID when modal opens
|
30 |
+
$effect(() => {
|
31 |
+
if (open && compute && !sessionId) {
|
32 |
+
sessionId = `${compute.id}-session-${Date.now()}`;
|
33 |
+
}
|
34 |
+
});
|
35 |
+
|
36 |
+
async function handleCreateSession() {
|
37 |
+
if (!compute) return;
|
38 |
+
|
39 |
+
if (!sessionId.trim() || !policyPath.trim()) {
|
40 |
+
toast.error('Please fill in all required fields');
|
41 |
+
return;
|
42 |
+
}
|
43 |
+
|
44 |
+
isConnecting = true;
|
45 |
+
try {
|
46 |
+
const cameras = cameraNames.split(',').map(name => name.trim()).filter(name => name);
|
47 |
+
if (cameras.length === 0) {
|
48 |
+
cameras.push('front');
|
49 |
+
}
|
50 |
+
|
51 |
+
const config: AISessionConfig = {
|
52 |
+
sessionId: sessionId.trim(),
|
53 |
+
policyPath: policyPath.trim(),
|
54 |
+
cameraNames: cameras,
|
55 |
+
transportServerUrl: settings.transportServerUrl,
|
56 |
+
workspaceId: useProvidedWorkspace ? workspaceId : undefined
|
57 |
+
};
|
58 |
+
|
59 |
+
const result = await remoteComputeManager.createSession(compute.id, config);
|
60 |
+
if (result.success) {
|
61 |
+
toast.success(`AI session created: ${sessionId}`);
|
62 |
+
open = false;
|
63 |
+
} else {
|
64 |
+
toast.error(`Failed to create session: ${result.error}`);
|
65 |
+
}
|
66 |
+
} catch (error) {
|
67 |
+
console.error('Session creation error:', error);
|
68 |
+
toast.error('Failed to create session');
|
69 |
+
} finally {
|
70 |
+
isConnecting = false;
|
71 |
+
}
|
72 |
+
}
|
73 |
+
|
74 |
+
async function handleStartSession() {
|
75 |
+
if (!compute) return;
|
76 |
+
|
77 |
+
isConnecting = true;
|
78 |
+
try {
|
79 |
+
const result = await remoteComputeManager.startSession(compute.id);
|
80 |
+
if (result.success) {
|
81 |
+
toast.success('AI session started');
|
82 |
+
} else {
|
83 |
+
toast.error(`Failed to start session: ${result.error}`);
|
84 |
+
}
|
85 |
+
} catch (error) {
|
86 |
+
console.error('Session start error:', error);
|
87 |
+
toast.error('Failed to start session');
|
88 |
+
} finally {
|
89 |
+
isConnecting = false;
|
90 |
+
}
|
91 |
+
}
|
92 |
+
|
93 |
+
async function handleStopSession() {
|
94 |
+
if (!compute) return;
|
95 |
+
|
96 |
+
isConnecting = true;
|
97 |
+
try {
|
98 |
+
const result = await remoteComputeManager.stopSession(compute.id);
|
99 |
+
if (result.success) {
|
100 |
+
toast.success('AI session stopped');
|
101 |
+
} else {
|
102 |
+
toast.error(`Failed to stop session: ${result.error}`);
|
103 |
+
}
|
104 |
+
} catch (error) {
|
105 |
+
console.error('Session stop error:', error);
|
106 |
+
toast.error('Failed to stop session');
|
107 |
+
} finally {
|
108 |
+
isConnecting = false;
|
109 |
+
}
|
110 |
+
}
|
111 |
+
|
112 |
+
async function handleDeleteSession() {
|
113 |
+
if (!compute) return;
|
114 |
+
|
115 |
+
isConnecting = true;
|
116 |
+
try {
|
117 |
+
const result = await remoteComputeManager.deleteSession(compute.id);
|
118 |
+
if (result.success) {
|
119 |
+
toast.success('AI session deleted');
|
120 |
+
} else {
|
121 |
+
toast.error(`Failed to delete session: ${result.error}`);
|
122 |
+
}
|
123 |
+
} catch (error) {
|
124 |
+
console.error('Session delete error:', error);
|
125 |
+
toast.error('Failed to delete session');
|
126 |
+
} finally {
|
127 |
+
isConnecting = false;
|
128 |
+
}
|
129 |
+
}
|
130 |
+
</script>
|
131 |
+
|
132 |
+
<Dialog.Root bind:open>
|
133 |
+
<Dialog.Content
|
134 |
+
class="max-h-[80vh] max-w-2xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
|
135 |
+
>
|
136 |
+
<Dialog.Header class="pb-3">
|
137 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
138 |
+
<span class="icon-[mdi--robot-outline] size-5 text-purple-400"></span>
|
139 |
+
AI Compute Session - {compute.name || 'No Compute Selected'}
|
140 |
+
</Dialog.Title>
|
141 |
+
<Dialog.Description class="text-sm text-slate-400">
|
142 |
+
Configure and manage ACT model inference sessions for robot control
|
143 |
+
</Dialog.Description>
|
144 |
+
</Dialog.Header>
|
145 |
+
|
146 |
+
<div class="space-y-4">
|
147 |
+
<!-- Current Session Status -->
|
148 |
+
<div
|
149 |
+
class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
|
150 |
+
>
|
151 |
+
<div class="flex items-center gap-2">
|
152 |
+
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
153 |
+
<span class="text-sm font-medium text-purple-300">Session Status</span>
|
154 |
+
</div>
|
155 |
+
{#if compute.hasSession}
|
156 |
+
<Badge variant="default" class="bg-purple-600 text-xs">
|
157 |
+
{compute.statusInfo.statusText}
|
158 |
+
</Badge>
|
159 |
+
{:else}
|
160 |
+
<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
|
161 |
+
{/if}
|
162 |
+
</div>
|
163 |
+
|
164 |
+
<!-- Current Session Details -->
|
165 |
+
{#if compute.hasSession && compute.sessionData}
|
166 |
+
<Card.Root class="border-purple-500/30 bg-purple-500/5">
|
167 |
+
<Card.Header>
|
168 |
+
<Card.Title class="flex items-center gap-2 text-base text-purple-200">
|
169 |
+
<span class="icon-[mdi--cog] size-4"></span>
|
170 |
+
Current Session
|
171 |
+
</Card.Title>
|
172 |
+
</Card.Header>
|
173 |
+
<Card.Content>
|
174 |
+
<div class="space-y-3">
|
175 |
+
<div class="rounded-lg border border-purple-500/30 bg-purple-900/20 p-3">
|
176 |
+
<div class="grid grid-cols-2 gap-2 text-xs">
|
177 |
+
<div>
|
178 |
+
<span class="text-purple-300 font-medium">Session ID:</span>
|
179 |
+
<span class="text-purple-100 block">{compute.sessionId}</span>
|
180 |
+
</div>
|
181 |
+
<div>
|
182 |
+
<span class="text-purple-300 font-medium">Status:</span>
|
183 |
+
<span class="text-purple-100 block">{compute.statusInfo.emoji} {compute.statusInfo.statusText}</span>
|
184 |
+
</div>
|
185 |
+
<div>
|
186 |
+
<span class="text-purple-300 font-medium">Policy:</span>
|
187 |
+
<span class="text-purple-100 block">{compute.sessionConfig?.policyPath}</span>
|
188 |
+
</div>
|
189 |
+
<div>
|
190 |
+
<span class="text-purple-300 font-medium">Cameras:</span>
|
191 |
+
<span class="text-purple-100 block">{compute.sessionConfig?.cameraNames.join(', ')}</span>
|
192 |
+
</div>
|
193 |
+
</div>
|
194 |
+
</div>
|
195 |
+
|
196 |
+
<!-- Connection Details -->
|
197 |
+
<div class="rounded-lg border border-green-500/30 bg-green-900/20 p-3">
|
198 |
+
<div class="text-sm font-medium text-green-300 mb-2">📡 Inference Server Connections</div>
|
199 |
+
<div class="space-y-1 text-xs">
|
200 |
+
<div>
|
201 |
+
<span class="text-green-400">Workspace:</span>
|
202 |
+
<span class="text-green-200 font-mono ml-2">{compute.sessionData.workspace_id}</span>
|
203 |
+
</div>
|
204 |
+
{#each Object.entries(compute.sessionData.camera_room_ids) as [camera, roomId]}
|
205 |
+
<div>
|
206 |
+
<span class="text-green-400">📹 {camera}:</span>
|
207 |
+
<span class="text-green-200 font-mono ml-2">{roomId}</span>
|
208 |
+
</div>
|
209 |
+
{/each}
|
210 |
+
<div>
|
211 |
+
<span class="text-green-400">📥 Joint Input:</span>
|
212 |
+
<span class="text-green-200 font-mono ml-2">{compute.sessionData.joint_input_room_id}</span>
|
213 |
+
</div>
|
214 |
+
<div>
|
215 |
+
<span class="text-green-400">📤 Joint Output:</span>
|
216 |
+
<span class="text-green-200 font-mono ml-2">{compute.sessionData.joint_output_room_id}</span>
|
217 |
+
</div>
|
218 |
+
</div>
|
219 |
+
</div>
|
220 |
+
|
221 |
+
<!-- Session Controls -->
|
222 |
+
<div class="flex gap-2">
|
223 |
+
{#if compute.canStart}
|
224 |
+
<Button
|
225 |
+
variant="default"
|
226 |
+
size="sm"
|
227 |
+
onclick={handleStartSession}
|
228 |
+
disabled={isConnecting}
|
229 |
+
class="bg-green-600 hover:bg-green-700 text-xs disabled:opacity-50"
|
230 |
+
>
|
231 |
+
{#if isConnecting}
|
232 |
+
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
233 |
+
Starting...
|
234 |
+
{:else}
|
235 |
+
<span class="icon-[mdi--play] mr-1 size-3"></span>
|
236 |
+
Start Inference
|
237 |
+
{/if}
|
238 |
+
</Button>
|
239 |
+
{/if}
|
240 |
+
{#if compute.canStop}
|
241 |
+
<Button
|
242 |
+
variant="secondary"
|
243 |
+
size="sm"
|
244 |
+
onclick={handleStopSession}
|
245 |
+
disabled={isConnecting}
|
246 |
+
class="text-xs disabled:opacity-50"
|
247 |
+
>
|
248 |
+
{#if isConnecting}
|
249 |
+
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
250 |
+
Stopping...
|
251 |
+
{:else}
|
252 |
+
<span class="icon-[mdi--stop] mr-1 size-3"></span>
|
253 |
+
Stop Inference
|
254 |
+
{/if}
|
255 |
+
</Button>
|
256 |
+
{/if}
|
257 |
+
<Button
|
258 |
+
variant="destructive"
|
259 |
+
size="sm"
|
260 |
+
onclick={handleDeleteSession}
|
261 |
+
disabled={isConnecting}
|
262 |
+
class="text-xs disabled:opacity-50"
|
263 |
+
>
|
264 |
+
{#if isConnecting}
|
265 |
+
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
266 |
+
Deleting...
|
267 |
+
{:else}
|
268 |
+
<span class="icon-[mdi--delete] mr-1 size-3"></span>
|
269 |
+
Delete Session
|
270 |
+
{/if}
|
271 |
+
</Button>
|
272 |
+
</div>
|
273 |
+
</div>
|
274 |
+
</Card.Content>
|
275 |
+
</Card.Root>
|
276 |
+
{/if}
|
277 |
+
|
278 |
+
<!-- Create New Session -->
|
279 |
+
{#if !compute.hasSession}
|
280 |
+
<Card.Root class="border-purple-500/30 bg-purple-500/5">
|
281 |
+
<Card.Header>
|
282 |
+
<Card.Title class="flex items-center gap-2 text-base text-purple-200">
|
283 |
+
<span class="icon-[mdi--plus-circle] size-4"></span>
|
284 |
+
Create AI Session
|
285 |
+
</Card.Title>
|
286 |
+
</Card.Header>
|
287 |
+
<Card.Content>
|
288 |
+
<div class="space-y-4">
|
289 |
+
<div class="grid grid-cols-2 gap-4">
|
290 |
+
<div class="space-y-2">
|
291 |
+
<Label for="sessionId" class="text-purple-300">Session ID</Label>
|
292 |
+
<Input
|
293 |
+
id="sessionId"
|
294 |
+
bind:value={sessionId}
|
295 |
+
placeholder="my-session-01"
|
296 |
+
class="bg-slate-800 border-slate-600 text-slate-100"
|
297 |
+
/>
|
298 |
+
</div>
|
299 |
+
<div class="space-y-2">
|
300 |
+
<Label for="policyPath" class="text-purple-300">Policy Path</Label>
|
301 |
+
<Input
|
302 |
+
id="policyPath"
|
303 |
+
bind:value={policyPath}
|
304 |
+
placeholder="./checkpoints/act_so101_beyond"
|
305 |
+
class="bg-slate-800 border-slate-600 text-slate-100"
|
306 |
+
/>
|
307 |
+
</div>
|
308 |
+
</div>
|
309 |
+
|
310 |
+
<div class="grid grid-cols-2 gap-4">
|
311 |
+
<div class="space-y-2">
|
312 |
+
<Label for="cameraNames" class="text-purple-300">Camera Names</Label>
|
313 |
+
<Input
|
314 |
+
id="cameraNames"
|
315 |
+
bind:value={cameraNames}
|
316 |
+
placeholder="front, wrist, overhead"
|
317 |
+
class="bg-slate-800 border-slate-600 text-slate-100"
|
318 |
+
/>
|
319 |
+
<p class="text-xs text-slate-400">Comma-separated camera names</p>
|
320 |
+
</div>
|
321 |
+
<div class="space-y-2">
|
322 |
+
<Label for="transportServerUrl" class="text-purple-300">Transport Server URL</Label>
|
323 |
+
<Input
|
324 |
+
id="transportServerUrl"
|
325 |
+
value={settings.transportServerUrl}
|
326 |
+
disabled
|
327 |
+
placeholder="http://localhost:8000"
|
328 |
+
class="bg-slate-800 border-slate-600 text-slate-100 opacity-60 cursor-not-allowed"
|
329 |
+
title="Change this value in the settings panel"
|
330 |
+
/>
|
331 |
+
<p class="text-xs text-slate-400">Configure in settings panel</p>
|
332 |
+
</div>
|
333 |
+
</div>
|
334 |
+
|
335 |
+
<div class="flex items-center space-x-2">
|
336 |
+
<input
|
337 |
+
type="checkbox"
|
338 |
+
id="useWorkspace"
|
339 |
+
bind:checked={useProvidedWorkspace}
|
340 |
+
class="rounded border-slate-600 bg-slate-800"
|
341 |
+
/>
|
342 |
+
<Label for="useWorkspace" class="text-purple-300 text-sm">
|
343 |
+
Use current workspace ({workspaceId})
|
344 |
+
</Label>
|
345 |
+
</div>
|
346 |
+
|
347 |
+
<Alert.Root>
|
348 |
+
<span class="icon-[mdi--information] size-4"></span>
|
349 |
+
<Alert.Description>
|
350 |
+
This will create a new ACT inference session with dedicated rooms for camera inputs,
|
351 |
+
joint inputs, and joint outputs in the inference server communication system.
|
352 |
+
</Alert.Description>
|
353 |
+
</Alert.Root>
|
354 |
+
|
355 |
+
<Button
|
356 |
+
variant="default"
|
357 |
+
onclick={handleCreateSession}
|
358 |
+
disabled={isConnecting || !sessionId.trim() || !policyPath.trim()}
|
359 |
+
class="w-full bg-purple-600 hover:bg-purple-700 disabled:opacity-50"
|
360 |
+
>
|
361 |
+
{#if isConnecting}
|
362 |
+
<span class="icon-[mdi--loading] animate-spin mr-2 size-4"></span>
|
363 |
+
Creating Session...
|
364 |
+
{:else}
|
365 |
+
<span class="icon-[mdi--rocket-launch] mr-2 size-4"></span>
|
366 |
+
Create AI Session
|
367 |
+
{/if}
|
368 |
+
</Button>
|
369 |
+
</div>
|
370 |
+
</Card.Content>
|
371 |
+
</Card.Root>
|
372 |
+
{/if}
|
373 |
+
|
374 |
+
<!-- Quick Info -->
|
375 |
+
<div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
|
376 |
+
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
377 |
+
AI sessions require a trained ACT model and create dedicated communication rooms for video inputs,
|
378 |
+
robot joint states, and control outputs in the inference server system.
|
379 |
+
</div>
|
380 |
+
</div>
|
381 |
+
</Dialog.Content>
|
382 |
+
</Dialog.Root>
|
src/lib/components/3d/elements/compute/modal/RobotInputConnectionModal.svelte
ADDED
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import * as Dialog from "@/components/ui/dialog";
|
3 |
+
import { Button } from "@/components/ui/button";
|
4 |
+
import * as Card from "@/components/ui/card";
|
5 |
+
import { Badge } from "@/components/ui/badge";
|
6 |
+
import { toast } from "svelte-sonner";
|
7 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
8 |
+
import { robotManager } from "$lib/elements/robot/RobotManager.svelte";
|
9 |
+
|
10 |
+
interface Props {
|
11 |
+
workspaceId: string;
|
12 |
+
open: boolean;
|
13 |
+
compute: RemoteCompute;
|
14 |
+
}
|
15 |
+
|
16 |
+
let { open = $bindable(), compute, workspaceId }: Props = $props();
|
17 |
+
|
18 |
+
let isConnecting = $state(false);
|
19 |
+
let selectedRobotId = $state('');
|
20 |
+
let robotProducer: any = null;
|
21 |
+
let connectedRobotId = $state<string | null>(null);
|
22 |
+
|
23 |
+
// Get available robots from robot manager
|
24 |
+
const robots = $derived(robotManager.robots);
|
25 |
+
|
26 |
+
async function handleConnectRobotInput() {
|
27 |
+
if (!compute.hasSession) {
|
28 |
+
toast.error('No AI session available. Create a session first.');
|
29 |
+
return;
|
30 |
+
}
|
31 |
+
|
32 |
+
if (!selectedRobotId) {
|
33 |
+
toast.error('Please select a robot to connect.');
|
34 |
+
return;
|
35 |
+
}
|
36 |
+
|
37 |
+
isConnecting = true;
|
38 |
+
try {
|
39 |
+
// Get the joint input room ID from the AI session
|
40 |
+
const jointInputRoomId = compute.sessionData?.joint_input_room_id;
|
41 |
+
if (!jointInputRoomId) {
|
42 |
+
throw new Error('No joint input room found in AI session');
|
43 |
+
}
|
44 |
+
|
45 |
+
// Find the selected robot
|
46 |
+
const robot = robotManager.robots.find(r => r.id === selectedRobotId);
|
47 |
+
if (!robot) {
|
48 |
+
throw new Error(`Robot ${selectedRobotId} not found`);
|
49 |
+
}
|
50 |
+
|
51 |
+
// Connect robot as PRODUCER to the joint input room (robot sends joint states TO AI)
|
52 |
+
await robotManager.connectProducerToRoom(workspaceId, selectedRobotId, jointInputRoomId);
|
53 |
+
|
54 |
+
connectedRobotId = selectedRobotId;
|
55 |
+
|
56 |
+
toast.success('Robot input connected to AI session', {
|
57 |
+
description: `Robot ${selectedRobotId} now sends joint data to AI`
|
58 |
+
});
|
59 |
+
|
60 |
+
} catch (error) {
|
61 |
+
console.error('Robot input connection error:', error);
|
62 |
+
toast.error('Failed to connect robot input', {
|
63 |
+
description: error instanceof Error ? error.message : 'Unknown error'
|
64 |
+
});
|
65 |
+
} finally {
|
66 |
+
isConnecting = false;
|
67 |
+
}
|
68 |
+
}
|
69 |
+
|
70 |
+
async function handleDisconnectRobotInput() {
|
71 |
+
if (!connectedRobotId) return;
|
72 |
+
|
73 |
+
try {
|
74 |
+
// Find the connected robot
|
75 |
+
const robot = robotManager.robots.find(r => r.id === connectedRobotId);
|
76 |
+
if (robot) {
|
77 |
+
// Disconnect producer from the joint input room
|
78 |
+
for (const producer of robot.producers) {
|
79 |
+
await robot.removeProducer(producer.id);
|
80 |
+
}
|
81 |
+
}
|
82 |
+
|
83 |
+
connectedRobotId = null;
|
84 |
+
toast.success('Robot input disconnected');
|
85 |
+
} catch (error) {
|
86 |
+
console.error('Disconnect error:', error);
|
87 |
+
toast.error('Error disconnecting robot input');
|
88 |
+
}
|
89 |
+
}
|
90 |
+
|
91 |
+
// Cleanup on modal close
|
92 |
+
$effect(() => {
|
93 |
+
if (!open) {
|
94 |
+
// Don't auto-disconnect when modal closes, user might want to keep connection
|
95 |
+
}
|
96 |
+
});
|
97 |
+
</script>
|
98 |
+
|
99 |
+
<Dialog.Root bind:open>
|
100 |
+
<Dialog.Content
|
101 |
+
class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
|
102 |
+
>
|
103 |
+
<Dialog.Header class="pb-3">
|
104 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
105 |
+
<span class="icon-[mdi--robot-industrial] size-5 text-amber-400"></span>
|
106 |
+
Robot Input - {compute.name || 'No Compute Selected'}
|
107 |
+
</Dialog.Title>
|
108 |
+
<Dialog.Description class="text-sm text-slate-400">
|
109 |
+
Connect robot joint data as input for AI inference
|
110 |
+
</Dialog.Description>
|
111 |
+
</Dialog.Header>
|
112 |
+
|
113 |
+
<div class="space-y-4">
|
114 |
+
<!-- AI Session Status -->
|
115 |
+
<div
|
116 |
+
class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
|
117 |
+
>
|
118 |
+
<div class="flex items-center gap-2">
|
119 |
+
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
120 |
+
<span class="text-sm font-medium text-purple-300">AI Session</span>
|
121 |
+
</div>
|
122 |
+
{#if compute.hasSession}
|
123 |
+
<Badge variant="default" class="bg-purple-600 text-xs">
|
124 |
+
{compute.statusInfo.statusText}
|
125 |
+
</Badge>
|
126 |
+
{:else}
|
127 |
+
<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
|
128 |
+
{/if}
|
129 |
+
</div>
|
130 |
+
|
131 |
+
{#if !compute.hasSession}
|
132 |
+
<Card.Root class="border-yellow-500/30 bg-yellow-500/5">
|
133 |
+
<Card.Header>
|
134 |
+
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
135 |
+
<span class="icon-[mdi--alert] size-4"></span>
|
136 |
+
AI Session Required
|
137 |
+
</Card.Title>
|
138 |
+
</Card.Header>
|
139 |
+
<Card.Content class="text-sm text-yellow-300">
|
140 |
+
You need to create an AI session before connecting robot inputs.
|
141 |
+
The session provides a joint input room for receiving robot data.
|
142 |
+
</Card.Content>
|
143 |
+
</Card.Root>
|
144 |
+
{:else}
|
145 |
+
<!-- Robot Selection and Connection -->
|
146 |
+
<Card.Root class="border-amber-500/30 bg-amber-500/5">
|
147 |
+
<Card.Header>
|
148 |
+
<Card.Title class="flex items-center gap-2 text-base text-amber-200">
|
149 |
+
<span class="icon-[mdi--robot-industrial] size-4"></span>
|
150 |
+
Robot Input Connection
|
151 |
+
</Card.Title>
|
152 |
+
</Card.Header>
|
153 |
+
<Card.Content class="space-y-4">
|
154 |
+
<!-- Available Robots -->
|
155 |
+
<div class="space-y-2">
|
156 |
+
<div class="text-sm font-medium text-amber-300">Available Robots:</div>
|
157 |
+
<div class="max-h-40 overflow-y-auto space-y-2">
|
158 |
+
{#if robots.length === 0}
|
159 |
+
<div class="text-center py-4 text-sm text-slate-400">
|
160 |
+
No robots available. Add robots first.
|
161 |
+
</div>
|
162 |
+
{:else}
|
163 |
+
{#each robots as robot}
|
164 |
+
<button
|
165 |
+
onclick={() => selectedRobotId = robot.id}
|
166 |
+
class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
|
167 |
+
? 'border-amber-500 bg-amber-500/20'
|
168 |
+
: 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
|
169 |
+
>
|
170 |
+
<div class="flex items-center justify-between">
|
171 |
+
<div>
|
172 |
+
<div class="text-xs text-slate-400">
|
173 |
+
ID: {robot.id}
|
174 |
+
</div>
|
175 |
+
<div class="text-xs text-slate-400">
|
176 |
+
Producers: {robot.producers.length}
|
177 |
+
</div>
|
178 |
+
</div>
|
179 |
+
<div class="flex items-center gap-2">
|
180 |
+
{#if robot.producers.length > 0}
|
181 |
+
<Badge variant="default" class="bg-green-600 text-xs">
|
182 |
+
Active
|
183 |
+
</Badge>
|
184 |
+
{:else}
|
185 |
+
<Badge variant="secondary" class="text-xs">
|
186 |
+
Available
|
187 |
+
</Badge>
|
188 |
+
{/if}
|
189 |
+
</div>
|
190 |
+
</div>
|
191 |
+
</button>
|
192 |
+
{/each}
|
193 |
+
{/if}
|
194 |
+
</div>
|
195 |
+
</div>
|
196 |
+
|
197 |
+
<!-- Connection Status -->
|
198 |
+
{#if selectedRobotId}
|
199 |
+
<div class="rounded-lg border border-amber-500/30 bg-amber-900/20 p-3">
|
200 |
+
<div class="flex items-center justify-between">
|
201 |
+
<div>
|
202 |
+
<p class="text-sm font-medium text-amber-300">
|
203 |
+
Selected Robot: {selectedRobotId}
|
204 |
+
</p>
|
205 |
+
<p class="text-xs text-amber-400/70">
|
206 |
+
{connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
|
207 |
+
</p>
|
208 |
+
</div>
|
209 |
+
{#if connectedRobotId !== selectedRobotId}
|
210 |
+
<Button
|
211 |
+
variant="default"
|
212 |
+
size="sm"
|
213 |
+
onclick={handleConnectRobotInput}
|
214 |
+
disabled={isConnecting}
|
215 |
+
class="bg-amber-600 hover:bg-amber-700 text-xs disabled:opacity-50"
|
216 |
+
>
|
217 |
+
{#if isConnecting}
|
218 |
+
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
219 |
+
Connecting...
|
220 |
+
{:else}
|
221 |
+
<span class="icon-[mdi--link] mr-1 size-3"></span>
|
222 |
+
Connect Input
|
223 |
+
{/if}
|
224 |
+
</Button>
|
225 |
+
{:else}
|
226 |
+
<Button
|
227 |
+
variant="destructive"
|
228 |
+
size="sm"
|
229 |
+
onclick={handleDisconnectRobotInput}
|
230 |
+
class="text-xs"
|
231 |
+
>
|
232 |
+
<span class="icon-[mdi--close-circle] mr-1 size-3"></span>
|
233 |
+
Disconnect
|
234 |
+
</Button>
|
235 |
+
{/if}
|
236 |
+
</div>
|
237 |
+
</div>
|
238 |
+
{/if}
|
239 |
+
</Card.Content>
|
240 |
+
</Card.Root>
|
241 |
+
|
242 |
+
<!-- Session Joint Input Details -->
|
243 |
+
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
244 |
+
<Card.Header>
|
245 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
246 |
+
<span class="icon-[mdi--information] size-4"></span>
|
247 |
+
Data Flow: Robot → AI Session
|
248 |
+
</Card.Title>
|
249 |
+
</Card.Header>
|
250 |
+
<Card.Content>
|
251 |
+
<div class="space-y-2 text-xs">
|
252 |
+
<div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
|
253 |
+
<span class="text-blue-300 font-medium">Joint Input Room:</span>
|
254 |
+
<span class="text-blue-200 font-mono">{compute.sessionData?.joint_input_room_id}</span>
|
255 |
+
</div>
|
256 |
+
<div class="text-slate-400 text-xs">
|
257 |
+
The robot will act as a <strong>PRODUCER</strong> and send its current joint positions to this room for AI processing.
|
258 |
+
The inference server receives this data as a CONSUMER.
|
259 |
+
All joint values should be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
|
260 |
+
</div>
|
261 |
+
</div>
|
262 |
+
</Card.Content>
|
263 |
+
</Card.Root>
|
264 |
+
|
265 |
+
<!-- Connection Status -->
|
266 |
+
{#if connectedRobotId}
|
267 |
+
<Card.Root class="border-green-500/30 bg-green-500/5">
|
268 |
+
<Card.Header>
|
269 |
+
<Card.Title class="flex items-center gap-2 text-base text-green-200">
|
270 |
+
<span class="icon-[mdi--check-circle] size-4"></span>
|
271 |
+
Active Connection
|
272 |
+
</Card.Title>
|
273 |
+
</Card.Header>
|
274 |
+
<Card.Content>
|
275 |
+
<div class="text-sm text-green-300">
|
276 |
+
Robot <span class="font-mono">{connectedRobotId}</span> is now sending joint data to the AI session as a producer.
|
277 |
+
The AI model will use this data along with camera inputs for inference.
|
278 |
+
</div>
|
279 |
+
</Card.Content>
|
280 |
+
</Card.Root>
|
281 |
+
{/if}
|
282 |
+
{/if}
|
283 |
+
|
284 |
+
<!-- Quick Info -->
|
285 |
+
<div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
|
286 |
+
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
287 |
+
Robot input: Robot acts as PRODUCER sending joint positions → Inference server acts as CONSUMER receiving data for processing.
|
288 |
+
</div>
|
289 |
+
</div>
|
290 |
+
</Dialog.Content>
|
291 |
+
</Dialog.Root>
|
src/lib/components/3d/elements/compute/modal/RobotOutputConnectionModal.svelte
ADDED
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import * as Dialog from "@/components/ui/dialog";
|
3 |
+
import { Button } from "@/components/ui/button";
|
4 |
+
import * as Card from "@/components/ui/card";
|
5 |
+
import { Badge } from "@/components/ui/badge";
|
6 |
+
import { toast } from "svelte-sonner";
|
7 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
8 |
+
import { robotManager } from "$lib/elements/robot/RobotManager.svelte";
|
9 |
+
|
10 |
+
interface Props {
|
11 |
+
workspaceId: string;
|
12 |
+
open: boolean;
|
13 |
+
compute: RemoteCompute;
|
14 |
+
}
|
15 |
+
|
16 |
+
let { open = $bindable(), compute, workspaceId }: Props = $props();
|
17 |
+
|
18 |
+
let isConnecting = $state(false);
|
19 |
+
let selectedRobotId = $state('');
|
20 |
+
let robotConsumer: any = null;
|
21 |
+
let connectedRobotId = $state<string | null>(null);
|
22 |
+
|
23 |
+
// Get available robots from robot manager
|
24 |
+
const robots = $derived(robotManager.robots);
|
25 |
+
|
26 |
+
async function handleConnectRobotOutput() {
|
27 |
+
if (!compute.hasSession) {
|
28 |
+
toast.error('No AI session available. Create a session first.');
|
29 |
+
return;
|
30 |
+
}
|
31 |
+
|
32 |
+
if (!selectedRobotId) {
|
33 |
+
toast.error('Please select a robot to connect.');
|
34 |
+
return;
|
35 |
+
}
|
36 |
+
|
37 |
+
isConnecting = true;
|
38 |
+
try {
|
39 |
+
// Get the joint output room ID from the AI session
|
40 |
+
const jointOutputRoomId = compute.sessionData?.joint_output_room_id;
|
41 |
+
if (!jointOutputRoomId) {
|
42 |
+
throw new Error('No joint output room found in AI session');
|
43 |
+
}
|
44 |
+
|
45 |
+
// Find the selected robot
|
46 |
+
const robot = robotManager.robots.find(r => r.id === selectedRobotId);
|
47 |
+
if (!robot) {
|
48 |
+
throw new Error(`Robot ${selectedRobotId} not found`);
|
49 |
+
}
|
50 |
+
|
51 |
+
// Connect robot as CONSUMER to the joint output room (robot receives commands FROM AI)
|
52 |
+
await robotManager.connectConsumerToRoom(workspaceId, selectedRobotId, jointOutputRoomId);
|
53 |
+
|
54 |
+
connectedRobotId = selectedRobotId;
|
55 |
+
|
56 |
+
toast.success('Robot output connected to AI session', {
|
57 |
+
description: `Robot ${selectedRobotId} now receives AI commands`
|
58 |
+
});
|
59 |
+
|
60 |
+
} catch (error) {
|
61 |
+
console.error('Robot output connection error:', error);
|
62 |
+
toast.error('Failed to connect robot output', {
|
63 |
+
description: error instanceof Error ? error.message : 'Unknown error'
|
64 |
+
});
|
65 |
+
} finally {
|
66 |
+
isConnecting = false;
|
67 |
+
}
|
68 |
+
}
|
69 |
+
|
70 |
+
async function handleDisconnectRobotOutput() {
|
71 |
+
if (!connectedRobotId) return;
|
72 |
+
|
73 |
+
try {
|
74 |
+
// Find the connected robot
|
75 |
+
const robot = robotManager.robots.find(r => r.id === connectedRobotId);
|
76 |
+
if (robot) {
|
77 |
+
await robot.removeConsumer();
|
78 |
+
}
|
79 |
+
|
80 |
+
connectedRobotId = null;
|
81 |
+
toast.success('Robot output disconnected');
|
82 |
+
} catch (error) {
|
83 |
+
console.error('Disconnect error:', error);
|
84 |
+
toast.error('Error disconnecting robot output');
|
85 |
+
}
|
86 |
+
}
|
87 |
+
|
88 |
+
// Cleanup on modal close
|
89 |
+
$effect(() => {
|
90 |
+
if (!open) {
|
91 |
+
// Don't auto-disconnect when modal closes, user might want to keep connection
|
92 |
+
}
|
93 |
+
});
|
94 |
+
</script>
|
95 |
+
|
96 |
+
<Dialog.Root bind:open>
|
97 |
+
<Dialog.Content
|
98 |
+
class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
|
99 |
+
>
|
100 |
+
<Dialog.Header class="pb-3">
|
101 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
102 |
+
<span class="icon-[mdi--robot-outline] size-5 text-blue-400"></span>
|
103 |
+
Robot Output - {compute.name || 'No Compute Selected'}
|
104 |
+
</Dialog.Title>
|
105 |
+
<Dialog.Description class="text-sm text-slate-400">
|
106 |
+
Connect AI command output to control robot actuators
|
107 |
+
</Dialog.Description>
|
108 |
+
</Dialog.Header>
|
109 |
+
|
110 |
+
<div class="space-y-4">
|
111 |
+
<!-- AI Session Status -->
|
112 |
+
<div
|
113 |
+
class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
|
114 |
+
>
|
115 |
+
<div class="flex items-center gap-2">
|
116 |
+
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
117 |
+
<span class="text-sm font-medium text-purple-300">AI Session</span>
|
118 |
+
</div>
|
119 |
+
{#if compute.hasSession}
|
120 |
+
<Badge variant="default" class="bg-purple-600 text-xs">
|
121 |
+
{compute.statusInfo.statusText}
|
122 |
+
</Badge>
|
123 |
+
{:else}
|
124 |
+
<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
|
125 |
+
{/if}
|
126 |
+
</div>
|
127 |
+
|
128 |
+
{#if !compute.hasSession}
|
129 |
+
<Card.Root class="border-yellow-500/30 bg-yellow-500/5">
|
130 |
+
<Card.Header>
|
131 |
+
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
132 |
+
<span class="icon-[mdi--alert] size-4"></span>
|
133 |
+
AI Session Required
|
134 |
+
</Card.Title>
|
135 |
+
</Card.Header>
|
136 |
+
<Card.Content class="text-sm text-yellow-300">
|
137 |
+
You need to create an AI session before connecting robot outputs.
|
138 |
+
The session provides a joint output room for sending AI commands.
|
139 |
+
</Card.Content>
|
140 |
+
</Card.Root>
|
141 |
+
{:else}
|
142 |
+
<!-- Robot Selection and Connection -->
|
143 |
+
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
144 |
+
<Card.Header>
|
145 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
146 |
+
<span class="icon-[mdi--robot-outline] size-4"></span>
|
147 |
+
Robot Output Connection
|
148 |
+
</Card.Title>
|
149 |
+
</Card.Header>
|
150 |
+
<Card.Content class="space-y-4">
|
151 |
+
<!-- Available Robots -->
|
152 |
+
<div class="space-y-2">
|
153 |
+
<div class="text-sm font-medium text-blue-300">Available Robots:</div>
|
154 |
+
<div class="max-h-40 overflow-y-auto space-y-2">
|
155 |
+
{#if robots.length === 0}
|
156 |
+
<div class="text-center py-4 text-sm text-slate-400">
|
157 |
+
No robots available. Add robots first.
|
158 |
+
</div>
|
159 |
+
{:else}
|
160 |
+
{#each robots as robot}
|
161 |
+
<button
|
162 |
+
onclick={() => selectedRobotId = robot.id}
|
163 |
+
class="w-full p-3 rounded border text-left {selectedRobotId === robot.id
|
164 |
+
? 'border-blue-500 bg-blue-500/20'
|
165 |
+
: 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
|
166 |
+
>
|
167 |
+
<div class="flex items-center justify-between">
|
168 |
+
<div>
|
169 |
+
<div class="text-xs text-slate-400">
|
170 |
+
ID: {robot.id}
|
171 |
+
</div>
|
172 |
+
<div class="text-xs text-slate-400">
|
173 |
+
Consumer: {robot.hasConsumer ? 'Connected' : 'None'}
|
174 |
+
</div>
|
175 |
+
</div>
|
176 |
+
<div class="flex items-center gap-2">
|
177 |
+
{#if robot.hasConsumer}
|
178 |
+
<Badge variant="default" class="bg-green-600 text-xs">
|
179 |
+
Active
|
180 |
+
</Badge>
|
181 |
+
{:else}
|
182 |
+
<Badge variant="secondary" class="text-xs">
|
183 |
+
Available
|
184 |
+
</Badge>
|
185 |
+
{/if}
|
186 |
+
</div>
|
187 |
+
</div>
|
188 |
+
</button>
|
189 |
+
{/each}
|
190 |
+
{/if}
|
191 |
+
</div>
|
192 |
+
</div>
|
193 |
+
|
194 |
+
<!-- Connection Status -->
|
195 |
+
{#if selectedRobotId}
|
196 |
+
<div class="rounded-lg border border-blue-500/30 bg-blue-900/20 p-3">
|
197 |
+
<div class="flex items-center justify-between">
|
198 |
+
<div>
|
199 |
+
<p class="text-sm font-medium text-blue-300">
|
200 |
+
Selected Robot: {selectedRobotId}
|
201 |
+
</p>
|
202 |
+
<p class="text-xs text-blue-400/70">
|
203 |
+
{connectedRobotId === selectedRobotId ? 'Connected to AI' : 'Not Connected'}
|
204 |
+
</p>
|
205 |
+
</div>
|
206 |
+
{#if connectedRobotId !== selectedRobotId}
|
207 |
+
<Button
|
208 |
+
variant="default"
|
209 |
+
size="sm"
|
210 |
+
onclick={handleConnectRobotOutput}
|
211 |
+
disabled={isConnecting}
|
212 |
+
class="bg-blue-600 hover:bg-blue-700 text-xs disabled:opacity-50"
|
213 |
+
>
|
214 |
+
{#if isConnecting}
|
215 |
+
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
216 |
+
Connecting...
|
217 |
+
{:else}
|
218 |
+
<span class="icon-[mdi--link] mr-1 size-3"></span>
|
219 |
+
Connect Output
|
220 |
+
{/if}
|
221 |
+
</Button>
|
222 |
+
{:else}
|
223 |
+
<Button
|
224 |
+
variant="destructive"
|
225 |
+
size="sm"
|
226 |
+
onclick={handleDisconnectRobotOutput}
|
227 |
+
class="text-xs"
|
228 |
+
>
|
229 |
+
<span class="icon-[mdi--close-circle] mr-1 size-3"></span>
|
230 |
+
Disconnect
|
231 |
+
</Button>
|
232 |
+
{/if}
|
233 |
+
</div>
|
234 |
+
</div>
|
235 |
+
{/if}
|
236 |
+
</Card.Content>
|
237 |
+
</Card.Root>
|
238 |
+
|
239 |
+
<!-- Session Joint Output Details -->
|
240 |
+
<Card.Root class="border-orange-500/30 bg-orange-500/5">
|
241 |
+
<Card.Header>
|
242 |
+
<Card.Title class="flex items-center gap-2 text-base text-orange-200">
|
243 |
+
<span class="icon-[mdi--information] size-4"></span>
|
244 |
+
Data Flow: AI Session → Robot
|
245 |
+
</Card.Title>
|
246 |
+
</Card.Header>
|
247 |
+
<Card.Content>
|
248 |
+
<div class="space-y-2 text-xs">
|
249 |
+
<div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
|
250 |
+
<span class="text-orange-300 font-medium">Joint Output Room:</span>
|
251 |
+
<span class="text-orange-200 font-mono">{compute.sessionData?.joint_output_room_id}</span>
|
252 |
+
</div>
|
253 |
+
<div class="text-slate-400 text-xs">
|
254 |
+
The inference server will act as a <strong>PRODUCER</strong> and send predicted joint commands to this room for robot execution.
|
255 |
+
The robot receives this data as a CONSUMER.
|
256 |
+
All joint values will be normalized (-100 to +100 for most joints, 0 to 100 for gripper).
|
257 |
+
</div>
|
258 |
+
</div>
|
259 |
+
</Card.Content>
|
260 |
+
</Card.Root>
|
261 |
+
|
262 |
+
<!-- Connection Status -->
|
263 |
+
{#if connectedRobotId}
|
264 |
+
<Card.Root class="border-green-500/30 bg-green-500/5">
|
265 |
+
<Card.Header>
|
266 |
+
<Card.Title class="flex items-center gap-2 text-base text-green-200">
|
267 |
+
<span class="icon-[mdi--check-circle] size-4"></span>
|
268 |
+
Active Connection
|
269 |
+
</Card.Title>
|
270 |
+
</Card.Header>
|
271 |
+
<Card.Content>
|
272 |
+
<div class="text-sm text-green-300">
|
273 |
+
Robot <span class="font-mono">{connectedRobotId}</span> is now receiving AI commands as a consumer.
|
274 |
+
The robot will execute joint movements based on AI inference results.
|
275 |
+
</div>
|
276 |
+
</Card.Content>
|
277 |
+
</Card.Root>
|
278 |
+
{/if}
|
279 |
+
{/if}
|
280 |
+
|
281 |
+
<!-- Quick Info -->
|
282 |
+
<div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
|
283 |
+
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
284 |
+
Robot output: Inference server acts as PRODUCER sending commands → Robot acts as CONSUMER receiving and executing movements.
|
285 |
+
</div>
|
286 |
+
</div>
|
287 |
+
</Dialog.Content>
|
288 |
+
</Dialog.Root>
|
src/lib/components/3d/elements/compute/modal/VideoInputConnectionModal.svelte
ADDED
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import * as Dialog from "@/components/ui/dialog";
|
3 |
+
import { Button } from "@/components/ui/button";
|
4 |
+
import * as Card from "@/components/ui/card";
|
5 |
+
import { Badge } from "@/components/ui/badge";
|
6 |
+
import { toast } from "svelte-sonner";
|
7 |
+
import { settings } from "$lib/runes/settings.svelte";
|
8 |
+
import { videoManager } from "$lib/elements/video//VideoManager.svelte";
|
9 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
10 |
+
|
11 |
+
interface Props {
|
12 |
+
workspaceId: string;
|
13 |
+
open: boolean;
|
14 |
+
compute: RemoteCompute;
|
15 |
+
}
|
16 |
+
|
17 |
+
let { open = $bindable(), compute, workspaceId }: Props = $props();
|
18 |
+
|
19 |
+
let isConnecting = $state(false);
|
20 |
+
let selectedCameraName = $state('front');
|
21 |
+
let localStream: MediaStream | null = $state(null);
|
22 |
+
let videoProducer: any = null;
|
23 |
+
|
24 |
+
// Auto-refresh rooms when modal opens
|
25 |
+
$effect(() => {
|
26 |
+
if (open) {
|
27 |
+
videoManager.refreshRooms(workspaceId);
|
28 |
+
}
|
29 |
+
});
|
30 |
+
|
31 |
+
async function handleConnectLocalCamera() {
|
32 |
+
if (!compute.hasSession) {
|
33 |
+
toast.error('No AI session available. Create a session first.');
|
34 |
+
return;
|
35 |
+
}
|
36 |
+
|
37 |
+
isConnecting = true;
|
38 |
+
try {
|
39 |
+
// Get user media
|
40 |
+
const stream = await navigator.mediaDevices.getUserMedia({
|
41 |
+
video: true,
|
42 |
+
audio: false
|
43 |
+
});
|
44 |
+
localStream = stream;
|
45 |
+
|
46 |
+
// Get the camera room ID for the selected camera
|
47 |
+
const cameraRoomId = compute.sessionData?.camera_room_ids[selectedCameraName];
|
48 |
+
if (!cameraRoomId) {
|
49 |
+
throw new Error(`No room found for camera: ${selectedCameraName}`);
|
50 |
+
}
|
51 |
+
|
52 |
+
// Create video producer and connect to the camera room
|
53 |
+
const { VideoProducer } = await import("@robohub/transport-server-client/video");
|
54 |
+
videoProducer = new VideoProducer(settings.transportServerUrl);
|
55 |
+
|
56 |
+
// Connect to the EXISTING camera room (don't create new one)
|
57 |
+
const participantId = `frontend-camera-${selectedCameraName}-${Date.now()}`;
|
58 |
+
const success = await videoProducer.connect(workspaceId, cameraRoomId, participantId);
|
59 |
+
|
60 |
+
if (!success) {
|
61 |
+
throw new Error('Failed to connect to camera room');
|
62 |
+
}
|
63 |
+
|
64 |
+
// Start streaming
|
65 |
+
await videoProducer.startCamera();
|
66 |
+
|
67 |
+
toast.success(`Camera connected to AI session`, {
|
68 |
+
description: `Local camera streaming to ${selectedCameraName} input`
|
69 |
+
});
|
70 |
+
|
71 |
+
} catch (error) {
|
72 |
+
console.error('Camera connection error:', error);
|
73 |
+
toast.error('Failed to connect camera', {
|
74 |
+
description: error instanceof Error ? error.message : 'Unknown error'
|
75 |
+
});
|
76 |
+
} finally {
|
77 |
+
isConnecting = false;
|
78 |
+
}
|
79 |
+
}
|
80 |
+
|
81 |
+
async function handleDisconnectCamera() {
|
82 |
+
try {
|
83 |
+
if (videoProducer) {
|
84 |
+
await videoProducer.stopStreaming();
|
85 |
+
await videoProducer.disconnect();
|
86 |
+
videoProducer = null;
|
87 |
+
}
|
88 |
+
|
89 |
+
if (localStream) {
|
90 |
+
localStream.getTracks().forEach(track => track.stop());
|
91 |
+
localStream = null;
|
92 |
+
}
|
93 |
+
|
94 |
+
toast.success('Camera disconnected');
|
95 |
+
} catch (error) {
|
96 |
+
console.error('Disconnect error:', error);
|
97 |
+
toast.error('Error disconnecting camera');
|
98 |
+
}
|
99 |
+
}
|
100 |
+
|
101 |
+
// Cleanup on modal close
|
102 |
+
$effect(() => {
|
103 |
+
return () => {
|
104 |
+
if (!open) {
|
105 |
+
handleDisconnectCamera();
|
106 |
+
}
|
107 |
+
};
|
108 |
+
});
|
109 |
+
</script>
|
110 |
+
|
111 |
+
<Dialog.Root bind:open>
|
112 |
+
<Dialog.Content
|
113 |
+
class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
|
114 |
+
>
|
115 |
+
<Dialog.Header class="pb-3">
|
116 |
+
<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
|
117 |
+
<span class="icon-[mdi--video-input-component] size-5 text-green-400"></span>
|
118 |
+
Video Input - {compute.name || 'No Compute Selected'}
|
119 |
+
</Dialog.Title>
|
120 |
+
<Dialog.Description class="text-sm text-slate-400">
|
121 |
+
Connect camera streams to provide visual input for AI inference
|
122 |
+
</Dialog.Description>
|
123 |
+
</Dialog.Header>
|
124 |
+
|
125 |
+
<div class="space-y-4">
|
126 |
+
<!-- AI Session Status -->
|
127 |
+
<div
|
128 |
+
class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
|
129 |
+
>
|
130 |
+
<div class="flex items-center gap-2">
|
131 |
+
<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
|
132 |
+
<span class="text-sm font-medium text-purple-300">AI Session</span>
|
133 |
+
</div>
|
134 |
+
{#if compute.hasSession}
|
135 |
+
<Badge variant="default" class="bg-purple-600 text-xs">
|
136 |
+
{compute.statusInfo.statusText}
|
137 |
+
</Badge>
|
138 |
+
{:else}
|
139 |
+
<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
|
140 |
+
{/if}
|
141 |
+
</div>
|
142 |
+
|
143 |
+
{#if !compute.hasSession}
|
144 |
+
<Card.Root class="border-yellow-500/30 bg-yellow-500/5">
|
145 |
+
<Card.Header>
|
146 |
+
<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
|
147 |
+
<span class="icon-[mdi--alert] size-4"></span>
|
148 |
+
AI Session Required
|
149 |
+
</Card.Title>
|
150 |
+
</Card.Header>
|
151 |
+
<Card.Content class="text-sm text-yellow-300">
|
152 |
+
You need to create an AI session before connecting video inputs.
|
153 |
+
The session defines which camera names are available for connection.
|
154 |
+
</Card.Content>
|
155 |
+
</Card.Root>
|
156 |
+
{:else}
|
157 |
+
<!-- Camera Selection and Connection -->
|
158 |
+
<Card.Root class="border-green-500/30 bg-green-500/5">
|
159 |
+
<Card.Header>
|
160 |
+
<Card.Title class="flex items-center gap-2 text-base text-green-200">
|
161 |
+
<span class="icon-[mdi--camera] size-4"></span>
|
162 |
+
Camera Connection
|
163 |
+
</Card.Title>
|
164 |
+
</Card.Header>
|
165 |
+
<Card.Content class="space-y-4">
|
166 |
+
<!-- Available Cameras -->
|
167 |
+
<div class="space-y-2">
|
168 |
+
<div class="text-sm font-medium text-green-300">Available Camera Inputs:</div>
|
169 |
+
<div class="grid grid-cols-2 gap-2">
|
170 |
+
{#each compute.sessionConfig?.cameraNames || [] as cameraName}
|
171 |
+
<button
|
172 |
+
onclick={() => selectedCameraName = cameraName}
|
173 |
+
class="p-2 rounded border text-left {selectedCameraName === cameraName
|
174 |
+
? 'border-green-500 bg-green-500/20'
|
175 |
+
: 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
|
176 |
+
>
|
177 |
+
<div class="text-sm font-medium">{cameraName}</div>
|
178 |
+
<div class="text-xs text-slate-400">
|
179 |
+
Room: {compute.sessionData?.camera_room_ids[cameraName]?.slice(-8)}
|
180 |
+
</div>
|
181 |
+
</button>
|
182 |
+
{/each}
|
183 |
+
</div>
|
184 |
+
</div>
|
185 |
+
|
186 |
+
<!-- Connection Status -->
|
187 |
+
<div class="rounded-lg border border-green-500/30 bg-green-900/20 p-3">
|
188 |
+
<div class="flex items-center justify-between">
|
189 |
+
<div>
|
190 |
+
<p class="text-sm font-medium text-green-300">
|
191 |
+
Selected Camera: {selectedCameraName}
|
192 |
+
</p>
|
193 |
+
<p class="text-xs text-green-400/70">
|
194 |
+
{localStream ? 'Connected' : 'Not Connected'}
|
195 |
+
</p>
|
196 |
+
</div>
|
197 |
+
{#if !localStream}
|
198 |
+
<Button
|
199 |
+
variant="default"
|
200 |
+
size="sm"
|
201 |
+
onclick={handleConnectLocalCamera}
|
202 |
+
disabled={isConnecting}
|
203 |
+
class="bg-green-600 hover:bg-green-700 text-xs disabled:opacity-50"
|
204 |
+
>
|
205 |
+
{#if isConnecting}
|
206 |
+
<span class="icon-[mdi--loading] animate-spin mr-1 size-3"></span>
|
207 |
+
Connecting...
|
208 |
+
{:else}
|
209 |
+
<span class="icon-[mdi--camera] mr-1 size-3"></span>
|
210 |
+
Connect Camera
|
211 |
+
{/if}
|
212 |
+
</Button>
|
213 |
+
{:else}
|
214 |
+
<Button
|
215 |
+
variant="destructive"
|
216 |
+
size="sm"
|
217 |
+
onclick={handleDisconnectCamera}
|
218 |
+
class="text-xs"
|
219 |
+
>
|
220 |
+
<span class="icon-[mdi--close-circle] mr-1 size-3"></span>
|
221 |
+
Disconnect
|
222 |
+
</Button>
|
223 |
+
{/if}
|
224 |
+
</div>
|
225 |
+
</div>
|
226 |
+
|
227 |
+
<!-- Live Preview -->
|
228 |
+
{#if localStream}
|
229 |
+
<div class="space-y-2">
|
230 |
+
<div class="text-sm font-medium text-green-300">Live Preview:</div>
|
231 |
+
<div class="rounded border border-green-500/30 bg-black/50 aspect-video overflow-hidden">
|
232 |
+
<video
|
233 |
+
autoplay
|
234 |
+
muted
|
235 |
+
playsinline
|
236 |
+
class="w-full h-full object-cover"
|
237 |
+
onloadedmetadata={(e) => {
|
238 |
+
const video = e.target as HTMLVideoElement;
|
239 |
+
video.srcObject = localStream;
|
240 |
+
}}
|
241 |
+
></video>
|
242 |
+
</div>
|
243 |
+
</div>
|
244 |
+
{/if}
|
245 |
+
</Card.Content>
|
246 |
+
</Card.Root>
|
247 |
+
|
248 |
+
<!-- Session Camera Details -->
|
249 |
+
<Card.Root class="border-blue-500/30 bg-blue-500/5">
|
250 |
+
<Card.Header>
|
251 |
+
<Card.Title class="flex items-center gap-2 text-base text-blue-200">
|
252 |
+
<span class="icon-[mdi--information] size-4"></span>
|
253 |
+
Session Camera Details
|
254 |
+
</Card.Title>
|
255 |
+
</Card.Header>
|
256 |
+
<Card.Content>
|
257 |
+
<div class="space-y-2 text-xs">
|
258 |
+
{#each Object.entries(compute.sessionData?.camera_room_ids || {}) as [camera, roomId]}
|
259 |
+
<div class="flex justify-between items-center p-2 rounded bg-slate-800/50">
|
260 |
+
<span class="text-blue-300 font-medium">{camera}</span>
|
261 |
+
<span class="text-blue-200 font-mono">{roomId}</span>
|
262 |
+
</div>
|
263 |
+
{/each}
|
264 |
+
</div>
|
265 |
+
</Card.Content>
|
266 |
+
</Card.Root>
|
267 |
+
{/if}
|
268 |
+
|
269 |
+
<!-- Quick Info -->
|
270 |
+
<div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
|
271 |
+
<span class="icon-[mdi--information] mr-1 size-3"></span>
|
272 |
+
Video inputs stream camera data to the AI model for visual processing. Each camera connects to a dedicated room in the session.
|
273 |
+
</div>
|
274 |
+
</div>
|
275 |
+
</Dialog.Content>
|
276 |
+
</Dialog.Root>
|
src/lib/components/3d/elements/compute/status/ComputeBoxUIKit.svelte
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
3 |
+
import { ICON } from "$lib/utils/icon";
|
4 |
+
import {
|
5 |
+
BaseStatusBox,
|
6 |
+
StatusHeader,
|
7 |
+
StatusContent,
|
8 |
+
StatusIndicator
|
9 |
+
} from "$lib/components/3d/ui";
|
10 |
+
import { Text } from "threlte-uikit";
|
11 |
+
|
12 |
+
interface Props {
|
13 |
+
compute: RemoteCompute;
|
14 |
+
}
|
15 |
+
|
16 |
+
let { compute }: Props = $props();
|
17 |
+
|
18 |
+
// Compute theme color
|
19 |
+
const computeColor = "rgb(139, 69, 219)";
|
20 |
+
</script>
|
21 |
+
|
22 |
+
|
23 |
+
<BaseStatusBox
|
24 |
+
minWidth={110}
|
25 |
+
minHeight={135}
|
26 |
+
color={computeColor}
|
27 |
+
borderOpacity={0.6}
|
28 |
+
backgroundOpacity={0.2}
|
29 |
+
clickable={false}
|
30 |
+
>
|
31 |
+
<!-- Header -->
|
32 |
+
<StatusHeader
|
33 |
+
icon={ICON["icon-[mdi--brain]"].svg}
|
34 |
+
text="AI COMPUTE"
|
35 |
+
color={computeColor}
|
36 |
+
opacity={0.9}
|
37 |
+
fontSize={12}
|
38 |
+
/>
|
39 |
+
|
40 |
+
<!-- Compute Info -->
|
41 |
+
<StatusContent
|
42 |
+
title={compute.name}
|
43 |
+
subtitle={compute.statusInfo.statusText}
|
44 |
+
color="rgb(221, 214, 254)"
|
45 |
+
variant="primary"
|
46 |
+
/>
|
47 |
+
</BaseStatusBox>
|
48 |
+
|
src/lib/components/3d/elements/compute/status/ComputeConnectionFlowBoxUIKit.svelte
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import VideoInputBoxUIKit from "./VideoInputBoxUIKit.svelte";
|
3 |
+
import RobotInputBoxUIKit from "./RobotInputBoxUIKit.svelte";
|
4 |
+
import ComputeOutputBoxUIKit from "./ComputeOutputBoxUIKit.svelte";
|
5 |
+
import ComputeBoxUIKit from "./ComputeBoxUIKit.svelte";
|
6 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
7 |
+
import { Container } from "threlte-uikit";
|
8 |
+
import { StatusArrow } from "$lib/components/3d/ui";
|
9 |
+
|
10 |
+
interface Props {
|
11 |
+
compute: RemoteCompute;
|
12 |
+
onVideoInputBoxClick: (compute: RemoteCompute) => void;
|
13 |
+
onRobotInputBoxClick: (compute: RemoteCompute) => void;
|
14 |
+
onRobotOutputBoxClick: (compute: RemoteCompute) => void;
|
15 |
+
}
|
16 |
+
|
17 |
+
let { compute, onVideoInputBoxClick, onRobotInputBoxClick, onRobotOutputBoxClick }: Props = $props();
|
18 |
+
|
19 |
+
// Colors
|
20 |
+
const inputColor = "rgb(34, 197, 94)";
|
21 |
+
const outputColor = "rgb(59, 130, 246)";
|
22 |
+
</script>
|
23 |
+
|
24 |
+
<!--
|
25 |
+
@component
|
26 |
+
Elegant 2->1->1 connection flow layout for AI compute processing.
|
27 |
+
Clean vertical stacking of inputs that merge into compute, then flow to output.
|
28 |
+
-->
|
29 |
+
|
30 |
+
<Container flexDirection="row" alignItems="center" gap={12}>
|
31 |
+
<!-- Left: Stacked Inputs -->
|
32 |
+
<Container flexDirection="column" alignItems="center" gap={6}>
|
33 |
+
<VideoInputBoxUIKit {compute} handleClick={() => onVideoInputBoxClick(compute)} />
|
34 |
+
<RobotInputBoxUIKit {compute} handleClick={() => onRobotInputBoxClick(compute)} />
|
35 |
+
</Container>
|
36 |
+
|
37 |
+
<!-- Arrow: Inputs to Compute -->
|
38 |
+
<StatusArrow
|
39 |
+
direction="right"
|
40 |
+
color={inputColor}
|
41 |
+
opacity={compute.hasSession ? 1 : 0.5}
|
42 |
+
/>
|
43 |
+
|
44 |
+
<!-- Center: Compute -->
|
45 |
+
<ComputeBoxUIKit {compute} />
|
46 |
+
|
47 |
+
<!-- Arrow: Compute to Output -->
|
48 |
+
<StatusArrow
|
49 |
+
direction="right"
|
50 |
+
color={outputColor}
|
51 |
+
opacity={compute.hasSession && compute.isRunning ? 1 : compute.hasSession ? 0.7 : 0.5}
|
52 |
+
/>
|
53 |
+
|
54 |
+
<!-- Right: Output -->
|
55 |
+
<ComputeOutputBoxUIKit {compute} handleClick={() => onRobotOutputBoxClick(compute)} />
|
56 |
+
</Container>
|
src/lib/components/3d/elements/compute/status/ComputeInputBoxUIKit.svelte
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
3 |
+
import { ICON } from "$lib/utils/icon";
|
4 |
+
import {
|
5 |
+
BaseStatusBox,
|
6 |
+
StatusHeader,
|
7 |
+
StatusContent,
|
8 |
+
StatusIndicator,
|
9 |
+
StatusButton
|
10 |
+
} from "$lib/components/3d/ui";
|
11 |
+
import { Container, SVG, Text } from "threlte-uikit";
|
12 |
+
|
13 |
+
interface Props {
|
14 |
+
compute: RemoteCompute;
|
15 |
+
handleClick?: () => void;
|
16 |
+
}
|
17 |
+
|
18 |
+
let { compute, handleClick }: Props = $props();
|
19 |
+
|
20 |
+
// Input theme color (green)
|
21 |
+
const inputColor = "rgb(34, 197, 94)";
|
22 |
+
</script>
|
23 |
+
|
24 |
+
<!--
|
25 |
+
@component
|
26 |
+
Compact input box showing the status of video and robot inputs for AI sessions.
|
27 |
+
Displays input connection information when session exists or connection prompt when disconnected.
|
28 |
+
-->
|
29 |
+
|
30 |
+
<BaseStatusBox
|
31 |
+
minWidth={120}
|
32 |
+
minHeight={80}
|
33 |
+
color={inputColor}
|
34 |
+
borderOpacity={compute.hasSession ? 0.8 : 0.4}
|
35 |
+
backgroundOpacity={0.2}
|
36 |
+
opacity={compute.hasSession ? 1 : 0.6}
|
37 |
+
onclick={handleClick}
|
38 |
+
>
|
39 |
+
{#if compute.hasSession && compute.inputConnections}
|
40 |
+
<!-- Active Input State -->
|
41 |
+
<StatusHeader
|
42 |
+
icon={ICON["icon-[material-symbols--download]"].svg}
|
43 |
+
text="INPUTS"
|
44 |
+
color={inputColor}
|
45 |
+
opacity={0.9}
|
46 |
+
fontSize={12}
|
47 |
+
/>
|
48 |
+
|
49 |
+
<!-- Camera Inputs -->
|
50 |
+
<StatusContent
|
51 |
+
title={`${Object.keys(compute.inputConnections.cameras).length} Cameras`}
|
52 |
+
subtitle="Joint States"
|
53 |
+
color="rgb(187, 247, 208)"
|
54 |
+
variant="primary"
|
55 |
+
/>
|
56 |
+
|
57 |
+
<!-- Active indicator -->
|
58 |
+
<StatusIndicator color={inputColor} />
|
59 |
+
{:else}
|
60 |
+
<!-- No Session State -->
|
61 |
+
<StatusHeader
|
62 |
+
icon={ICON["icon-[material-symbols--download]"].svg}
|
63 |
+
text="NO INPUTS"
|
64 |
+
color={inputColor}
|
65 |
+
opacity={0.7}
|
66 |
+
iconSize={12}
|
67 |
+
fontSize={12}
|
68 |
+
/>
|
69 |
+
|
70 |
+
<StatusContent
|
71 |
+
title="Setup Required"
|
72 |
+
color="rgb(134, 239, 172)"
|
73 |
+
variant="secondary"
|
74 |
+
/>
|
75 |
+
|
76 |
+
<StatusButton
|
77 |
+
text="Add Session"
|
78 |
+
icon={ICON["icon-[mdi--plus]"].svg}
|
79 |
+
color={inputColor}
|
80 |
+
backgroundOpacity={0.1}
|
81 |
+
textOpacity={0.7}
|
82 |
+
/>
|
83 |
+
{/if}
|
84 |
+
</BaseStatusBox>
|
src/lib/components/3d/elements/compute/status/ComputeOutputBoxUIKit.svelte
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
3 |
+
import { ICON } from "$lib/utils/icon";
|
4 |
+
import {
|
5 |
+
BaseStatusBox,
|
6 |
+
StatusHeader,
|
7 |
+
StatusContent,
|
8 |
+
StatusIndicator,
|
9 |
+
StatusButton
|
10 |
+
} from "$lib/components/3d/ui";
|
11 |
+
import { Container, SVG, Text } from "threlte-uikit";
|
12 |
+
|
13 |
+
interface Props {
|
14 |
+
compute: RemoteCompute;
|
15 |
+
handleClick?: () => void;
|
16 |
+
}
|
17 |
+
|
18 |
+
let { compute, handleClick }: Props = $props();
|
19 |
+
|
20 |
+
// Output theme color (blue)
|
21 |
+
const outputColor = "rgb(59, 130, 246)";
|
22 |
+
|
23 |
+
// Icons
|
24 |
+
// const exportIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTJsLTcgN3YtNEg1di02aDd2LTR6Ii8+PC9zdmc+";
|
25 |
+
// const robotIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTIgMTJjMC0yLjIxIDEuNzktNCA0LTRzNCAxLjc5IDQgNC0xLjc5IDQtNCA0LTQtMS43OS00LTR6TTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTAgMThDNy4zIDIwIDMuOCAxNi42IDMuOCAxMlM3LjMgNCA5IDRzNS4yIDMuNCA1LjIgOC02IDgtNSA4eiIvPjwvc3ZnPg==";
|
26 |
+
// const plusIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTkgMTNoLTZ2NmgtMnYtNkg1di0yaDZWNWgydjZoNnoiLz48L3N2Zz4=";
|
27 |
+
</script>
|
28 |
+
|
29 |
+
<!--
|
30 |
+
@component
|
31 |
+
Compact output box showing the status of robot outputs for AI sessions.
|
32 |
+
Displays output connection information when session exists or connection prompt when disconnected.
|
33 |
+
-->
|
34 |
+
|
35 |
+
<BaseStatusBox
|
36 |
+
minWidth={110}
|
37 |
+
minHeight={135}
|
38 |
+
color={outputColor}
|
39 |
+
borderOpacity={compute.hasSession && compute.isRunning ? 0.8 : 0.4}
|
40 |
+
backgroundOpacity={0.2}
|
41 |
+
opacity={compute.hasSession && compute.isRunning ? 1 : !compute.hasSession ? 0.4 : 0.6}
|
42 |
+
onclick={handleClick}
|
43 |
+
>
|
44 |
+
{#if compute.hasSession && compute.outputConnections}
|
45 |
+
<!-- Active Output State -->
|
46 |
+
<StatusHeader
|
47 |
+
icon={ICON["icon-[material-symbols--upload]"].svg}
|
48 |
+
text="OUTPUT"
|
49 |
+
color={outputColor}
|
50 |
+
opacity={0.9}
|
51 |
+
fontSize={12}
|
52 |
+
/>
|
53 |
+
|
54 |
+
<StatusContent
|
55 |
+
title={compute.isRunning ? "Active" : "Ready"}
|
56 |
+
subtitle="Commands"
|
57 |
+
color="rgb(191, 219, 254)"
|
58 |
+
variant="primary"
|
59 |
+
/>
|
60 |
+
|
61 |
+
<!-- Status indicator based on running state -->
|
62 |
+
<StatusIndicator
|
63 |
+
color={compute.isRunning ? outputColor : "rgb(245, 158, 11)"}
|
64 |
+
type={compute.isRunning ? "pulse" : "dot"}
|
65 |
+
/>
|
66 |
+
{:else}
|
67 |
+
<!-- No Session State -->
|
68 |
+
<StatusHeader
|
69 |
+
icon={ICON["icon-[material-symbols--upload]"].svg}
|
70 |
+
text="NO OUTPUT"
|
71 |
+
color={outputColor}
|
72 |
+
opacity={0.7}
|
73 |
+
iconSize={12}
|
74 |
+
fontSize={12}
|
75 |
+
/>
|
76 |
+
|
77 |
+
<StatusContent
|
78 |
+
title={!compute.hasSession ? 'Need Session' : 'Configure'}
|
79 |
+
color="rgb(147, 197, 253)"
|
80 |
+
variant="secondary"
|
81 |
+
/>
|
82 |
+
|
83 |
+
<StatusButton
|
84 |
+
text="Setup"
|
85 |
+
icon={ICON["icon-[mdi--plus]"].svg}
|
86 |
+
color={outputColor}
|
87 |
+
backgroundOpacity={0.1}
|
88 |
+
textOpacity={0.7}
|
89 |
+
/>
|
90 |
+
{/if}
|
91 |
+
</BaseStatusBox>
|
src/lib/components/3d/elements/compute/status/ComputeStatusBillboard.svelte
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T } from "@threlte/core";
|
3 |
+
import { Billboard, interactivity } from "@threlte/extras";
|
4 |
+
import { Root, Container } from "threlte-uikit";
|
5 |
+
import ComputeConnectionFlowBoxUIKit from "./ComputeConnectionFlowBoxUIKit.svelte";
|
6 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
7 |
+
|
8 |
+
interface Props {
|
9 |
+
compute: RemoteCompute;
|
10 |
+
offset?: number;
|
11 |
+
visible?: boolean;
|
12 |
+
onVideoInputBoxClick: (compute: RemoteCompute) => void;
|
13 |
+
onRobotInputBoxClick: (compute: RemoteCompute) => void;
|
14 |
+
onRobotOutputBoxClick: (compute: RemoteCompute) => void;
|
15 |
+
}
|
16 |
+
|
17 |
+
let {
|
18 |
+
compute,
|
19 |
+
offset = 10,
|
20 |
+
visible = true,
|
21 |
+
onVideoInputBoxClick,
|
22 |
+
onRobotInputBoxClick,
|
23 |
+
onRobotOutputBoxClick
|
24 |
+
}: Props = $props();
|
25 |
+
|
26 |
+
interactivity();
|
27 |
+
</script>
|
28 |
+
|
29 |
+
<T.Group
|
30 |
+
onclick={(e) => e.stopPropagation()}
|
31 |
+
position.z={0.4}
|
32 |
+
padding={10}
|
33 |
+
rotation={[-Math.PI / 2, 0, 0]}
|
34 |
+
scale={[0.1, 0.1, 0.1]}
|
35 |
+
pointerEvents="listener"
|
36 |
+
{visible}
|
37 |
+
>
|
38 |
+
<Billboard>
|
39 |
+
<Root name={`compute-status-billboard-${compute.id}`}>
|
40 |
+
<Container
|
41 |
+
width="100%"
|
42 |
+
height="100%"
|
43 |
+
alignItems="center"
|
44 |
+
justifyContent="center"
|
45 |
+
padding={20}
|
46 |
+
>
|
47 |
+
<ComputeConnectionFlowBoxUIKit
|
48 |
+
{compute}
|
49 |
+
{onVideoInputBoxClick}
|
50 |
+
{onRobotInputBoxClick}
|
51 |
+
{onRobotOutputBoxClick}
|
52 |
+
/>
|
53 |
+
</Container>
|
54 |
+
</Root>
|
55 |
+
</Billboard>
|
56 |
+
</T.Group>
|
src/lib/components/3d/elements/compute/status/RobotInputBoxUIKit.svelte
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { ICON } from "$lib/utils/icon";
|
3 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
4 |
+
import { BaseStatusBox, StatusHeader, StatusContent, StatusIndicator, StatusButton } from "$lib/components/3d/ui";
|
5 |
+
|
6 |
+
interface Props {
|
7 |
+
compute: RemoteCompute;
|
8 |
+
handleClick?: () => void;
|
9 |
+
}
|
10 |
+
|
11 |
+
let { compute, handleClick }: Props = $props();
|
12 |
+
|
13 |
+
// Robot theme color (orange - consistent with robot system)
|
14 |
+
const robotColor = "rgb(245, 158, 11)";
|
15 |
+
|
16 |
+
// Icons
|
17 |
+
// const robotIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTIgMTJjMC0yLjIxIDEuNzktNCA0LTRzNCAxLjc5IDQgNC0xLjc5IDQtNCA0LTQtMS43OS00LTR6TTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTAgMThDNy4zIDIwIDMuOCAxNi42IDMuOCAxMlM3LjMgNCA5IDRzNS4yIDMuNCA1LjIgOC02IDgtNSA4eiIvPjwvc3ZnPg==";
|
18 |
+
// const robotOffIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMjAuNzMgMTgsNjYgMy4yN0wyIDQuNzNsMTQuMTQgMTQuMTRDMTcuOSAxNS42MiAxNiAxNy40OSAxNiAxOS44VjIxaDJWMTMuNWgzdjIuNWgydjE1LjJDMjIgMTkuNCAyMi44IDE5IDE0LjE4IDEwLjE4TDEwIDZIMTJWNEgxMlY1aDNjMC0yLjc2IDEuMjQtNSA0LTVzNCAxLjI0IDQgNS0yLjI0IDUtNSA1SDEzdjFIOFY3SDZ2Mm0xIDIuNzNMMjAgMjAuNzNsLS4wMSAwIi8+PC9zdmc+";
|
19 |
+
// const robotOutlineIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTIgMmMtNS41NyAwLTEwIDQuNDMtMTAgMTBzNC40MyAxMCAxMCAxMCAxMC00LjQzIDEwLTEwUzE3LjU3IDIgMTIgMnptMCAxOGMtNC40MSAwLTgtMy41OS04LThzMy41OS04IDgtOCA4IDMuNTkgOCA4LTMuNTkgOC04IDh6bTAgLTEyYy0yLjIxIDAtNCAuNzktNCA0czEuNzkgNCA0IDQgNC0xLjc5IDQtNC0xLjc5LTQtNC00em0wIDZjLTEuMTEgMC0yLS44OS0yLTJzLjg5LTIgMi0yIDIgLjg5IDIgMi0uODkgMi0yIDJ6Ii8+PC9zdmc+";
|
20 |
+
// const formatListNumberedIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMiAxN2gybC0xLTFjLjU1IDAgMS0uNDUgMS0xcy0uNDUtMS0xLTEtMSAuNDUtMSAxaC0yYzAtMS4xLjktMiAyLTJzMiAuOSAyIDItLjkgMi0yIDJ6bS0yIDJoNHYySDJabTUtMTJoMTVoMlY5SDdabTAgNGgxNXYySHdtNS0xMEgyTDZ2MmgxdjJIN3ptMC00aDJsLTEtMXYtMmgxVjFINHYyaDJabTAtMUgxWiIvPjwvc3ZnPg==";
|
21 |
+
</script>
|
22 |
+
|
23 |
+
<!--
|
24 |
+
@component
|
25 |
+
Compact robot input box showing the status of robot joint states input for AI sessions.
|
26 |
+
Displays robot connection information when session exists or connection prompt when disconnected.
|
27 |
+
-->
|
28 |
+
|
29 |
+
<BaseStatusBox
|
30 |
+
minWidth={100}
|
31 |
+
minHeight={65}
|
32 |
+
color={robotColor}
|
33 |
+
borderOpacity={0.6}
|
34 |
+
backgroundOpacity={0.2}
|
35 |
+
opacity={compute.hasSession ? 1 : 0.6}
|
36 |
+
onclick={handleClick}
|
37 |
+
>
|
38 |
+
{#if compute.hasSession && compute.inputConnections}
|
39 |
+
<!-- Active Robot Input State -->
|
40 |
+
<StatusHeader
|
41 |
+
icon={ICON["icon-[ix--robotic-arm]"].svg}
|
42 |
+
text="ROBOT"
|
43 |
+
color={robotColor}
|
44 |
+
opacity={0.9}
|
45 |
+
fontSize={11}
|
46 |
+
/>
|
47 |
+
|
48 |
+
<StatusContent
|
49 |
+
title="Joint States"
|
50 |
+
subtitle="6 DOF Robot"
|
51 |
+
color="rgb(254, 215, 170)"
|
52 |
+
variant="primary"
|
53 |
+
/>
|
54 |
+
|
55 |
+
<!-- Connected status -->
|
56 |
+
<StatusIndicator color={robotColor} />
|
57 |
+
{:else}
|
58 |
+
<!-- No Session State -->
|
59 |
+
<StatusHeader
|
60 |
+
icon={ICON["icon-[ix--robotic-arm]"].svg}
|
61 |
+
text="NO ROBOT"
|
62 |
+
color={robotColor}
|
63 |
+
opacity={0.7}
|
64 |
+
fontSize={11}
|
65 |
+
/>
|
66 |
+
|
67 |
+
<StatusContent
|
68 |
+
title="Setup Robot"
|
69 |
+
color="rgb(254, 215, 170)"
|
70 |
+
variant="secondary"
|
71 |
+
/>
|
72 |
+
|
73 |
+
<StatusButton
|
74 |
+
icon={ICON["icon-[mdi--plus]"].svg}
|
75 |
+
text="Add"
|
76 |
+
color={robotColor}
|
77 |
+
backgroundOpacity={0.1}
|
78 |
+
textOpacity={0.7}
|
79 |
+
/>
|
80 |
+
{/if}
|
81 |
+
</BaseStatusBox>
|
src/lib/components/3d/elements/compute/status/RobotOutputBoxUIKit.svelte
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
3 |
+
import { ICON } from "$lib/utils/icon";
|
4 |
+
import { BaseStatusBox, StatusHeader, StatusContent, StatusIndicator, StatusButton } from "$lib/components/3d/ui";
|
5 |
+
|
6 |
+
interface Props {
|
7 |
+
compute: RemoteCompute;
|
8 |
+
handleClick?: () => void;
|
9 |
+
}
|
10 |
+
|
11 |
+
let { compute, handleClick }: Props = $props();
|
12 |
+
|
13 |
+
// Output theme color (blue)
|
14 |
+
const outputColor = "rgb(59, 130, 246)";
|
15 |
+
</script>
|
16 |
+
|
17 |
+
<!--
|
18 |
+
@component
|
19 |
+
Robot output box showing the status of robot joint commands output from AI sessions.
|
20 |
+
Displays robot command output information when session exists or connection prompt when disconnected.
|
21 |
+
-->
|
22 |
+
|
23 |
+
<BaseStatusBox
|
24 |
+
color={outputColor}
|
25 |
+
borderOpacity={0.6}
|
26 |
+
backgroundOpacity={0.2}
|
27 |
+
opacity={compute.hasSession && compute.isRunning ? 1 : !compute.hasSession ? 0.4 : 0.6}
|
28 |
+
onclick={handleClick}
|
29 |
+
>
|
30 |
+
{#if compute.hasSession && compute.outputConnections}
|
31 |
+
<!-- Active Robot Output State -->
|
32 |
+
<StatusHeader
|
33 |
+
icon={ICON["icon-[ix--robotic-arm]"].svg}
|
34 |
+
text="COMMANDS"
|
35 |
+
color={outputColor}
|
36 |
+
opacity={0.9}
|
37 |
+
/>
|
38 |
+
|
39 |
+
<StatusContent
|
40 |
+
title={compute.isRunning ? "AI Commands Active" : "Session Ready"}
|
41 |
+
subtitle="Motor Control"
|
42 |
+
color="rgb(191, 219, 254)"
|
43 |
+
variant="primary"
|
44 |
+
/>
|
45 |
+
|
46 |
+
<!-- Status indicator based on running state -->
|
47 |
+
{#if compute.isRunning}
|
48 |
+
<!-- Active pulse indicator -->
|
49 |
+
<StatusIndicator color={outputColor} type="pulse" />
|
50 |
+
{:else}
|
51 |
+
<!-- Ready but not running indicator -->
|
52 |
+
<StatusIndicator color="rgb(245, 158, 11)" />
|
53 |
+
{/if}
|
54 |
+
{:else}
|
55 |
+
<!-- No Session State -->
|
56 |
+
<StatusHeader
|
57 |
+
icon={ICON["icon-[ix--robotic-arm]"].svg}
|
58 |
+
text="NO OUTPUT"
|
59 |
+
color={outputColor}
|
60 |
+
opacity={0.7}
|
61 |
+
/>
|
62 |
+
|
63 |
+
<StatusContent
|
64 |
+
title={!compute.hasSession ? 'Need Session' : 'Click to Configure'}
|
65 |
+
color="rgb(147, 197, 253)"
|
66 |
+
variant="secondary"
|
67 |
+
/>
|
68 |
+
|
69 |
+
<StatusButton
|
70 |
+
icon={ICON["icon-[ix--robotic-arm]"].svg}
|
71 |
+
text="Setup Output"
|
72 |
+
color={outputColor}
|
73 |
+
backgroundOpacity={0.1}
|
74 |
+
textOpacity={0.7}
|
75 |
+
/>
|
76 |
+
{/if}
|
77 |
+
</BaseStatusBox>
|
src/lib/components/3d/elements/compute/status/VideoInputBoxUIKit.svelte
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { Text } from "threlte-uikit";
|
3 |
+
import { ICON } from "$lib/utils/icon";
|
4 |
+
import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";
|
5 |
+
import { BaseStatusBox, StatusHeader, StatusContent, StatusIndicator, StatusButton } from "$lib/components/3d/ui";
|
6 |
+
|
7 |
+
interface Props {
|
8 |
+
compute: RemoteCompute;
|
9 |
+
handleClick?: () => void;
|
10 |
+
}
|
11 |
+
|
12 |
+
let { compute, handleClick }: Props = $props();
|
13 |
+
|
14 |
+
// Input theme color (green)
|
15 |
+
const inputColor = "rgb(34, 197, 94)";
|
16 |
+
|
17 |
+
// Icons
|
18 |
+
// const videoIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTcgMTAuNVY3YTEgMSAwIDAgMC0xLTFINGExIDEgMCAwIDAtMSAxdjEwYTEgMSAwIDAgMCAxIDFoMTJhMSAxIDAgMCAwIDEtMXYtMy41bDQgNHYtMTF6Ii8+PC9zdmc+";
|
19 |
+
// const videoOffIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMy4yNyAyTDIgMy4yN2wxLjY4IDEuNjhDMy4yNiA1LjMgMyA1LjY0IDMgNnYxMmMwIC41NS40NSAxIDEgMWg4Yy4zNiAwIC42OC0uMTUuOTItLjM5bDEuNzMgMS43M0wxNiAyMC43M0wxOC43MyAxOGwtLjg5LS44OUwyMCAxNXYtMWwtNC05VjVjMC0uNTUtLjQ1LTEtMS0xSDlsLTEuMTYtMS4xNkMxMS41NSAyIDEwIDQuMjcgMTAgNHYxTDMuMjcgMnoiLz48L3N2Zz4=";
|
20 |
+
// const videoPlusIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTMgMTRoMXYzaDN2MWgtM3YzaC0xdi0zaC0zdi0xaDN2LTN6bTUgMGwtNS0zdjZsNS0zek0xMSAzSDNDMS45IDMgMSAzLjkgMSA1djEwYzAgMS4xLjkgMiAyIDJoOGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yeiIvPjwvc3ZnPg==";
|
21 |
+
// const cameraMultipleIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjMDAwIiBkPSJNNCAyaDZsMS41IDEuNUg2djJoOGwtLjUtLjVIMThsMS41IDEuNWgzYzEuMSAwIDIgLjkgMiAydjEwYzAgMS4xLS45IDItMiAySDZjLTEuMSAwLTItLjktMi0yVjRjMC0xLjEuOS0yIDItMnptNSA2Yy0xLjExIDAtMi4wOC45LTIuMDggMnMuOTcgMiAyLjA4IDIgMi4wOC0uOSAyLjA4LTItLjk3LTItMi4wOC0yeiIvPjwvc3ZnPg==";
|
22 |
+
</script>
|
23 |
+
|
24 |
+
<!--
|
25 |
+
@component
|
26 |
+
Compact video input box showing the status of camera video streams for AI sessions.
|
27 |
+
Displays video connection information when session exists or connection prompt when disconnected.
|
28 |
+
-->
|
29 |
+
|
30 |
+
<BaseStatusBox
|
31 |
+
minWidth={100}
|
32 |
+
minHeight={65}
|
33 |
+
color={inputColor}
|
34 |
+
borderOpacity={0.6}
|
35 |
+
backgroundOpacity={0.2}
|
36 |
+
opacity={compute.hasSession ? 1 : 0.6}
|
37 |
+
onclick={handleClick}
|
38 |
+
>
|
39 |
+
{#if compute.hasSession && compute.inputConnections}
|
40 |
+
<!-- Active Video Input State -->
|
41 |
+
<StatusHeader
|
42 |
+
icon={ICON["icon-[mdi--video]"].svg}
|
43 |
+
text="VIDEO"
|
44 |
+
color={inputColor}
|
45 |
+
opacity={0.9}
|
46 |
+
fontSize={11}
|
47 |
+
/>
|
48 |
+
|
49 |
+
<!-- Camera Streams -->
|
50 |
+
<StatusContent
|
51 |
+
title={`${Object.keys(compute.inputConnections.cameras).length} Cameras`}
|
52 |
+
color="rgb(187, 247, 208)"
|
53 |
+
variant="primary"
|
54 |
+
/>
|
55 |
+
|
56 |
+
<!-- Connected status -->
|
57 |
+
<StatusIndicator color={inputColor} />
|
58 |
+
{:else}
|
59 |
+
<!-- No Session State -->
|
60 |
+
<StatusHeader
|
61 |
+
icon={ICON["icon-[mdi--video-off]"].svg}
|
62 |
+
text="NO VIDEO"
|
63 |
+
color={inputColor}
|
64 |
+
opacity={0.7}
|
65 |
+
fontSize={11}
|
66 |
+
/>
|
67 |
+
|
68 |
+
<StatusContent
|
69 |
+
title="Setup Video"
|
70 |
+
color="rgb(134, 239, 172)"
|
71 |
+
variant="secondary"
|
72 |
+
/>
|
73 |
+
|
74 |
+
<StatusButton
|
75 |
+
icon={ICON["icon-[mdi--plus]"].svg}
|
76 |
+
text="Add"
|
77 |
+
color={inputColor}
|
78 |
+
backgroundOpacity={0.1}
|
79 |
+
textOpacity={0.7}
|
80 |
+
/>
|
81 |
+
{/if}
|
82 |
+
</BaseStatusBox>
|
src/lib/components/3d/elements/robot/RobotGridItem.svelte
ADDED
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { T } from "@threlte/core";
|
3 |
+
import { Group } from "three";
|
4 |
+
import { getRootLinks } from "@/components/3d/elements/robot/URDF/utils/UrdfParser";
|
5 |
+
import UrdfLink from "@/components/3d/elements/robot/URDF/primitives/UrdfLink.svelte";
|
6 |
+
import RobotStatusBillboard from "@/components/3d/elements/robot/status/RobotStatusBillboard.svelte";
|
7 |
+
import type { Robot } from "$lib/elements/robot/Robot.svelte.js";
|
8 |
+
import { createUrdfRobot } from "$lib/elements/robot/createRobot.svelte";
|
9 |
+
import { interactivity, type IntersectionEvent, useCursor } from "@threlte/extras";
|
10 |
+
import type { RobotUrdfConfig } from "$lib/types/urdf";
|
11 |
+
import { onMount } from 'svelte';
|
12 |
+
import type IUrdfRobot from '@/components/3d/elements/robot/URDF/interfaces/IUrdfRobot.js';
|
13 |
+
import { ROBOT_CONFIG } from '$lib/elements/robot/config.js';
|
14 |
+
|
15 |
+
interface Props {
|
16 |
+
robot: Robot;
|
17 |
+
onCameraMove: (ref: any) => void;
|
18 |
+
onInputBoxClick: (robot: Robot) => void;
|
19 |
+
onRobotBoxClick: (robot: Robot) => void;
|
20 |
+
onOutputBoxClick: (robot: Robot) => void;
|
21 |
+
}
|
22 |
+
|
23 |
+
let ref = $state<Group | undefined>(undefined);
|
24 |
+
|
25 |
+
let { robot = $bindable(), onCameraMove, onInputBoxClick, onRobotBoxClick, onOutputBoxClick }: Props = $props();
|
26 |
+
|
27 |
+
let urdfRobotState = $state<IUrdfRobot | null>(null);
|
28 |
+
let lastJointValues = $state<Record<string, number>>({});
|
29 |
+
|
30 |
+
onMount(async () => {
|
31 |
+
const urdfConfig: RobotUrdfConfig = {
|
32 |
+
urdfUrl: "/robots/so-100/so_arm100.urdf"
|
33 |
+
};
|
34 |
+
|
35 |
+
try {
|
36 |
+
const UrdfRobotState = await createUrdfRobot(urdfConfig);
|
37 |
+
urdfRobotState = UrdfRobotState.urdfRobot;
|
38 |
+
} catch (error) {
|
39 |
+
console.error('Failed to load URDF robot:', error);
|
40 |
+
}
|
41 |
+
});
|
42 |
+
|
43 |
+
// Sync joint values from Robot to URDF joints with optimized updates
|
44 |
+
$effect(() => {
|
45 |
+
if (!urdfRobotState) return;
|
46 |
+
if (robot.jointArray.length === 0) return;
|
47 |
+
|
48 |
+
// Check if this is the initial sync (no previous values recorded)
|
49 |
+
const isInitialSync = Object.keys(lastJointValues).length === 0;
|
50 |
+
|
51 |
+
// Check if any joint values have actually changed (using config threshold)
|
52 |
+
const threshold = isInitialSync ? 0 : ROBOT_CONFIG.performance.uiUpdateThreshold;
|
53 |
+
const hasSignificantChanges = isInitialSync || robot.jointArray.some(joint =>
|
54 |
+
Math.abs((lastJointValues[joint.name] || 0) - joint.value) > threshold
|
55 |
+
);
|
56 |
+
if (!hasSignificantChanges) return;
|
57 |
+
|
58 |
+
// Batch update all joints that have changed (or all joints on initial sync)
|
59 |
+
let updatedCount = 0;
|
60 |
+
robot.jointArray.forEach(joint => {
|
61 |
+
if (isInitialSync || Math.abs((lastJointValues[joint.name] || 0) - joint.value) > threshold) {
|
62 |
+
lastJointValues[joint.name] = joint.value;
|
63 |
+
const urdfJoint = findUrdfJoint(urdfRobotState, joint.name);
|
64 |
+
if (urdfJoint) {
|
65 |
+
// Initialize rotation array if it doesn't exist
|
66 |
+
if (!urdfJoint.rotation) {
|
67 |
+
urdfJoint.rotation = [0, 0, 0];
|
68 |
+
}
|
69 |
+
|
70 |
+
// Use the Robot's conversion method for proper coordinate mapping
|
71 |
+
const radians = robot.convertNormalizedToUrdfRadians(joint.name, joint.value);
|
72 |
+
const axis = urdfJoint.axis_xyz || [0, 0, 1];
|
73 |
+
|
74 |
+
// Reset rotation and apply to the appropriate axis
|
75 |
+
urdfJoint.rotation = [0, 0, 0];
|
76 |
+
for (let i = 0; i < 3; i++) {
|
77 |
+
if (Math.abs(axis[i]) > 0.001) {
|
78 |
+
urdfJoint.rotation[i] = radians * axis[i];
|
79 |
+
}
|
80 |
+
}
|
81 |
+
updatedCount++;
|
82 |
+
}
|
83 |
+
}
|
84 |
+
});
|
85 |
+
});
|
86 |
+
|
87 |
+
function findUrdfJoint(robot: Robot, jointName: string): any {
|
88 |
+
// Search through the robot's joints array
|
89 |
+
if (robot.joints && Array.isArray(robot.joints)) {
|
90 |
+
for (const joint of robot.joints) {
|
91 |
+
if (joint.name === jointName) {
|
92 |
+
return joint;
|
93 |
+
}
|
94 |
+
}
|
95 |
+
}
|
96 |
+
return null;
|
97 |
+
}
|
98 |
+
|
99 |
+
const { onPointerEnter, onPointerLeave, hovering } = useCursor();
|
100 |
+
interactivity();
|
101 |
+
|
102 |
+
let isToggled = $state(false);
|
103 |
+
|
104 |
+
function handleClick(event: IntersectionEvent<MouseEvent>) {
|
105 |
+
event.stopPropagation();
|
106 |
+
isToggled = !isToggled;
|
107 |
+
}
|
108 |
+
|
109 |
+
</script>
|
110 |
+
|
111 |
+
<T.Group
|
112 |
+
bind:ref
|
113 |
+
position.x={robot.position.x}
|
114 |
+
position.y={robot.position.y}
|
115 |
+
position.z={robot.position.z}
|
116 |
+
scale={[10, 10, 10]}
|
117 |
+
rotation={[-Math.PI / 2, 0, 0]}
|
118 |
+
>
|
119 |
+
<T.Group
|
120 |
+
onpointerenter={onPointerEnter}
|
121 |
+
onpointerleave={onPointerLeave}
|
122 |
+
onclick={handleClick}
|
123 |
+
>
|
124 |
+
{#if urdfRobotState}
|
125 |
+
{#each getRootLinks(urdfRobotState) as link}
|
126 |
+
<UrdfLink
|
127 |
+
robot={urdfRobotState}
|
128 |
+
{link}
|
129 |
+
textScale={0.2}
|
130 |
+
showName={$hovering || isToggled}
|
131 |
+
showVisual={true}
|
132 |
+
showCollision={false}
|
133 |
+
visualColor={robot.connectionStatus.isConnected ? "#10b981" : "#6b7280"}
|
134 |
+
visualOpacity={$hovering || isToggled ? 0.4 : 1.0}
|
135 |
+
collisionOpacity={1.0}
|
136 |
+
collisionColor="#813d9c"
|
137 |
+
jointNames={$hovering}
|
138 |
+
joints={$hovering}
|
139 |
+
jointColor="#62a0ea"
|
140 |
+
jointIndicatorColor="#f66151"
|
141 |
+
nameHeight={0.1}
|
142 |
+
showLine={$hovering || isToggled}
|
143 |
+
opacity={1}
|
144 |
+
isInteractive={false}
|
145 |
+
/>
|
146 |
+
{/each}
|
147 |
+
{:else}
|
148 |
+
<!-- Fallback simple representation while URDF loads -->
|
149 |
+
<T.Mesh>
|
150 |
+
<T.BoxGeometry args={[0.1, 0.1, 0.1]} />
|
151 |
+
<T.MeshStandardMaterial
|
152 |
+
color={robot.connectionStatus.isConnected ? "#10b981" : "#6b7280"}
|
153 |
+
opacity={$hovering ? 0.8 : 1.0}
|
154 |
+
transparent
|
155 |
+
/>
|
156 |
+
</T.Mesh>
|
157 |
+
{/if}
|
158 |
+
</T.Group>
|
159 |
+
|
160 |
+
|
161 |
+
<RobotStatusBillboard
|
162 |
+
{robot}
|
163 |
+
onInputBoxClick={onInputBoxClick}
|
164 |
+
onRobotBoxClick={onRobotBoxClick}
|
165 |
+
onOutputBoxClick={onOutputBoxClick}
|
166 |
+
visible={isToggled}
|
167 |
+
/>
|
168 |
+
|
169 |
+
</T.Group>
|
src/lib/components/3d/elements/robot/Robots.svelte
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { robotManager } from "$lib/elements/robot/RobotManager.svelte.js";
|
3 |
+
import { onMount, onDestroy } from "svelte";
|
4 |
+
import InputConnectionModal from "@/components/3d/elements/robot/modal/InputConnectionModal.svelte";
|
5 |
+
import OutputConnectionModal from "@/components/3d/elements/robot/modal/OutputConnectionModal.svelte";
|
6 |
+
import ManualControlSheet from "@/components/3d/elements/robot/modal/ManualControlSheet.svelte";
|
7 |
+
import type { Robot } from "$lib/elements/robot/Robot.svelte.js";
|
8 |
+
import { generateName } from "$lib/utils/generateName";
|
9 |
+
import RobotGridItem from "@/components/3d/elements/robot/RobotGridItem.svelte";
|
10 |
+
|
11 |
+
interface Props {
|
12 |
+
workspaceId: string;
|
13 |
+
}
|
14 |
+
let {workspaceId}: Props = $props();
|
15 |
+
|
16 |
+
let isInputModalOpen = $state(false);
|
17 |
+
let isOutputModalOpen = $state(false);
|
18 |
+
let isManualControlSheetOpen = $state(false);
|
19 |
+
let selectedRobot = $state<Robot | null>(null);
|
20 |
+
|
21 |
+
function onInputBoxClick(robot: Robot) {
|
22 |
+
selectedRobot = robot;
|
23 |
+
isInputModalOpen = true;
|
24 |
+
}
|
25 |
+
|
26 |
+
function onRobotBoxClick(robot: Robot) {
|
27 |
+
selectedRobot = robot;
|
28 |
+
isManualControlSheetOpen = true;
|
29 |
+
}
|
30 |
+
|
31 |
+
function onOutputBoxClick(robot: Robot) {
|
32 |
+
selectedRobot = robot;
|
33 |
+
isOutputModalOpen = true;
|
34 |
+
}
|
35 |
+
|
36 |
+
onMount(async () => {
|
37 |
+
async function createRobot() {
|
38 |
+
try {
|
39 |
+
const robotId = generateName();
|
40 |
+
await robotManager.createSO100Robot(robotId, {
|
41 |
+
x: 0,
|
42 |
+
y: 0,
|
43 |
+
z: 0
|
44 |
+
});
|
45 |
+
} catch (error) {
|
46 |
+
console.error('Failed to create robot:', error);
|
47 |
+
}
|
48 |
+
}
|
49 |
+
|
50 |
+
if (robotManager.robots.length === 0) {
|
51 |
+
await createRobot();
|
52 |
+
}
|
53 |
+
});
|
54 |
+
|
55 |
+
onDestroy(() => {
|
56 |
+
// Clean up robots and unlock servos for safety
|
57 |
+
console.log('🧹 Cleaning up robots and unlocking servos...');
|
58 |
+
robotManager.destroy().then(() => {
|
59 |
+
console.log('✅ Cleanup completed successfully');
|
60 |
+
}).catch((error) => {
|
61 |
+
console.error('❌ Error during cleanup:', error);
|
62 |
+
});
|
63 |
+
});
|
64 |
+
</script>
|
65 |
+
|
66 |
+
{#each robotManager.robots as robot (robot.id)}
|
67 |
+
<RobotGridItem
|
68 |
+
{robot}
|
69 |
+
onCameraMove={() => {}}
|
70 |
+
onInputBoxClick={onInputBoxClick}
|
71 |
+
onRobotBoxClick={onRobotBoxClick}
|
72 |
+
onOutputBoxClick={onOutputBoxClick}
|
73 |
+
/>
|
74 |
+
{/each}
|
75 |
+
|
76 |
+
<!-- Connection Modals -->
|
77 |
+
{#if selectedRobot}
|
78 |
+
<InputConnectionModal bind:open={isInputModalOpen} robot={selectedRobot} {workspaceId} />
|
79 |
+
<OutputConnectionModal bind:open={isOutputModalOpen} robot={selectedRobot} {workspaceId} />
|
80 |
+
<ManualControlSheet bind:open={isManualControlSheetOpen} robot={selectedRobot} {workspaceId} />
|
81 |
+
{/if}
|
src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfBox.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
export default interface IUrdfBox {
|
2 |
+
size: [x: number, y: number, z: number];
|
3 |
+
}
|
src/lib/components/3d/elements/robot/URDF/interfaces/IUrdfCylinder.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default interface IUrdfCylinder {
|
2 |
+
radius: number;
|
3 |
+
length: number;
|
4 |
+
}
|