NERDDISCO commited on
Commit
b664dbe
Β·
1 Parent(s): 2aa0afa

feat: move "src/lerobot/web" to "packages/web/src"

Browse files
Files changed (50) hide show
  1. .changeset/config.json +11 -0
  2. .github/CONTRIBUTING.md +225 -0
  3. .github/workflows/hf-space.yml +24 -0
  4. .github/workflows/release.yml +46 -0
  5. CHANGESET_GUIDE.md +87 -0
  6. README.md +31 -303
  7. package.json +8 -0
  8. packages/web/CHANGELOG.md +7 -0
  9. packages/web/README.md +55 -0
  10. packages/web/package.json +54 -0
  11. {src/lerobot/web β†’ packages/web/src}/calibrate.ts +0 -0
  12. {src/lerobot/web β†’ packages/web/src}/find_port.ts +0 -0
  13. packages/web/src/index.ts +45 -0
  14. {src/lerobot/web β†’ packages/web/src}/robots/so100_config.ts +0 -0
  15. {src/lerobot/web β†’ packages/web/src}/teleoperate.ts +0 -0
  16. {src/lerobot/web β†’ packages/web/src}/types/calibration.ts +0 -0
  17. {src/lerobot/web β†’ packages/web/src}/types/port-discovery.ts +0 -0
  18. {src/lerobot/web β†’ packages/web/src}/types/robot-config.ts +0 -0
  19. {src/lerobot/web β†’ packages/web/src}/types/robot-connection.ts +0 -0
  20. {src/lerobot/web β†’ packages/web/src}/types/teleoperation.ts +0 -0
  21. {src/lerobot/web β†’ packages/web/src}/utils/motor-calibration.ts +0 -0
  22. {src/lerobot/web β†’ packages/web/src}/utils/motor-communication.ts +0 -0
  23. {src/lerobot/web β†’ packages/web/src}/utils/serial-port-wrapper.ts +2 -0
  24. {src/lerobot/web β†’ packages/web/src}/utils/sign-magnitude.ts +0 -0
  25. {src/lerobot/web β†’ packages/web/src}/utils/sts3215-protocol.ts +0 -0
  26. packages/web/tsconfig.build.json +20 -0
  27. pnpm-lock.yaml +596 -0
  28. pnpm-workspace.yaml +2 -0
  29. src/demo/App.tsx +1 -1
  30. src/demo/components/CalibrationPanel.tsx +5 -5
  31. src/demo/components/PortManager.tsx +3 -3
  32. src/demo/components/TeleoperationPanel.tsx +3 -3
  33. src/demo/pages/Home.tsx +3 -3
  34. src/lerobot/node/calibrate.ts +0 -246
  35. src/lerobot/node/common/calibration.ts +0 -694
  36. src/lerobot/node/common/so100_config.ts +0 -137
  37. src/lerobot/node/find_port.ts +0 -125
  38. src/lerobot/node/robots/robot.ts +0 -199
  39. src/lerobot/node/robots/so100_follower.ts +0 -614
  40. src/lerobot/node/teleoperate.ts +0 -316
  41. src/lerobot/node/teleoperators/so100_leader.ts +0 -41
  42. src/lerobot/node/teleoperators/teleoperator.ts +0 -148
  43. src/lerobot/node/types/calibration.ts +0 -45
  44. src/lerobot/node/types/robot-config.ts +0 -23
  45. src/lerobot/node/types/teleoperation.ts +0 -17
  46. src/lerobot/node/types/teleoperator-config.ts +0 -11
  47. src/lerobot/node/utils/constants.ts +0 -48
  48. src/lerobot/node/utils/keyboard-teleop.ts +0 -284
  49. src/lerobot/web/robots/robot.ts +0 -171
  50. vite.config.ts +8 -0
.changeset/config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "ignore": []
11
+ }
.github/CONTRIBUTING.md ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to lerobot.js
2
+
3
+ Thanks for your interest in contributing! This guide will help you get started.
4
+
5
+ ## πŸš€ Quick Start
6
+
7
+ ```bash
8
+ # Clone and setup
9
+ git clone https://github.com/timpietrusky/lerobot.js.git
10
+ cd lerobot.js
11
+ pnpm install
12
+
13
+ # Run demo
14
+ pnpm dev
15
+
16
+ # Build packages
17
+ pnpm --filter "@lerobot/web" run build
18
+ ```
19
+
20
+ ## πŸ“¦ Package Structure
21
+
22
+ - **`packages/web/`** - Browser package (`@lerobot/web` on npm)
23
+ - **`src/demo/`** - Demo application (deployed to HF Spaces)
24
+ - **`src/cli/`** - Node.js CLI tool
25
+ - **`src/lerobot/node/`** - Node.js library
26
+
27
+ ## πŸ”„ Making Changes
28
+
29
+ ### 1. Development Workflow
30
+
31
+ ```bash
32
+ # Make your changes to packages/web/ or other code
33
+ # Test your changes
34
+ pnpm dev # for demo
35
+ pnpm --filter "@lerobot/web" run build # for package
36
+
37
+ # Run any relevant tests
38
+ ```
39
+
40
+ ### 2. Creating a Changeset
41
+
42
+ **For any changes to `packages/web/`:**
43
+
44
+ ```bash
45
+ # Describe your changes
46
+ pnpm changeset
47
+ ```
48
+
49
+ - **Select package**: `@lerobot/web`
50
+ - **Version type**:
51
+ - `patch` (0.1.1 β†’ 0.1.2) - Bug fixes
52
+ - `minor` (0.1.1 β†’ 0.2.0) - New features
53
+ - `major` (0.1.1 β†’ 1.0.0) - Breaking changes
54
+ - **Summary**: Clear description for changelog
55
+
56
+ ### 3. Submit Pull Request
57
+
58
+ ```bash
59
+ git add .
60
+ git commit -m "feat: your change description"
61
+ git push origin your-branch
62
+ ```
63
+
64
+ Create a PR with:
65
+
66
+ - Clear description of changes
67
+ - Reference any related issues
68
+ - Include changeset if modifying `@lerobot/web`
69
+
70
+ ## πŸš€ Release Process
71
+
72
+ ### Automated Releases (Recommended)
73
+
74
+ 1. **Changeset added** β†’ PR merged to `main`
75
+ 2. **GitHub Actions** creates Release PR automatically
76
+ 3. **Maintainer merges** Release PR
77
+ 4. **Package published** to npm with GitHub release
78
+
79
+ ### Manual Testing
80
+
81
+ ```bash
82
+ # Preview version changes (safe)
83
+ pnpm changeset:version
84
+
85
+ # Publish manually (only for emergencies)
86
+ pnpm changeset:publish
87
+ ```
88
+
89
+ ## πŸ“‹ Code Standards
90
+
91
+ ### TypeScript
92
+
93
+ - Use strict TypeScript settings
94
+ - Export types explicitly
95
+ - Document public APIs with JSDoc
96
+
97
+ ### Code Style
98
+
99
+ - Follow existing patterns
100
+ - Use meaningful variable names
101
+ - Add comments for complex logic
102
+ - **NO explanation comments** in code (see `docs/conventions.md`)
103
+
104
+ ### Commit Messages
105
+
106
+ - `feat:` - New features
107
+ - `fix:` - Bug fixes
108
+ - `docs:` - Documentation
109
+ - `chore:` - Maintenance
110
+ - `refactor:` - Code restructuring
111
+
112
+ ## πŸ§ͺ Testing
113
+
114
+ ### Manual Testing
115
+
116
+ ```bash
117
+ # Test demo locally
118
+ pnpm dev
119
+
120
+ # Test CLI
121
+ pnpm cli:find-port
122
+ pnpm cli:calibrate
123
+
124
+ # Test package build
125
+ pnpm --filter "@lerobot/web" run build
126
+ ```
127
+
128
+ ### Hardware Testing
129
+
130
+ - Use SO-100 leader/follower arms when available
131
+ - Test calibration, teleoperation, port discovery
132
+ - Verify WebSerial API compatibility
133
+
134
+ ## πŸ“ Documentation
135
+
136
+ ### Update Documentation For:
137
+
138
+ - New functions in `@lerobot/web`
139
+ - CLI command changes
140
+ - Hardware support additions
141
+ - Breaking changes
142
+
143
+ ### Files to Update:
144
+
145
+ - `packages/web/README.md` - Package documentation
146
+ - `README.md` - Main project overview
147
+ - JSDoc comments for new APIs
148
+
149
+ ## πŸ› Bug Reports
150
+
151
+ Include:
152
+
153
+ - Steps to reproduce
154
+ - Expected vs actual behavior
155
+ - Browser/Node.js version
156
+ - Hardware setup (if relevant)
157
+ - Error messages/console output
158
+
159
+ ## πŸ’‘ Feature Requests
160
+
161
+ - Explain the use case
162
+ - Provide examples if possible
163
+ - Consider backward compatibility
164
+ - Discuss implementation approach
165
+
166
+ ## 🎯 Areas for Contribution
167
+
168
+ ### High Priority
169
+
170
+ - New robot hardware support
171
+ - Browser compatibility improvements
172
+ - Performance optimizations
173
+ - Documentation improvements
174
+
175
+ ### Medium Priority
176
+
177
+ - Additional calibration methods
178
+ - UI/UX enhancements
179
+ - CLI tool features
180
+ - Testing infrastructure
181
+
182
+ ### Advanced
183
+
184
+ - WebRTC integration
185
+ - Computer vision features
186
+ - Machine learning integration
187
+ - Protocol implementations
188
+
189
+ ## πŸ”§ Development Tips
190
+
191
+ ### Common Commands
192
+
193
+ ```bash
194
+ # Install dependencies
195
+ pnpm install
196
+
197
+ # Start demo development
198
+ pnpm dev
199
+
200
+ # Build everything
201
+ pnpm build
202
+
203
+ # Create changeset
204
+ pnpm changeset
205
+
206
+ # Work with specific package
207
+ pnpm --filter "@lerobot/web" run build
208
+ ```
209
+
210
+ ### Debugging
211
+
212
+ - Use browser DevTools for WebSerial issues
213
+ - Check console for hardware communication errors
214
+ - Use `console.log` for motor position debugging
215
+ - Test with different hardware configurations
216
+
217
+ ## πŸ“ž Getting Help
218
+
219
+ - **Issues**: Use GitHub issues for bugs/features
220
+ - **Discussions**: Use GitHub discussions for questions
221
+ - **Hardware**: Check hardware documentation in `docs/`
222
+
223
+ ## πŸ™ Recognition
224
+
225
+ All contributors will be recognized in releases and project documentation. Thank you for helping make robotics more accessible! πŸ€–βœ¨
.github/workflows/hf-space.yml CHANGED
@@ -13,6 +13,30 @@ jobs:
13
  fetch-depth: 0
14
  lfs: true
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  - name: Prepare README for HF Space
17
  run: |
18
  # Create HF Space frontmatter by directly copying from hf_config.yml
 
13
  fetch-depth: 0
14
  lfs: true
15
 
16
+ - name: Setup pnpm
17
+ uses: pnpm/action-setup@v4
18
+ with:
19
+ version: latest
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 18
25
+ cache: "pnpm"
26
+
27
+ - name: Install dependencies
28
+ run: pnpm install --frozen-lockfile
29
+
30
+ - name: Build web package
31
+ run: pnpm --filter "@lerobot/web" run build
32
+
33
+ - name: Install web package for demo
34
+ run: |
35
+ cd packages/web
36
+ npm pack
37
+ cd ../..
38
+ pnpm add ./packages/web/lerobot-web-*.tgz
39
+
40
  - name: Prepare README for HF Space
41
  run: |
42
  # Create HF Space frontmatter by directly copying from hf_config.yml
.github/workflows/release.yml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ concurrency: ${{ github.workflow }}-${{ github.ref }}
9
+
10
+ jobs:
11
+ release:
12
+ name: Release
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout Repo
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup pnpm
19
+ uses: pnpm/action-setup@v4
20
+ with:
21
+ version: latest
22
+
23
+ - name: Setup Node.js
24
+ uses: actions/setup-node@v4
25
+ with:
26
+ node-version: 18
27
+ cache: "pnpm"
28
+
29
+ - name: Install Dependencies
30
+ run: pnpm install --frozen-lockfile
31
+
32
+ - name: Build packages
33
+ run: pnpm --filter "@lerobot/web" run build
34
+
35
+ - name: Create Release Pull Request or Publish to npm
36
+ id: changesets
37
+ uses: changesets/action@v1
38
+ with:
39
+ # This expects you to have a script called release which does a build for your packages and calls changeset publish
40
+ publish: pnpm changeset:publish
41
+ version: pnpm changeset:version
42
+ commit: "chore: version packages"
43
+ title: "chore: version packages"
44
+ env:
45
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
CHANGESET_GUIDE.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸš€ Changesets Workflow Guide
2
+
3
+ ## Setup (One Time Only)
4
+
5
+ ### 1. Add NPM Token to GitHub
6
+
7
+ 1. Go to [npmjs.com](https://npmjs.com) β†’ Account β†’ Access Tokens
8
+ 2. Create **Automation** token with **Publish** permission
9
+ 3. Copy the token
10
+ 4. Go to GitHub β†’ Settings β†’ Secrets β†’ Actions
11
+ 5. Add secret: `NPM_TOKEN` = your token
12
+
13
+ ### 2. Verify Access
14
+
15
+ Make sure you have publish access to `@lerobot/web` on npm.
16
+
17
+ ## Daily Workflow
18
+
19
+ ### 1. Make Changes
20
+
21
+ ```bash
22
+ # Edit code in packages/web/
23
+ # Test your changes
24
+ pnpm --filter "@lerobot/web" run build
25
+ ```
26
+
27
+ ### 2. Create Changeset
28
+
29
+ ```bash
30
+ pnpm changeset
31
+ ```
32
+
33
+ - Select package: `@lerobot/web`
34
+ - Choose version bump: `patch` | `minor` | `major`
35
+ - Write summary: "Add new feature X" or "Fix bug Y"
36
+
37
+ ### 3. Commit & Push
38
+
39
+ ```bash
40
+ git add .
41
+ git commit -m "feat: add new functionality"
42
+ git push origin main
43
+ ```
44
+
45
+ ### 4. Magic Happens! ✨
46
+
47
+ - GitHub Actions creates **Release PR** with changelog
48
+ - **Merge the PR** β†’ Automatic publish to npm
49
+ - **New version is live!**
50
+
51
+ ## Commands Available
52
+
53
+ ```bash
54
+ # Create a changeset (describe your changes)
55
+ pnpm changeset
56
+
57
+ # Preview what will be released
58
+ pnpm changeset:version
59
+
60
+ # Manual publish (emergency only)
61
+ pnpm changeset:publish
62
+ ```
63
+
64
+ ## Version Types
65
+
66
+ - **patch** (0.1.0 β†’ 0.1.1) - Bug fixes
67
+ - **minor** (0.1.0 β†’ 0.2.0) - New features
68
+ - **major** (0.1.0 β†’ 1.0.0) - Breaking changes
69
+
70
+ ## Example Flow
71
+
72
+ ```bash
73
+ # 1. Edit packages/web/src/calibrate.ts
74
+ # 2. Create changeset
75
+ pnpm changeset
76
+ # β†’ Select @lerobot/web
77
+ # β†’ Choose "minor"
78
+ # β†’ Summary: "Add new calibration mode"
79
+
80
+ # 3. Commit
81
+ git add .
82
+ git commit -m "feat: add new calibration mode"
83
+ git push
84
+
85
+ # 4. GitHub will create Release PR automatically
86
+ # 5. Merge PR β†’ @lerobot/[email protected] published! πŸŽ‰
87
+ ```
README.md CHANGED
@@ -1,335 +1,63 @@
1
  # πŸ€– lerobot.js
2
 
3
- **State-of-the-art AI for real-world robotics in JavaScript/TypeScript**
4
 
5
- A faithful TypeScript/JavaScript port of [Hugging Face's lerobot](https://github.com/huggingface/lerobot), bringing cutting-edge robotics AI to the JavaScript ecosystem with **zero Python dependencies**.
6
-
7
- ## πŸš€ **[Complete SO-100 Setup Guide β†’](docs/getting_started_nodejs.md)**
8
-
9
- **Get your SO-100 robot arms working in 10 minutes with lerobot.js!**
10
- Step-by-step guide covering port detection, motor setup, calibration, and teleoperation.
11
-
12
- ## ✨ Features
13
-
14
- - πŸ”Œ **USB Port Detection**: Find robot arm serial ports in Node.js CLI and browser
15
- - πŸŽ›οΈ **Robot Calibration**: Complete SO-100 follower/leader calibration system
16
- - 🌐 **Universal**: Works in Node.js, browsers, and Edge devices
17
- - 🎯 **Python Faithful**: Identical UX and messaging to original lerobot
18
- - πŸ“± **WebSerial**: Browser-native serial port access (Chrome/Edge 89+)
19
- - πŸš€ **Zero Dependencies**: No Python runtime required
20
- - πŸ“¦ **Lightweight**: Pure TypeScript implementation
21
-
22
- ## πŸš€ Quick Start
23
-
24
- ### Installation & Setup
25
 
26
  ```bash
27
- # Option 1: Install globally (recommended)
28
  npm install -g lerobot
29
 
30
- # Option 2: Use directly with npx (no installation)
31
- npx lerobot --help
32
-
33
- # Verify installation
34
- lerobot --help
35
  ```
36
 
37
- ### Essential Commands
38
 
39
  ```bash
40
- # 1. Find USB ports for your robot arms
41
  lerobot find-port
42
 
43
- # 2. Calibrate follower robot
44
- lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm
45
-
46
- # 3. Calibrate leader teleoperator
47
- lerobot calibrate --teleop.type=so100_leader --teleop.port=COM3 --teleop.id=my_leader_arm
48
-
49
- # Show command help
50
- lerobot calibrate --help
51
- ```
52
-
53
- ### Alternative Usage Methods
54
-
55
- ```bash
56
- # Method 1: Global CLI (after installation)
57
  lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_robot
58
 
59
- # Method 2: Direct with npx (no installation needed)
60
- npx lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_robot
61
-
62
- # Method 3: Development setup (if you cloned the repo)
63
- git clone https://github.com/timpietrusky/lerobot.js
64
- cd lerobot.js && pnpm install && pnpm run install-global
65
- ```
66
-
67
- ### Browser Usage
68
-
69
- 1. **Development**:
70
-
71
- ```bash
72
- git clone https://github.com/timpietrusky/lerobot.js
73
- cd lerobot.js
74
- pnpm install
75
- pnpm run dev
76
- ```
77
-
78
- 2. **Visit**: `http://localhost:5173`
79
-
80
- 3. **Use the interface**:
81
- - Click "Show Available Ports" to see connected devices
82
- - Click "Find MotorsBus Port" for guided port detection
83
-
84
- ## πŸ“– Documentation
85
-
86
- ### Find USB Ports
87
-
88
- Identify which USB ports your robot arms are connected to - essential for SO-100 setup.
89
-
90
- #### CLI Example
91
-
92
- ```bash
93
- $ lerobot find-port
94
-
95
- Finding all available ports for the MotorsBus.
96
- Ports before disconnecting: ['COM3', 'COM4']
97
- Remove the USB cable from your MotorsBus and press Enter when done.
98
-
99
- The port of this MotorsBus is 'COM3'
100
- Reconnect the USB cable.
101
- ```
102
-
103
- ### Robot Calibration
104
-
105
- Calibrate SO-100 robot arms for precise control and teleoperation.
106
-
107
- #### Calibrate Follower Robot
108
-
109
- ```bash
110
- $ lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm
111
-
112
- Calibrating device...
113
- Device type: so100_follower
114
- Port: COM4
115
- ID: my_follower_arm
116
-
117
- Connecting to so100_follower on port COM4...
118
- Connected successfully.
119
- Starting calibration procedure...
120
- [... calibration steps ...]
121
- Calibration completed successfully.
122
- Configuration saved to: ~/.cache/huggingface/lerobot/calibration/robots/so100_follower/my_follower_arm.json
123
- Disconnecting from robot...
124
- ```
125
-
126
- #### Calibrate Leader Teleoperator
127
-
128
- ```bash
129
- $ lerobot calibrate --teleop.type=so100_leader --teleop.port=COM3 --teleop.id=my_leader_arm
130
-
131
- Calibrating teleoperator...
132
- [... guided calibration process ...]
133
- Configuration saved to: ~/.cache/huggingface/lerobot/calibration/teleoperators/so100_leader/my_leader_arm.json
134
- ```
135
-
136
- ### Browser Interface
137
-
138
- 1. **Visit**: Built-in web interface with calibration controls
139
- 2. **Port Selection**: Browser dialog for device selection
140
- 3. **Interactive Calibration**: Step-by-step guided process
141
- 4. **File Download**: Automatic calibration file download
142
-
143
- ### Platform Support
144
-
145
- | Platform | Method | Requirements |
146
- | ----------- | -------------------------------- | ----------------------------------- |
147
- | **Node.js** | `lerobot find-port`, `calibrate` | Node.js 18+, Windows/macOS/Linux |
148
- | **Browser** | Web interface + calibration | Chrome/Edge 89+, HTTPS or localhost |
149
- | **Mobile** | Browser | Chrome Android 105+ |
150
-
151
- ### Browser Compatibility
152
-
153
- The browser version uses the [WebSerial API](https://web.dev/serial/):
154
-
155
- - βœ… **Chrome/Edge 89+** (Desktop)
156
- - βœ… **Chrome Android 105+** (Mobile)
157
- - βœ… **HTTPS** or localhost required
158
- - ❌ Firefox/Safari (WebSerial not supported)
159
-
160
- ## πŸ› οΈ Development
161
-
162
- ### Setup
163
-
164
- ```bash
165
- git clone https://github.com/timpietrusky/lerobot.js
166
- cd lerobot.js
167
- pnpm install
168
- ```
169
-
170
- ### Scripts
171
-
172
- ```bash
173
- # Development server (browser)
174
- pnpm run dev
175
-
176
- # Build CLI for Node.js
177
- pnpm run build:cli
178
-
179
- # Build web interface
180
- pnpm run build:web
181
-
182
- # Test CLI locally
183
- pnpm run cli:find-port
184
-
185
- # Build everything
186
- pnpm run build
187
- ```
188
-
189
- ### Project Structure
190
-
191
- ```
192
- src/
193
- β”œβ”€β”€ lerobot/
194
- β”‚ β”œβ”€β”€ node/
195
- β”‚ β”‚ └── find_port.ts # Node.js implementation
196
- β”‚ └── web/
197
- β”‚ └── find_port.ts # Browser implementation (WebSerial)
198
- β”œβ”€β”€ cli/
199
- β”‚ └── index.ts # CLI interface
200
- β”œβ”€β”€ main.ts # Web interface
201
- └── web_interface.css # UI styles
202
  ```
203
 
204
- ## 🎯 Design Principles
205
-
206
- ### 1. Python lerobot Faithfulness
207
-
208
- - **Identical commands**: `npx lerobot find-port` ↔ `python -m lerobot.find_port`
209
- - **Same terminology**: "MotorsBus", not "robot arms"
210
- - **Matching output**: Error messages and workflows identical
211
- - **Familiar UX**: Python lerobot users feel immediately at home
212
-
213
- ### 2. Platform Abstraction
214
-
215
- - **Universal core**: Shared robotics logic
216
- - **Adaptive UX**: CLI prompts vs. browser modals
217
- - **Progressive enhancement**: Works everywhere, enhanced on capable platforms
218
-
219
- ### 3. Zero Dependencies
220
-
221
- - **Node.js**: Only uses built-in modules + lightweight `serialport`
222
- - **Browser**: Native WebSerial API, no external libraries
223
- - **Deployment**: Single package, no Python runtime needed
224
-
225
- ## πŸ”§ Technical Details
226
-
227
- ### WebSerial API
228
-
229
- The browser implementation leverages modern web APIs:
230
 
231
  ```typescript
232
- // Request permission to access serial ports
233
- await navigator.serial.requestPort();
234
-
235
- // List granted ports
236
- const ports = await navigator.serial.getPorts();
237
-
238
- // Detect disconnected devices
239
- const removedPorts = portsBefore.filter(/* ... */);
240
- ```
241
 
242
- ### CLI Implementation
 
 
243
 
244
- Node.js version uses the `serialport` library for cross-platform compatibility:
 
 
245
 
246
- ```typescript
247
- import { SerialPort } from "serialport";
248
-
249
- // Cross-platform port detection
250
- const ports = await SerialPort.list();
251
  ```
252
 
253
- ## πŸ—ΊοΈ Roadmap
254
-
255
- - [x] **Phase 1**: USB port detection (CLI + Browser)
256
- - [ ] **Phase 2**: Motor communication and setup
257
- - [x] **Phase 3**: Robot calibration tools βœ… **COMPLETE!**
258
- - [ ] **Phase 4**: Dataset management and visualization
259
- - [ ] **Phase 5**: Policy inference (ONNX.js)
260
- - [ ] **Phase 6**: Training infrastructure
261
-
262
- ### βœ… Recently Completed
263
 
264
- **Phase 3 - Robot Calibration (December 2024)**
 
 
265
 
266
- - Complete SO-100 follower/leader calibration system
267
- - CLI commands identical to Python lerobot
268
- - Web browser calibration interface
269
- - HF-compatible configuration storage
270
- - Comprehensive error handling and validation
271
-
272
- ## πŸ“‹ CLI Command Reference
273
-
274
- ### Available Commands
275
 
276
  ```bash
277
- # Show all commands
278
- lerobot --help
279
-
280
- # Find USB ports
281
- lerobot find-port
282
-
283
- # Calibrate robot
284
- lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=ROBOT_ID
285
-
286
- # Calibrate teleoperator
287
- lerobot calibrate --teleop.type=so100_leader --teleop.port=COM3 --teleop.id=TELEOP_ID
288
-
289
- # Show calibration help
290
- lerobot calibrate --help
291
- ```
292
-
293
- ### Configuration Files
294
-
295
- Calibration data follows Hugging Face directory structure:
296
-
297
- ```
298
- ~/.cache/huggingface/lerobot/calibration/
299
- β”œβ”€β”€ robots/
300
- β”‚ └── so100_follower/
301
- β”‚ └── ROBOT_ID.json
302
- └── teleoperators/
303
- └── so100_leader/
304
- └── TELEOP_ID.json
305
  ```
306
 
307
- **Environment Variables:**
308
-
309
- - `HF_HOME`: Override Hugging Face home directory
310
- - `HF_LEROBOT_CALIBRATION`: Override calibration directory
311
-
312
- ## 🀝 Contributing
313
-
314
- We welcome contributions! This project follows the principle of **Python lerobot faithfulness** - all features should maintain identical UX to the original.
315
-
316
- ### Guidelines
317
-
318
- 1. Check Python lerobot implementation first
319
- 2. Maintain identical command structure and messaging
320
- 3. Follow snake_case file naming convention
321
- 4. Test on both Node.js and browser platforms
322
-
323
- ## πŸ“„ License
324
-
325
- Apache 2.0 - Same as original lerobot
326
-
327
- ## πŸ™ Acknowledgments
328
-
329
- - [Hugging Face lerobot team](https://github.com/huggingface/lerobot) for the original Python implementation
330
- - [WebSerial API](https://web.dev/serial/) for browser-native hardware access
331
- - [serialport](https://github.com/serialport/node-serialport) for Node.js cross-platform support
332
 
333
- ---
334
 
335
- **Built with ❀️ for the robotics community**
 
1
  # πŸ€– lerobot.js
2
 
3
+ JavaScript/TypeScript robotics library for Node.js and browsers. Control hardware directly without Python dependencies.
4
 
5
+ ## Install
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  ```bash
8
+ # CLI tools
9
  npm install -g lerobot
10
 
11
+ # Web library
12
+ npm install @lerobot/web
 
 
 
13
  ```
14
 
15
+ ## CLI Usage
16
 
17
  ```bash
18
+ # Find hardware
19
  lerobot find-port
20
 
21
+ # Calibrate device
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_robot
23
 
24
+ # Show help
25
+ lerobot --help
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  ```
27
 
28
+ ## Web Usage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  ```typescript
31
+ import { findPort, calibrate, teleoperate } from "@lerobot/web";
 
 
 
 
 
 
 
 
32
 
33
+ // Find and connect to hardware
34
+ const devices = await findPort();
35
+ const robot = devices[0];
36
 
37
+ // Calibrate
38
+ const calibration = await calibrate(robot);
39
+ await calibration.result;
40
 
41
+ // Control
42
+ const controller = await teleoperate(robot);
43
+ controller.start();
 
 
44
  ```
45
 
46
+ ## Hardware Support
 
 
 
 
 
 
 
 
 
47
 
48
+ - **SO-100**: Follower/leader robot arms
49
+ - **WebSerial API**: Chrome/Edge 89+, HTTPS required
50
+ - **Node.js**: Cross-platform serial port access
51
 
52
+ ## Demo
 
 
 
 
 
 
 
 
53
 
54
  ```bash
55
+ git clone https://github.com/timpietrusky/lerobot.js
56
+ cd lerobot.js && npm install && npm run dev
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  ```
58
 
59
+ Visit `http://localhost:5173` for the web interface.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ ## License
62
 
63
+ Apache-2.0
package.json CHANGED
@@ -3,6 +3,9 @@
3
  "version": "0.0.0",
4
  "description": "State-of-the-art AI for real-world robotics in JS",
5
  "type": "module",
 
 
 
6
  "bin": {
7
  "lerobot": "./dist/cli/index.js"
8
  },
@@ -30,16 +33,21 @@
30
  "cli:find-port": "tsx src/cli/index.ts find-port",
31
  "cli:calibrate": "tsx src/cli/index.ts calibrate",
32
  "cli:teleoperate": "tsx src/cli/index.ts teleoperate",
 
 
 
33
  "prepublishOnly": "pnpm run build",
34
  "install-global": "pnpm run build && npm link"
35
  },
36
  "dependencies": {
 
37
  "@radix-ui/react-dialog": "^1.1.14",
38
  "@radix-ui/react-progress": "^1.1.7",
39
  "log-update": "^6.1.0",
40
  "serialport": "^12.0.0"
41
  },
42
  "devDependencies": {
 
43
  "@types/node": "^22.10.5",
44
  "@types/react": "^18.2.79",
45
  "@types/react-dom": "^18.2.25",
 
3
  "version": "0.0.0",
4
  "description": "State-of-the-art AI for real-world robotics in JS",
5
  "type": "module",
6
+ "workspaces": [
7
+ "packages/*"
8
+ ],
9
  "bin": {
10
  "lerobot": "./dist/cli/index.js"
11
  },
 
33
  "cli:find-port": "tsx src/cli/index.ts find-port",
34
  "cli:calibrate": "tsx src/cli/index.ts calibrate",
35
  "cli:teleoperate": "tsx src/cli/index.ts teleoperate",
36
+ "changeset": "changeset",
37
+ "changeset:version": "changeset version",
38
+ "changeset:publish": "changeset publish",
39
  "prepublishOnly": "pnpm run build",
40
  "install-global": "pnpm run build && npm link"
41
  },
42
  "dependencies": {
43
+ "@lerobot/web": "^0.1.1",
44
  "@radix-ui/react-dialog": "^1.1.14",
45
  "@radix-ui/react-progress": "^1.1.7",
46
  "log-update": "^6.1.0",
47
  "serialport": "^12.0.0"
48
  },
49
  "devDependencies": {
50
+ "@changesets/cli": "^2.29.5",
51
  "@types/node": "^22.10.5",
52
  "@types/react": "^18.2.79",
53
  "@types/react-dom": "^18.2.25",
packages/web/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # @lerobot/web
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - prepare "web" for release on npm
packages/web/README.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @lerobot/web
2
+
3
+ Control robotics hardware directly from the browser using WebSerial API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @lerobot/web
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { findPort, calibrate, teleoperate } from "@lerobot/web";
15
+
16
+ // 1. Find hardware
17
+ const devices = await findPort();
18
+ const robot = devices[0];
19
+
20
+ // 2. Calibrate
21
+ const calibration = await calibrate(robot);
22
+ await calibration.result;
23
+
24
+ // 3. Control
25
+ const controller = await teleoperate(robot);
26
+ controller.start();
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### `findPort(options?)`
32
+
33
+ Detects connected hardware. Returns `RobotConnection[]`.
34
+
35
+ ### `calibrate(robot, options?)`
36
+
37
+ Calibrates motors and records ranges. Returns `CalibrationProcess`.
38
+
39
+ ### `teleoperate(robot, options?)`
40
+
41
+ Enables real-time control. Returns `TeleoperationProcess`.
42
+
43
+ ## Browser Support
44
+
45
+ - Chrome/Edge 89+
46
+ - Requires HTTPS or localhost
47
+ - [WebSerial API](https://caniuse.com/web-serial) support
48
+
49
+ ## Hardware
50
+
51
+ Currently supports SO-100 follower/leader arms. More devices coming.
52
+
53
+ ## License
54
+
55
+ Apache-2.0
packages/web/package.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@lerobot/web",
3
+ "version": "0.1.1",
4
+ "description": "Web-based robotics control using WebSerial API",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./calibrate": {
14
+ "import": "./dist/calibrate.js",
15
+ "types": "./dist/calibrate.d.ts"
16
+ },
17
+ "./teleoperate": {
18
+ "import": "./dist/teleoperate.js",
19
+ "types": "./dist/teleoperate.d.ts"
20
+ },
21
+ "./find-port": {
22
+ "import": "./dist/find_port.js",
23
+ "types": "./dist/find_port.d.ts"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist/**/*",
28
+ "README.md"
29
+ ],
30
+ "keywords": [
31
+ "robotics",
32
+ "webserial",
33
+ "hardware-control",
34
+ "browser",
35
+ "typescript"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc --project tsconfig.build.json",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "peerDependencies": {
42
+ "typescript": ">=4.5.0"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/timpietrusky/lerobot.js",
47
+ "directory": "packages/web"
48
+ },
49
+ "license": "Apache-2.0",
50
+ "author": "Tim Pietrusky",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }
{src/lerobot/web β†’ packages/web/src}/calibrate.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/find_port.ts RENAMED
File without changes
packages/web/src/index.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @lerobot/web - Web-based robotics control using WebSerial API
3
+ *
4
+ * Control robotics hardware directly from the browser using modern web APIs.
5
+ * No Python dependencies required.
6
+ */
7
+
8
+ // Core functions
9
+ export { calibrate, isWebSerialSupported } from "./calibrate.js";
10
+ export { teleoperate } from "./teleoperate.js";
11
+ export { findPort } from "./find_port.js";
12
+
13
+ // Types
14
+ export type {
15
+ RobotConnection,
16
+ RobotConfig,
17
+ SerialPort,
18
+ SerialPortInfo,
19
+ SerialOptions,
20
+ } from "./types/robot-connection.js";
21
+
22
+ export type {
23
+ WebCalibrationResults,
24
+ LiveCalibrationData,
25
+ CalibrationProcess,
26
+ } from "./types/calibration.js";
27
+
28
+ export type {
29
+ MotorConfig,
30
+ TeleoperationState,
31
+ TeleoperationProcess,
32
+ } from "./types/teleoperation.js";
33
+
34
+ export type {
35
+ RobotHardwareConfig,
36
+ KeyboardControl,
37
+ } from "./types/robot-config.js";
38
+
39
+ // Utilities (advanced users)
40
+ export { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js";
41
+ export {
42
+ createSO100Config,
43
+ SO100_KEYBOARD_CONTROLS,
44
+ } from "./robots/so100_config.js";
45
+ export { releaseMotors } from "./utils/motor-communication.js";
{src/lerobot/web β†’ packages/web/src}/robots/so100_config.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/teleoperate.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/types/calibration.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/types/port-discovery.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/types/robot-config.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/types/robot-connection.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/types/teleoperation.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/utils/motor-calibration.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/utils/motor-communication.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/utils/serial-port-wrapper.ts RENAMED
@@ -3,6 +3,8 @@
3
  * Standardized Web Serial API interface with immediate lock release
4
  */
5
 
 
 
6
  /**
7
  * Web Serial Port wrapper - direct write/read with immediate lock release
8
  */
 
3
  * Standardized Web Serial API interface with immediate lock release
4
  */
5
 
6
+ import type { SerialPort } from "../types/robot-connection.js";
7
+
8
  /**
9
  * Web Serial Port wrapper - direct write/read with immediate lock release
10
  */
{src/lerobot/web β†’ packages/web/src}/utils/sign-magnitude.ts RENAMED
File without changes
{src/lerobot/web β†’ packages/web/src}/utils/sts3215-protocol.ts RENAMED
File without changes
packages/web/tsconfig.build.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "bundler",
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "noEmit": false,
13
+ "strict": true,
14
+ "skipLibCheck": true,
15
+ "esModuleInterop": true,
16
+ "forceConsistentCasingInFileNames": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.spec.ts"]
20
+ }
pnpm-lock.yaml CHANGED
@@ -21,6 +21,9 @@ importers:
21
  specifier: ^12.0.0
22
  version: 12.0.0
23
  devDependencies:
 
 
 
24
  '@types/node':
25
  specifier: ^22.10.5
26
  version: 22.15.31
@@ -70,6 +73,12 @@ importers:
70
  specifier: ^6.3.5
71
72
 
 
 
 
 
 
 
73
  packages:
74
 
75
  '@alloc/[email protected]':
@@ -147,6 +156,10 @@ packages:
147
  peerDependencies:
148
  '@babel/core': ^7.0.0-0
149
 
 
 
 
 
150
  '@babel/[email protected]':
151
  resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
152
  engines: {node: '>=6.9.0'}
@@ -159,6 +172,61 @@ packages:
159
  resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
160
  engines: {node: '>=6.9.0'}
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  '@esbuild/[email protected]':
163
  resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
164
  engines: {node: '>=18'}
@@ -331,6 +399,12 @@ packages:
331
  '@jridgewell/[email protected]':
332
  resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
333
 
 
 
 
 
 
 
334
  '@nodelib/[email protected]':
335
  resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
336
  engines: {node: '>= 8'}
@@ -713,6 +787,9 @@ packages:
713
  '@types/[email protected]':
714
  resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
715
 
 
 
 
716
  '@types/[email protected]':
717
  resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
718
 
@@ -733,6 +810,10 @@ packages:
733
  peerDependencies:
734
  vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
735
 
 
 
 
 
736
737
  resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
738
  engines: {node: '>=18'}
@@ -763,10 +844,17 @@ packages:
763
764
  resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
765
 
 
 
 
766
767
  resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
768
  engines: {node: '>=10'}
769
 
 
 
 
 
770
771
  resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
772
  engines: {node: ^10 || ^12 || >=14}
@@ -777,6 +865,10 @@ packages:
777
778
  resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
779
 
 
 
 
 
780
781
  resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
782
  engines: {node: '>=8'}
@@ -800,10 +892,17 @@ packages:
800
801
  resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
802
 
 
 
 
803
804
  resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
805
  engines: {node: '>= 8.10.0'}
806
 
 
 
 
 
807
808
  resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
809
 
@@ -850,12 +949,20 @@ packages:
850
  supports-color:
851
  optional: true
852
 
 
 
 
 
853
854
  resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
855
 
856
857
  resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
858
 
 
 
 
 
859
860
  resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
861
 
@@ -874,6 +981,10 @@ packages:
874
875
  resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
876
 
 
 
 
 
877
878
  resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
879
  engines: {node: '>=18'}
@@ -887,6 +998,18 @@ packages:
887
  resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
888
  engines: {node: '>=6'}
889
 
 
 
 
 
 
 
 
 
 
 
 
 
890
891
  resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
892
  engines: {node: '>=8.6.0'}
@@ -906,6 +1029,10 @@ packages:
906
  resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
907
  engines: {node: '>=8'}
908
 
 
 
 
 
909
910
  resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
911
  engines: {node: '>=14'}
@@ -913,6 +1040,14 @@ packages:
913
914
  resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
915
 
 
 
 
 
 
 
 
 
916
917
  resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
918
  engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -952,10 +1087,29 @@ packages:
952
  resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
953
  engines: {node: '>=4'}
954
 
 
 
 
 
 
 
 
955
956
  resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
957
  engines: {node: '>= 0.4'}
958
 
 
 
 
 
 
 
 
 
 
 
 
 
959
960
  resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
961
  engines: {node: '>=8'}
@@ -984,6 +1138,14 @@ packages:
984
  resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
985
  engines: {node: '>=0.12.0'}
986
 
 
 
 
 
 
 
 
 
987
988
  resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
989
 
@@ -997,6 +1159,10 @@ packages:
997
998
  resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
999
 
 
 
 
 
1000
1001
  resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
1002
  engines: {node: '>=6'}
@@ -1007,6 +1173,9 @@ packages:
1007
  engines: {node: '>=6'}
1008
  hasBin: true
1009
 
 
 
 
1010
1011
  resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
1012
  engines: {node: '>=14'}
@@ -1014,6 +1183,13 @@ packages:
1014
1015
  resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
1016
 
 
 
 
 
 
 
 
1017
1018
  resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
1019
  engines: {node: '>=18'}
@@ -1053,6 +1229,10 @@ packages:
1053
  resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
1054
  engines: {node: '>=16 || 14 >=14.17'}
1055
 
 
 
 
 
1056
1057
  resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
1058
 
@@ -1094,9 +1274,43 @@ packages:
1094
  resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
1095
  engines: {node: '>=18'}
1096
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1097
1098
  resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
1099
 
 
 
 
 
 
 
 
1100
1101
  resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1102
  engines: {node: '>=8'}
@@ -1108,6 +1322,10 @@ packages:
1108
  resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
1109
  engines: {node: '>=16 || 14 >=14.18'}
1110
 
 
 
 
 
1111
1112
  resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1113
 
@@ -1123,6 +1341,10 @@ packages:
1123
  resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
1124
  engines: {node: '>=0.10.0'}
1125
 
 
 
 
 
1126
1127
  resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
1128
  engines: {node: '>= 6'}
@@ -1168,6 +1390,14 @@ packages:
1168
  resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
1169
  engines: {node: ^10 || ^12 || >=14}
1170
 
 
 
 
 
 
 
 
 
1171
1172
  resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
1173
 
@@ -1217,10 +1447,18 @@ packages:
1217
1218
  resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
1219
 
 
 
 
 
1220
1221
  resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
1222
  engines: {node: '>=8.10.0'}
1223
 
 
 
 
 
1224
1225
  resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
1226
 
@@ -1245,6 +1483,9 @@ packages:
1245
1246
  resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1247
 
 
 
 
1248
1249
  resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
1250
 
@@ -1252,6 +1493,11 @@ packages:
1252
  resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1253
  hasBin: true
1254
 
 
 
 
 
 
1255
1256
  resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
1257
  engines: {node: '>=16.0.0'}
@@ -1268,6 +1514,10 @@ packages:
1268
  resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
1269
  engines: {node: '>=14'}
1270
 
 
 
 
 
1271
1272
  resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
1273
  engines: {node: '>=18'}
@@ -1276,6 +1526,12 @@ packages:
1276
  resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1277
  engines: {node: '>=0.10.0'}
1278
 
 
 
 
 
 
 
1279
1280
  resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
1281
  engines: {node: '>=8'}
@@ -1296,6 +1552,10 @@ packages:
1296
  resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
1297
  engines: {node: '>=12'}
1298
 
 
 
 
 
1299
1300
  resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
1301
  engines: {node: '>=16 || 14 >=14.17'}
@@ -1313,6 +1573,10 @@ packages:
1313
  engines: {node: '>=14.0.0'}
1314
  hasBin: true
1315
 
 
 
 
 
1316
1317
  resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
1318
  engines: {node: '>=0.8'}
@@ -1324,6 +1588,10 @@ packages:
1324
  resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
1325
  engines: {node: '>=12.0.0'}
1326
 
 
 
 
 
1327
1328
  resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1329
  engines: {node: '>=8.0'}
@@ -1347,6 +1615,10 @@ packages:
1347
1348
  resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
1349
 
 
 
 
 
1350
1351
  resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
1352
  hasBin: true
@@ -1537,6 +1809,8 @@ snapshots:
1537
  '@babel/core': 7.27.4
1538
  '@babel/helper-plugin-utils': 7.27.1
1539
 
 
 
1540
  '@babel/[email protected]':
1541
  dependencies:
1542
  '@babel/code-frame': 7.27.1
@@ -1560,6 +1834,148 @@ snapshots:
1560
  '@babel/helper-string-parser': 7.27.1
1561
  '@babel/helper-validator-identifier': 7.27.1
1562
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1563
  '@esbuild/[email protected]':
1564
  optional: true
1565
 
@@ -1661,6 +2077,22 @@ snapshots:
1661
  '@jridgewell/resolve-uri': 3.1.2
1662
  '@jridgewell/sourcemap-codec': 1.5.0
1663
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1664
  '@nodelib/[email protected]':
1665
  dependencies:
1666
  '@nodelib/fs.stat': 2.0.5
@@ -1968,6 +2400,8 @@ snapshots:
1968
 
1969
  '@types/[email protected]': {}
1970
 
 
 
1971
  '@types/[email protected]':
1972
  dependencies:
1973
  undici-types: 6.21.0
@@ -1995,6 +2429,8 @@ snapshots:
1995
  transitivePeerDependencies:
1996
  - supports-color
1997
 
 
 
1998
1999
  dependencies:
2000
  environment: 1.1.0
@@ -2018,10 +2454,16 @@ snapshots:
2018
 
2019
2020
 
 
 
 
 
2021
2022
  dependencies:
2023
  tslib: 2.8.1
2024
 
 
 
2025
2026
  dependencies:
2027
  browserslist: 4.25.0
@@ -2034,6 +2476,10 @@ snapshots:
2034
 
2035
2036
 
 
 
 
 
2037
2038
 
2039
@@ -2055,6 +2501,8 @@ snapshots:
2055
 
2056
2057
 
 
 
2058
2059
  dependencies:
2060
  anymatch: 3.1.3
@@ -2067,6 +2515,8 @@ snapshots:
2067
  optionalDependencies:
2068
  fsevents: 2.3.3
2069
 
 
 
2070
2071
  dependencies:
2072
  clsx: 2.1.1
@@ -2101,10 +2551,16 @@ snapshots:
2101
  dependencies:
2102
  ms: 2.1.2
2103
 
 
 
2104
2105
 
2106
2107
 
 
 
 
 
2108
2109
 
2110
@@ -2117,6 +2573,11 @@ snapshots:
2117
 
2118
2119
 
 
 
 
 
 
2120
2121
 
2122
@@ -2149,6 +2610,16 @@ snapshots:
2149
 
2150
2151
 
 
 
 
 
 
 
 
 
 
 
2152
2153
  dependencies:
2154
  '@nodelib/fs.stat': 2.0.5
@@ -2169,6 +2640,11 @@ snapshots:
2169
  dependencies:
2170
  to-regex-range: 5.0.1
2171
 
 
 
 
 
 
2172
2173
  dependencies:
2174
  cross-spawn: 7.0.6
@@ -2176,6 +2652,18 @@ snapshots:
2176
 
2177
2178
 
 
 
 
 
 
 
 
 
 
 
 
 
2179
2180
  optional: true
2181
 
@@ -2210,10 +2698,29 @@ snapshots:
2210
 
2211
2212
 
 
 
 
 
 
 
 
 
 
 
 
2213
2214
  dependencies:
2215
  function-bind: 1.1.2
2216
 
 
 
 
 
 
 
 
 
2217
2218
  dependencies:
2219
  binary-extensions: 2.3.0
@@ -2236,6 +2743,12 @@ snapshots:
2236
 
2237
2238
 
 
 
 
 
 
 
2239
2240
 
2241
@@ -2248,14 +2761,29 @@ snapshots:
2248
 
2249
2250
 
 
 
 
 
 
2251
2252
 
2253
2254
 
 
 
 
 
2255
2256
 
2257
2258
 
 
 
 
 
 
 
2259
2260
  dependencies:
2261
  ansi-escapes: 7.0.0
@@ -2293,6 +2821,8 @@ snapshots:
2293
 
2294
2295
 
 
 
2296
2297
 
2298
@@ -2321,8 +2851,34 @@ snapshots:
2321
  dependencies:
2322
  mimic-function: 5.0.1
2323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2324
2325
 
 
 
 
 
 
 
2326
2327
 
2328
@@ -2332,6 +2888,8 @@ snapshots:
2332
  lru-cache: 10.4.3
2333
  minipass: 7.1.2
2334
 
 
 
2335
2336
 
2337
@@ -2340,6 +2898,8 @@ snapshots:
2340
 
2341
2342
 
 
 
2343
2344
 
2345
@@ -2379,6 +2939,10 @@ snapshots:
2379
  picocolors: 1.1.1
2380
  source-map-js: 1.2.1
2381
 
 
 
 
 
2382
2383
 
2384
@@ -2424,10 +2988,19 @@ snapshots:
2424
  dependencies:
2425
  pify: 2.3.0
2426
 
 
 
 
 
 
 
 
2427
2428
  dependencies:
2429
  picomatch: 2.3.1
2430
 
 
 
2431
2432
 
2433
@@ -2473,12 +3046,16 @@ snapshots:
2473
  dependencies:
2474
  queue-microtask: 1.2.3
2475
 
 
 
2476
2477
  dependencies:
2478
  loose-envify: 1.4.0
2479
 
2480
2481
 
 
 
2482
2483
  dependencies:
2484
  '@serialport/binding-mock': 10.2.2
@@ -2506,6 +3083,8 @@ snapshots:
2506
 
2507
2508
 
 
 
2509
2510
  dependencies:
2511
  ansi-styles: 6.2.1
@@ -2513,6 +3092,13 @@ snapshots:
2513
 
2514
2515
 
 
 
 
 
 
 
 
2516
2517
  dependencies:
2518
  emoji-regex: 8.0.0
@@ -2539,6 +3125,8 @@ snapshots:
2539
  dependencies:
2540
  ansi-regex: 6.1.0
2541
 
 
 
2542
2543
  dependencies:
2544
  '@jridgewell/gen-mapping': 0.3.8
@@ -2580,6 +3168,8 @@ snapshots:
2580
  transitivePeerDependencies:
2581
  - ts-node
2582
 
 
 
2583
2584
  dependencies:
2585
  thenify: 3.3.1
@@ -2593,6 +3183,10 @@ snapshots:
2593
  fdir: 6.4.6([email protected])
2594
  picomatch: 4.0.2
2595
 
 
 
 
 
2596
2597
  dependencies:
2598
  is-number: 7.0.0
@@ -2612,6 +3206,8 @@ snapshots:
2612
 
2613
2614
 
 
 
2615
2616
  dependencies:
2617
  browserslist: 4.25.0
 
21
  specifier: ^12.0.0
22
  version: 12.0.0
23
  devDependencies:
24
+ '@changesets/cli':
25
+ specifier: ^2.29.5
26
+ version: 2.29.5
27
  '@types/node':
28
  specifier: ^22.10.5
29
  version: 22.15.31
 
73
  specifier: ^6.3.5
74
75
 
76
+ packages/web:
77
+ dependencies:
78
+ typescript:
79
+ specifier: '>=4.5.0'
80
+ version: 5.8.3
81
+
82
  packages:
83
 
84
  '@alloc/[email protected]':
 
156
  peerDependencies:
157
  '@babel/core': ^7.0.0-0
158
 
159
+ '@babel/[email protected]':
160
+ resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
161
+ engines: {node: '>=6.9.0'}
162
+
163
  '@babel/[email protected]':
164
  resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
165
  engines: {node: '>=6.9.0'}
 
172
  resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
173
  engines: {node: '>=6.9.0'}
174
 
175
+ '@changesets/[email protected]':
176
+ resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==}
177
+
178
+ '@changesets/[email protected]':
179
+ resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==}
180
+
181
+ '@changesets/[email protected]':
182
+ resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
183
+
184
+ '@changesets/[email protected]':
185
+ resolution: {integrity: sha512-0j0cPq3fgxt2dPdFsg4XvO+6L66RC0pZybT9F4dG5TBrLA3jA/1pNkdTXH9IBBVHkgsKrNKenI3n1mPyPlIydg==}
186
+ hasBin: true
187
+
188
+ '@changesets/[email protected]':
189
+ resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==}
190
+
191
+ '@changesets/[email protected]':
192
+ resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==}
193
+
194
+ '@changesets/[email protected]':
195
+ resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
196
+
197
+ '@changesets/[email protected]':
198
+ resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==}
199
+
200
+ '@changesets/[email protected]':
201
+ resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==}
202
+
203
+ '@changesets/[email protected]':
204
+ resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==}
205
+
206
+ '@changesets/[email protected]':
207
+ resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==}
208
+
209
+ '@changesets/[email protected]':
210
+ resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==}
211
+
212
+ '@changesets/[email protected]':
213
+ resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==}
214
+
215
+ '@changesets/[email protected]':
216
+ resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==}
217
+
218
+ '@changesets/[email protected]':
219
+ resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==}
220
+
221
+ '@changesets/[email protected]':
222
+ resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==}
223
+
224
+ '@changesets/[email protected]':
225
+ resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
226
+
227
+ '@changesets/[email protected]':
228
+ resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
229
+
230
  '@esbuild/[email protected]':
231
  resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
232
  engines: {node: '>=18'}
 
399
  '@jridgewell/[email protected]':
400
  resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
401
 
402
+ '@manypkg/[email protected]':
403
+ resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
404
+
405
+ '@manypkg/[email protected]':
406
+ resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
407
+
408
  '@nodelib/[email protected]':
409
  resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
410
  engines: {node: '>= 8'}
 
787
  '@types/[email protected]':
788
  resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
789
 
790
+ '@types/[email protected]':
791
+ resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
792
+
793
  '@types/[email protected]':
794
  resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
795
 
 
810
  peerDependencies:
811
  vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
812
 
813
814
+ resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
815
+ engines: {node: '>=6'}
816
+
817
818
  resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
819
  engines: {node: '>=18'}
 
844
845
  resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
846
 
847
848
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
849
+
850
851
  resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
852
  engines: {node: '>=10'}
853
 
854
855
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
856
+ engines: {node: '>=8'}
857
+
858
859
  resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==}
860
  engines: {node: ^10 || ^12 || >=14}
 
865
866
  resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
867
 
868
869
+ resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
870
+ engines: {node: '>=4'}
871
+
872
873
  resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
874
  engines: {node: '>=8'}
 
892
893
  resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
894
 
895
896
+ resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
897
+
898
899
  resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
900
  engines: {node: '>= 8.10.0'}
901
 
902
903
+ resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
904
+ engines: {node: '>=8'}
905
+
906
907
  resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
908
 
 
949
  supports-color:
950
  optional: true
951
 
952
953
+ resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
954
+ engines: {node: '>=8'}
955
+
956
957
  resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
958
 
959
960
  resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
961
 
962
963
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
964
+ engines: {node: '>=8'}
965
+
966
967
  resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
968
 
 
981
982
  resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
983
 
984
985
+ resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
986
+ engines: {node: '>=8.6'}
987
+
988
989
  resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
990
  engines: {node: '>=18'}
 
998
  resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
999
  engines: {node: '>=6'}
1000
 
1001
1002
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
1003
+ engines: {node: '>=4'}
1004
+ hasBin: true
1005
+
1006
1007
+ resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
1008
+
1009
1010
+ resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
1011
+ engines: {node: '>=4'}
1012
+
1013
1014
  resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
1015
  engines: {node: '>=8.6.0'}
 
1029
  resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
1030
  engines: {node: '>=8'}
1031
 
1032
1033
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
1034
+ engines: {node: '>=8'}
1035
+
1036
1037
  resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
1038
  engines: {node: '>=14'}
 
1040
1041
  resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
1042
 
1043
1044
+ resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
1045
+ engines: {node: '>=6 <7 || >=8'}
1046
+
1047
1048
+ resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
1049
+ engines: {node: '>=6 <7 || >=8'}
1050
+
1051
1052
  resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1053
  engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
 
1087
  resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
1088
  engines: {node: '>=4'}
1089
 
1090
1091
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
1092
+ engines: {node: '>=10'}
1093
+
1094
1095
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
1096
+
1097
1098
  resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
1099
  engines: {node: '>= 0.4'}
1100
 
1101
1102
+ resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==}
1103
+ hasBin: true
1104
+
1105
1106
+ resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
1107
+ engines: {node: '>=0.10.0'}
1108
+
1109
1110
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
1111
+ engines: {node: '>= 4'}
1112
+
1113
1114
  resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
1115
  engines: {node: '>=8'}
 
1138
  resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
1139
  engines: {node: '>=0.12.0'}
1140
 
1141
1142
+ resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
1143
+ engines: {node: '>=4'}
1144
+
1145
1146
+ resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
1147
+ engines: {node: '>=0.10.0'}
1148
+
1149
1150
  resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
1151
 
 
1159
1160
  resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
1161
 
1162
1163
+ resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
1164
+ hasBin: true
1165
+
1166
1167
  resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
1168
  engines: {node: '>=6'}
 
1173
  engines: {node: '>=6'}
1174
  hasBin: true
1175
 
1176
1177
+ resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
1178
+
1179
1180
  resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
1181
  engines: {node: '>=14'}
 
1183
1184
  resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
1185
 
1186
1187
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
1188
+ engines: {node: '>=8'}
1189
+
1190
1191
+ resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
1192
+
1193
1194
  resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
1195
  engines: {node: '>=18'}
 
1229
  resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
1230
  engines: {node: '>=16 || 14 >=14.17'}
1231
 
1232
1233
+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
1234
+ engines: {node: '>=4'}
1235
+
1236
1237
  resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
1238
 
 
1274
  resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
1275
  engines: {node: '>=18'}
1276
 
1277
1278
+ resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
1279
+ engines: {node: '>=0.10.0'}
1280
+
1281
1282
+ resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
1283
+
1284
1285
+ resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
1286
+ engines: {node: '>=8'}
1287
+
1288
1289
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
1290
+ engines: {node: '>=6'}
1291
+
1292
1293
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
1294
+ engines: {node: '>=8'}
1295
+
1296
1297
+ resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
1298
+ engines: {node: '>=6'}
1299
+
1300
1301
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
1302
+ engines: {node: '>=6'}
1303
+
1304
1305
  resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
1306
 
1307
1308
+ resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
1309
+
1310
1311
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
1312
+ engines: {node: '>=8'}
1313
+
1314
1315
  resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1316
  engines: {node: '>=8'}
 
1322
  resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
1323
  engines: {node: '>=16 || 14 >=14.18'}
1324
 
1325
1326
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
1327
+ engines: {node: '>=8'}
1328
+
1329
1330
  resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1331
 
 
1341
  resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
1342
  engines: {node: '>=0.10.0'}
1343
 
1344
1345
+ resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
1346
+ engines: {node: '>=6'}
1347
+
1348
1349
  resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
1350
  engines: {node: '>= 6'}
 
1390
  resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
1391
  engines: {node: ^10 || ^12 || >=14}
1392
 
1393
1394
+ resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
1395
+ engines: {node: '>=10.13.0'}
1396
+ hasBin: true
1397
+
1398
1399
+ resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
1400
+
1401
1402
  resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
1403
 
 
1447
1448
  resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
1449
 
1450
1451
+ resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
1452
+ engines: {node: '>=6'}
1453
+
1454
1455
  resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
1456
  engines: {node: '>=8.10.0'}
1457
 
1458
1459
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
1460
+ engines: {node: '>=8'}
1461
+
1462
1463
  resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
1464
 
 
1483
1484
  resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1485
 
1486
1487
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
1488
+
1489
1490
  resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
1491
 
 
1493
  resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
1494
  hasBin: true
1495
 
1496
1497
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
1498
+ engines: {node: '>=10'}
1499
+ hasBin: true
1500
+
1501
1502
  resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
1503
  engines: {node: '>=16.0.0'}
 
1514
  resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
1515
  engines: {node: '>=14'}
1516
 
1517
1518
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
1519
+ engines: {node: '>=8'}
1520
+
1521
1522
  resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
1523
  engines: {node: '>=18'}
 
1526
  resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1527
  engines: {node: '>=0.10.0'}
1528
 
1529
1530
+ resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
1531
+
1532
1533
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
1534
+
1535
1536
  resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
1537
  engines: {node: '>=8'}
 
1552
  resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
1553
  engines: {node: '>=12'}
1554
 
1555
1556
+ resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
1557
+ engines: {node: '>=4'}
1558
+
1559
1560
  resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
1561
  engines: {node: '>=16 || 14 >=14.17'}
 
1573
  engines: {node: '>=14.0.0'}
1574
  hasBin: true
1575
 
1576
1577
+ resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
1578
+ engines: {node: '>=8'}
1579
+
1580
1581
  resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
1582
  engines: {node: '>=0.8'}
 
1588
  resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
1589
  engines: {node: '>=12.0.0'}
1590
 
1591
1592
+ resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
1593
+ engines: {node: '>=0.6.0'}
1594
+
1595
1596
  resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1597
  engines: {node: '>=8.0'}
 
1615
1616
  resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
1617
 
1618
1619
+ resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
1620
+ engines: {node: '>= 4.0.0'}
1621
+
1622
1623
  resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
1624
  hasBin: true
 
1809
  '@babel/core': 7.27.4
1810
  '@babel/helper-plugin-utils': 7.27.1
1811
 
1812
+ '@babel/[email protected]': {}
1813
+
1814
  '@babel/[email protected]':
1815
  dependencies:
1816
  '@babel/code-frame': 7.27.1
 
1834
  '@babel/helper-string-parser': 7.27.1
1835
  '@babel/helper-validator-identifier': 7.27.1
1836
 
1837
+ '@changesets/[email protected]':
1838
+ dependencies:
1839
+ '@changesets/config': 3.1.1
1840
+ '@changesets/get-version-range-type': 0.4.0
1841
+ '@changesets/git': 3.0.4
1842
+ '@changesets/should-skip-package': 0.1.2
1843
+ '@changesets/types': 6.1.0
1844
+ '@manypkg/get-packages': 1.1.3
1845
+ detect-indent: 6.1.0
1846
+ fs-extra: 7.0.1
1847
+ lodash.startcase: 4.4.0
1848
+ outdent: 0.5.0
1849
+ prettier: 2.8.8
1850
+ resolve-from: 5.0.0
1851
+ semver: 7.7.2
1852
+
1853
+ '@changesets/[email protected]':
1854
+ dependencies:
1855
+ '@changesets/errors': 0.2.0
1856
+ '@changesets/get-dependents-graph': 2.1.3
1857
+ '@changesets/should-skip-package': 0.1.2
1858
+ '@changesets/types': 6.1.0
1859
+ '@manypkg/get-packages': 1.1.3
1860
+ semver: 7.7.2
1861
+
1862
+ '@changesets/[email protected]':
1863
+ dependencies:
1864
+ '@changesets/types': 6.1.0
1865
+
1866
+ '@changesets/[email protected]':
1867
+ dependencies:
1868
+ '@changesets/apply-release-plan': 7.0.12
1869
+ '@changesets/assemble-release-plan': 6.0.9
1870
+ '@changesets/changelog-git': 0.2.1
1871
+ '@changesets/config': 3.1.1
1872
+ '@changesets/errors': 0.2.0
1873
+ '@changesets/get-dependents-graph': 2.1.3
1874
+ '@changesets/get-release-plan': 4.0.13
1875
+ '@changesets/git': 3.0.4
1876
+ '@changesets/logger': 0.1.1
1877
+ '@changesets/pre': 2.0.2
1878
+ '@changesets/read': 0.6.5
1879
+ '@changesets/should-skip-package': 0.1.2
1880
+ '@changesets/types': 6.1.0
1881
+ '@changesets/write': 0.4.0
1882
+ '@manypkg/get-packages': 1.1.3
1883
+ ansi-colors: 4.1.3
1884
+ ci-info: 3.9.0
1885
+ enquirer: 2.4.1
1886
+ external-editor: 3.1.0
1887
+ fs-extra: 7.0.1
1888
+ mri: 1.2.0
1889
+ p-limit: 2.3.0
1890
+ package-manager-detector: 0.2.11
1891
+ picocolors: 1.1.1
1892
+ resolve-from: 5.0.0
1893
+ semver: 7.7.2
1894
+ spawndamnit: 3.0.1
1895
+ term-size: 2.2.1
1896
+
1897
+ '@changesets/[email protected]':
1898
+ dependencies:
1899
+ '@changesets/errors': 0.2.0
1900
+ '@changesets/get-dependents-graph': 2.1.3
1901
+ '@changesets/logger': 0.1.1
1902
+ '@changesets/types': 6.1.0
1903
+ '@manypkg/get-packages': 1.1.3
1904
+ fs-extra: 7.0.1
1905
+ micromatch: 4.0.8
1906
+
1907
+ '@changesets/[email protected]':
1908
+ dependencies:
1909
+ extendable-error: 0.1.7
1910
+
1911
+ '@changesets/[email protected]':
1912
+ dependencies:
1913
+ '@changesets/types': 6.1.0
1914
+ '@manypkg/get-packages': 1.1.3
1915
+ picocolors: 1.1.1
1916
+ semver: 7.7.2
1917
+
1918
+ '@changesets/[email protected]':
1919
+ dependencies:
1920
+ '@changesets/assemble-release-plan': 6.0.9
1921
+ '@changesets/config': 3.1.1
1922
+ '@changesets/pre': 2.0.2
1923
+ '@changesets/read': 0.6.5
1924
+ '@changesets/types': 6.1.0
1925
+ '@manypkg/get-packages': 1.1.3
1926
+
1927
+ '@changesets/[email protected]': {}
1928
+
1929
+ '@changesets/[email protected]':
1930
+ dependencies:
1931
+ '@changesets/errors': 0.2.0
1932
+ '@manypkg/get-packages': 1.1.3
1933
+ is-subdir: 1.2.0
1934
+ micromatch: 4.0.8
1935
+ spawndamnit: 3.0.1
1936
+
1937
+ '@changesets/[email protected]':
1938
+ dependencies:
1939
+ picocolors: 1.1.1
1940
+
1941
+ '@changesets/[email protected]':
1942
+ dependencies:
1943
+ '@changesets/types': 6.1.0
1944
+ js-yaml: 3.14.1
1945
+
1946
+ '@changesets/[email protected]':
1947
+ dependencies:
1948
+ '@changesets/errors': 0.2.0
1949
+ '@changesets/types': 6.1.0
1950
+ '@manypkg/get-packages': 1.1.3
1951
+ fs-extra: 7.0.1
1952
+
1953
+ '@changesets/[email protected]':
1954
+ dependencies:
1955
+ '@changesets/git': 3.0.4
1956
+ '@changesets/logger': 0.1.1
1957
+ '@changesets/parse': 0.4.1
1958
+ '@changesets/types': 6.1.0
1959
+ fs-extra: 7.0.1
1960
+ p-filter: 2.1.0
1961
+ picocolors: 1.1.1
1962
+
1963
+ '@changesets/[email protected]':
1964
+ dependencies:
1965
+ '@changesets/types': 6.1.0
1966
+ '@manypkg/get-packages': 1.1.3
1967
+
1968
+ '@changesets/[email protected]': {}
1969
+
1970
+ '@changesets/[email protected]': {}
1971
+
1972
+ '@changesets/[email protected]':
1973
+ dependencies:
1974
+ '@changesets/types': 6.1.0
1975
+ fs-extra: 7.0.1
1976
+ human-id: 4.1.1
1977
+ prettier: 2.8.8
1978
+
1979
  '@esbuild/[email protected]':
1980
  optional: true
1981
 
 
2077
  '@jridgewell/resolve-uri': 3.1.2
2078
  '@jridgewell/sourcemap-codec': 1.5.0
2079
 
2080
+ '@manypkg/[email protected]':
2081
+ dependencies:
2082
+ '@babel/runtime': 7.27.6
2083
+ '@types/node': 12.20.55
2084
+ find-up: 4.1.0
2085
+ fs-extra: 8.1.0
2086
+
2087
+ '@manypkg/[email protected]':
2088
+ dependencies:
2089
+ '@babel/runtime': 7.27.6
2090
+ '@changesets/types': 4.1.0
2091
+ '@manypkg/find-root': 1.1.0
2092
+ fs-extra: 8.1.0
2093
+ globby: 11.1.0
2094
+ read-yaml-file: 1.1.0
2095
+
2096
  '@nodelib/[email protected]':
2097
  dependencies:
2098
  '@nodelib/fs.stat': 2.0.5
 
2400
 
2401
  '@types/[email protected]': {}
2402
 
2403
+ '@types/[email protected]': {}
2404
+
2405
  '@types/[email protected]':
2406
  dependencies:
2407
  undici-types: 6.21.0
 
2429
  transitivePeerDependencies:
2430
  - supports-color
2431
 
2432
2433
+
2434
2435
  dependencies:
2436
  environment: 1.1.0
 
2454
 
2455
2456
 
2457
2458
+ dependencies:
2459
+ sprintf-js: 1.0.3
2460
+
2461
2462
  dependencies:
2463
  tslib: 2.8.1
2464
 
2465
2466
+
2467
2468
  dependencies:
2469
  browserslist: 4.25.0
 
2476
 
2477
2478
 
2479
2480
+ dependencies:
2481
+ is-windows: 1.0.2
2482
+
2483
2484
 
2485
 
2501
 
2502
2503
 
2504
2505
+
2506
2507
  dependencies:
2508
  anymatch: 3.1.3
 
2515
  optionalDependencies:
2516
  fsevents: 2.3.3
2517
 
2518
2519
+
2520
2521
  dependencies:
2522
  clsx: 2.1.1
 
2551
  dependencies:
2552
  ms: 2.1.2
2553
 
2554
2555
+
2556
2557
 
2558
2559
 
2560
2561
+ dependencies:
2562
+ path-type: 4.0.0
2563
+
2564
2565
 
2566
 
2573
 
2574
2575
 
2576
2577
+ dependencies:
2578
+ ansi-colors: 4.1.3
2579
+ strip-ansi: 6.0.1
2580
+
2581
2582
 
2583
 
2610
 
2611
2612
 
2613
2614
+
2615
2616
+
2617
2618
+ dependencies:
2619
+ chardet: 0.7.0
2620
+ iconv-lite: 0.4.24
2621
+ tmp: 0.0.33
2622
+
2623
2624
  dependencies:
2625
  '@nodelib/fs.stat': 2.0.5
 
2640
  dependencies:
2641
  to-regex-range: 5.0.1
2642
 
2643
2644
+ dependencies:
2645
+ locate-path: 5.0.0
2646
+ path-exists: 4.0.0
2647
+
2648
2649
  dependencies:
2650
  cross-spawn: 7.0.6
 
2652
 
2653
2654
 
2655
2656
+ dependencies:
2657
+ graceful-fs: 4.2.11
2658
+ jsonfile: 4.0.0
2659
+ universalify: 0.1.2
2660
+
2661
2662
+ dependencies:
2663
+ graceful-fs: 4.2.11
2664
+ jsonfile: 4.0.0
2665
+ universalify: 0.1.2
2666
+
2667
2668
  optional: true
2669
 
 
2698
 
2699
2700
 
2701
2702
+ dependencies:
2703
+ array-union: 2.1.0
2704
+ dir-glob: 3.0.1
2705
+ fast-glob: 3.3.3
2706
+ ignore: 5.3.2
2707
+ merge2: 1.4.1
2708
+ slash: 3.0.0
2709
+
2710
2711
+
2712
2713
  dependencies:
2714
  function-bind: 1.1.2
2715
 
2716
2717
+
2718
2719
+ dependencies:
2720
+ safer-buffer: 2.1.2
2721
+
2722
2723
+
2724
2725
  dependencies:
2726
  binary-extensions: 2.3.0
 
2743
 
2744
2745
 
2746
2747
+ dependencies:
2748
+ better-path-resolve: 1.0.0
2749
+
2750
2751
+
2752
2753
 
2754
 
2761
 
2762
2763
 
2764
2765
+ dependencies:
2766
+ argparse: 1.0.10
2767
+ esprima: 4.0.1
2768
+
2769
2770
 
2771
2772
 
2773
2774
+ optionalDependencies:
2775
+ graceful-fs: 4.2.11
2776
+
2777
2778
 
2779
2780
 
2781
2782
+ dependencies:
2783
+ p-locate: 4.1.0
2784
+
2785
2786
+
2787
2788
  dependencies:
2789
  ansi-escapes: 7.0.0
 
2821
 
2822
2823
 
2824
2825
+
2826
2827
 
2828
 
2851
  dependencies:
2852
  mimic-function: 5.0.1
2853
 
2854
2855
+
2856
2857
+
2858
2859
+ dependencies:
2860
+ p-map: 2.1.0
2861
+
2862
2863
+ dependencies:
2864
+ p-try: 2.2.0
2865
+
2866
2867
+ dependencies:
2868
+ p-limit: 2.3.0
2869
+
2870
2871
+
2872
2873
+
2874
2875
 
2876
2877
+ dependencies:
2878
+ quansync: 0.2.10
2879
+
2880
2881
+
2882
2883
 
2884
 
2888
  lru-cache: 10.4.3
2889
  minipass: 7.1.2
2890
 
2891
2892
+
2893
2894
 
2895
 
2898
 
2899
2900
 
2901
2902
+
2903
2904
 
2905
 
2939
  picocolors: 1.1.1
2940
  source-map-js: 1.2.1
2941
 
2942
2943
+
2944
2945
+
2946
2947
 
2948
 
2988
  dependencies:
2989
  pify: 2.3.0
2990
 
2991
2992
+ dependencies:
2993
+ graceful-fs: 4.2.11
2994
+ js-yaml: 3.14.1
2995
+ pify: 4.0.1
2996
+ strip-bom: 3.0.0
2997
+
2998
2999
  dependencies:
3000
  picomatch: 2.3.1
3001
 
3002
3003
+
3004
3005
 
3006
 
3046
  dependencies:
3047
  queue-microtask: 1.2.3
3048
 
3049
3050
+
3051
3052
  dependencies:
3053
  loose-envify: 1.4.0
3054
 
3055
3056
 
3057
3058
+
3059
3060
  dependencies:
3061
  '@serialport/binding-mock': 10.2.2
 
3083
 
3084
3085
 
3086
3087
+
3088
3089
  dependencies:
3090
  ansi-styles: 6.2.1
 
3092
 
3093
3094
 
3095
3096
+ dependencies:
3097
+ cross-spawn: 7.0.6
3098
+ signal-exit: 4.1.0
3099
+
3100
3101
+
3102
3103
  dependencies:
3104
  emoji-regex: 8.0.0
 
3125
  dependencies:
3126
  ansi-regex: 6.1.0
3127
 
3128
3129
+
3130
3131
  dependencies:
3132
  '@jridgewell/gen-mapping': 0.3.8
 
3168
  transitivePeerDependencies:
3169
  - ts-node
3170
 
3171
3172
+
3173
3174
  dependencies:
3175
  thenify: 3.3.1
 
3183
  fdir: 6.4.6([email protected])
3184
  picomatch: 4.0.2
3185
 
3186
3187
+ dependencies:
3188
+ os-tmpdir: 1.0.2
3189
+
3190
3191
  dependencies:
3192
  is-number: 7.0.0
 
3206
 
3207
3208
 
3209
3210
+
3211
3212
  dependencies:
3213
  browserslist: 4.25.0
pnpm-workspace.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ packages:
2
+ - "packages/*"
src/demo/App.tsx CHANGED
@@ -1,7 +1,7 @@
1
  import { useState } from "react";
2
  import { Home } from "./pages/Home";
3
  import { ErrorBoundary } from "./components/ErrorBoundary";
4
- import type { RobotConnection } from "../lerobot/web/types/robot-connection.js";
5
 
6
  export function App() {
7
  const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
 
1
  import { useState } from "react";
2
  import { Home } from "./pages/Home";
3
  import { ErrorBoundary } from "./components/ErrorBoundary";
4
+ import type { RobotConnection } from "@lerobot/web";
5
 
6
  export function App() {
7
  const [connectedRobots, setConnectedRobots] = useState<RobotConnection[]>([]);
src/demo/components/CalibrationPanel.tsx CHANGED
@@ -13,12 +13,12 @@ import {
13
  type WebCalibrationResults,
14
  type LiveCalibrationData,
15
  type CalibrationProcess,
16
- } from "../../lerobot/web/calibrate";
17
- import { releaseMotors } from "../../lerobot/web/utils/motor-communication.js";
18
- import { WebSerialPortWrapper } from "../../lerobot/web/utils/serial-port-wrapper.js";
19
- import { createSO100Config } from "../../lerobot/web/robots/so100_config.js";
20
  import { CalibrationModal } from "./CalibrationModal";
21
- import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
22
 
23
  interface CalibrationPanelProps {
24
  robot: RobotConnection;
 
13
  type WebCalibrationResults,
14
  type LiveCalibrationData,
15
  type CalibrationProcess,
16
+ } from "@lerobot/web";
17
+ import { releaseMotors } from "@lerobot/web";
18
+ import { WebSerialPortWrapper } from "@lerobot/web";
19
+ import { createSO100Config } from "@lerobot/web";
20
  import { CalibrationModal } from "./CalibrationModal";
21
+ import type { RobotConnection } from "@lerobot/web";
22
 
23
  interface CalibrationPanelProps {
24
  robot: RobotConnection;
src/demo/components/PortManager.tsx CHANGED
@@ -17,8 +17,8 @@ import {
17
  DialogHeader,
18
  DialogTitle,
19
  } from "./ui/dialog";
20
- import { isWebSerialSupported } from "../../lerobot/web/calibrate";
21
- import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
22
 
23
  /**
24
  * Type definitions for WebSerial API (missing from TypeScript)
@@ -464,7 +464,7 @@ export function PortManager({
464
  setError(null);
465
 
466
  // Use the new findPort API from standard library
467
- const { findPort } = await import("../../lerobot/web/find_port.js");
468
 
469
  const findPortProcess = await findPort({
470
  onMessage: (message) => {
 
17
  DialogHeader,
18
  DialogTitle,
19
  } from "./ui/dialog";
20
+ import { isWebSerialSupported } from "@lerobot/web";
21
+ import type { RobotConnection } from "@lerobot/web";
22
 
23
  /**
24
  * Type definitions for WebSerial API (missing from TypeScript)
 
464
  setError(null);
465
 
466
  // Use the new findPort API from standard library
467
+ const { findPort } = await import("@lerobot/web");
468
 
469
  const findPortProcess = await findPort({
470
  onMessage: (message) => {
src/demo/components/TeleoperationPanel.tsx CHANGED
@@ -7,10 +7,10 @@ import {
7
  teleoperate,
8
  type TeleoperationProcess,
9
  type TeleoperationState,
10
- } from "../../lerobot/web/teleoperate.js";
11
  import { getUnifiedRobotData } from "../lib/unified-storage";
12
- import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
13
- import { SO100_KEYBOARD_CONTROLS } from "../../lerobot/web/robots/so100_config.js";
14
 
15
  interface TeleoperationPanelProps {
16
  robot: RobotConnection;
 
7
  teleoperate,
8
  type TeleoperationProcess,
9
  type TeleoperationState,
10
+ } from "@lerobot/web";
11
  import { getUnifiedRobotData } from "../lib/unified-storage";
12
+ import type { RobotConnection } from "@lerobot/web";
13
+ import { SO100_KEYBOARD_CONTROLS } from "@lerobot/web";
14
 
15
  interface TeleoperationPanelProps {
16
  robot: RobotConnection;
src/demo/pages/Home.tsx CHANGED
@@ -4,8 +4,8 @@ import { Alert, AlertDescription } from "../components/ui/alert";
4
  import { PortManager } from "../components/PortManager";
5
  import { CalibrationPanel } from "../components/CalibrationPanel";
6
  import { TeleoperationPanel } from "../components/TeleoperationPanel";
7
- import { isWebSerialSupported } from "../../lerobot/web/calibrate";
8
- import type { RobotConnection } from "../../lerobot/web/types/robot-connection.js";
9
 
10
  interface HomeProps {
11
  connectedRobots: RobotConnection[];
@@ -48,7 +48,7 @@ export function Home({ connectedRobots, onConnectedRobotsChange }: HomeProps) {
48
  πŸ€– LeRobot.js
49
  </h1>
50
  <p className="text-xl text-gray-600 mb-8">
51
- State-of-the-art AI for real-world robotics in JavaScript
52
  </p>
53
 
54
  {!isSupported && (
 
4
  import { PortManager } from "../components/PortManager";
5
  import { CalibrationPanel } from "../components/CalibrationPanel";
6
  import { TeleoperationPanel } from "../components/TeleoperationPanel";
7
+ import { isWebSerialSupported } from "@lerobot/web";
8
+ import type { RobotConnection } from "@lerobot/web";
9
 
10
  interface HomeProps {
11
  connectedRobots: RobotConnection[];
 
48
  πŸ€– LeRobot.js
49
  </h1>
50
  <p className="text-xl text-gray-600 mb-8">
51
+ Robotics for the web and node
52
  </p>
53
 
54
  {!isSupported && (
src/lerobot/node/calibrate.ts DELETED
@@ -1,246 +0,0 @@
1
- /**
2
- * Helper to recalibrate your device (robot or teleoperator).
3
- *
4
- * Example:
5
- * ```
6
- * npx lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm
7
- * ```
8
- */
9
-
10
- import { createSO100Follower } from "./robots/so100_follower.js";
11
- import { createSO100Leader } from "./teleoperators/so100_leader.js";
12
- import {
13
- initializeDeviceCommunication,
14
- readMotorPositions,
15
- performInteractiveCalibration,
16
- setMotorLimits,
17
- verifyCalibration,
18
- } from "./common/calibration.js";
19
- import type { CalibrateConfig } from "./types/robot-config.js";
20
- import type { CalibrationResults } from "./types/calibration.js";
21
- import { getSO100Config } from "./common/so100_config.js";
22
-
23
- /**
24
- * Main calibrate function
25
- * Mirrors Python lerobot calibrate.py calibrate() function
26
- * Uses shared calibration procedures instead of device-specific implementations
27
- */
28
- export async function calibrate(config: CalibrateConfig): Promise<void> {
29
- // Validate configuration - exactly one device must be specified
30
- if (Boolean(config.robot) === Boolean(config.teleop)) {
31
- throw new Error("Choose either a robot or a teleop.");
32
- }
33
-
34
- const deviceConfig = config.robot || config.teleop!;
35
-
36
- let device;
37
- let calibrationResults: CalibrationResults;
38
-
39
- try {
40
- // Create device for connection management only
41
- if (config.robot) {
42
- switch (config.robot.type) {
43
- case "so100_follower":
44
- device = createSO100Follower(config.robot);
45
- break;
46
- default:
47
- throw new Error(`Unsupported robot type: ${config.robot.type}`);
48
- }
49
- } else if (config.teleop) {
50
- switch (config.teleop.type) {
51
- case "so100_leader":
52
- device = createSO100Leader(config.teleop);
53
- break;
54
- default:
55
- throw new Error(
56
- `Unsupported teleoperator type: ${config.teleop.type}`
57
- );
58
- }
59
- }
60
-
61
- if (!device) {
62
- throw new Error("Failed to create device");
63
- }
64
-
65
- // Connect to device (silent unless error)
66
- await device.connect(false); // calibrate=False like Python
67
-
68
- // Get SO-100 calibration configuration
69
- const so100Config = getSO100Config(
70
- deviceConfig.type as "so100_follower" | "so100_leader",
71
- (device as any).port
72
- );
73
-
74
- // Perform shared calibration procedures (silent unless error)
75
- await initializeDeviceCommunication(so100Config);
76
- await setMotorLimits(so100Config);
77
-
78
- // Interactive calibration with live updates - THE MAIN PART
79
- calibrationResults = await performInteractiveCalibration(so100Config);
80
-
81
- // Save and cleanup (silent unless error)
82
- await verifyCalibration(so100Config);
83
- await (device as any).saveCalibration(calibrationResults);
84
- await device.disconnect();
85
- } catch (error) {
86
- // Ensure we disconnect even if there's an error
87
- if (device) {
88
- try {
89
- await device.disconnect();
90
- } catch (disconnectError) {
91
- console.warn("Warning: Failed to disconnect properly");
92
- }
93
- }
94
- throw error;
95
- }
96
- }
97
-
98
- /**
99
- * Parse command line arguments in Python argparse style
100
- * Handles --robot.type=so100_follower --robot.port=COM4 format
101
- */
102
- export function parseArgs(args: string[]): CalibrateConfig {
103
- const config: CalibrateConfig = {};
104
-
105
- for (const arg of args) {
106
- if (arg.startsWith("--robot.")) {
107
- if (!config.robot) {
108
- config.robot = { type: "so100_follower", port: "" };
109
- }
110
-
111
- const [key, value] = arg.substring(8).split("=");
112
- switch (key) {
113
- case "type":
114
- if (value !== "so100_follower") {
115
- throw new Error(`Unsupported robot type: ${value}`);
116
- }
117
- config.robot.type = value as "so100_follower";
118
- break;
119
- case "port":
120
- config.robot.port = value;
121
- break;
122
- case "id":
123
- config.robot.id = value;
124
- break;
125
- case "disable_torque_on_disconnect":
126
- config.robot.disable_torque_on_disconnect = value === "true";
127
- break;
128
- case "max_relative_target":
129
- config.robot.max_relative_target = value ? parseInt(value) : null;
130
- break;
131
- case "use_degrees":
132
- config.robot.use_degrees = value === "true";
133
- break;
134
- default:
135
- throw new Error(`Unknown robot parameter: ${key}`);
136
- }
137
- } else if (arg.startsWith("--teleop.")) {
138
- if (!config.teleop) {
139
- config.teleop = { type: "so100_leader", port: "" };
140
- }
141
-
142
- const [key, value] = arg.substring(9).split("=");
143
- switch (key) {
144
- case "type":
145
- if (value !== "so100_leader") {
146
- throw new Error(`Unsupported teleoperator type: ${value}`);
147
- }
148
- config.teleop.type = value as "so100_leader";
149
- break;
150
- case "port":
151
- config.teleop.port = value;
152
- break;
153
- case "id":
154
- config.teleop.id = value;
155
- break;
156
- default:
157
- throw new Error(`Unknown teleoperator parameter: ${key}`);
158
- }
159
- } else if (arg === "--help" || arg === "-h") {
160
- showUsage();
161
- process.exit(0);
162
- } else if (!arg.startsWith("--")) {
163
- // Skip non-option arguments
164
- continue;
165
- } else {
166
- throw new Error(`Unknown argument: ${arg}`);
167
- }
168
- }
169
-
170
- // Validate required fields
171
- if (config.robot && !config.robot.port) {
172
- throw new Error("Robot port is required (--robot.port=PORT)");
173
- }
174
- if (config.teleop && !config.teleop.port) {
175
- throw new Error("Teleoperator port is required (--teleop.port=PORT)");
176
- }
177
-
178
- return config;
179
- }
180
-
181
- /**
182
- * Show usage information matching Python argparse output
183
- */
184
- function showUsage(): void {
185
- console.log("Usage: lerobot calibrate [options]");
186
- console.log("");
187
- console.log("Recalibrate your device (robot or teleoperator)");
188
- console.log("");
189
- console.log("Options:");
190
- console.log(" --robot.type=TYPE Robot type (so100_follower)");
191
- console.log(
192
- " --robot.port=PORT Robot serial port (e.g., COM4, /dev/ttyUSB0)"
193
- );
194
- console.log(" --robot.id=ID Robot identifier");
195
- console.log(" --teleop.type=TYPE Teleoperator type (so100_leader)");
196
- console.log(" --teleop.port=PORT Teleoperator serial port");
197
- console.log(" --teleop.id=ID Teleoperator identifier");
198
- console.log(" -h, --help Show this help message");
199
- console.log("");
200
- console.log("Examples:");
201
- console.log(
202
- " lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm"
203
- );
204
- console.log(
205
- " lerobot calibrate --teleop.type=so100_leader --teleop.port=COM3 --teleop.id=my_leader_arm"
206
- );
207
- console.log("");
208
- console.log("Use 'lerobot find-port' to discover available ports.");
209
- }
210
-
211
- /**
212
- * CLI entry point when called directly
213
- * Mirrors Python's if __name__ == "__main__": pattern
214
- */
215
- export async function main(args: string[]): Promise<void> {
216
- try {
217
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
218
- showUsage();
219
- return;
220
- }
221
-
222
- const config = parseArgs(args);
223
- await calibrate(config);
224
- } catch (error) {
225
- if (error instanceof Error) {
226
- console.error("Error:", error.message);
227
- } else {
228
- console.error("Error:", error);
229
- }
230
-
231
- console.error("");
232
- console.error("Please verify:");
233
- console.error("1. The device is connected to the specified port");
234
- console.error("2. No other application is using the port");
235
- console.error("3. You have permission to access the port");
236
- console.error("");
237
- console.error("Use 'lerobot find-port' to discover available ports.");
238
-
239
- process.exit(1);
240
- }
241
- }
242
-
243
- if (import.meta.url === `file://${process.argv[1]}`) {
244
- const args = process.argv.slice(2);
245
- main(args);
246
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/common/calibration.ts DELETED
@@ -1,694 +0,0 @@
1
- /**
2
- * Shared calibration procedures for SO-100 devices (both leader and follower)
3
- * Mirrors Python lerobot calibrate.py common functionality
4
- *
5
- * Both SO-100 leader and follower use the same STS3215 servos and calibration procedures,
6
- * only differing in configuration parameters (drive modes, limits, etc.)
7
- */
8
-
9
- import * as readline from "readline";
10
- import { SerialPort } from "serialport";
11
- import logUpdate from "log-update";
12
-
13
- /**
14
- * Sign-magnitude encoding functions for Feetech STS3215 motors
15
- * Mirrors Python lerobot/common/utils/encoding_utils.py
16
- */
17
-
18
- /**
19
- * Encode a signed integer using sign-magnitude format
20
- * Bit at sign_bit_index represents sign (0=positive, 1=negative)
21
- * Lower bits represent magnitude
22
- */
23
- function encodeSignMagnitude(value: number, signBitIndex: number): number {
24
- const maxMagnitude = (1 << signBitIndex) - 1;
25
- const magnitude = Math.abs(value);
26
-
27
- if (magnitude > maxMagnitude) {
28
- throw new Error(
29
- `Magnitude ${magnitude} exceeds ${maxMagnitude} (max for signBitIndex=${signBitIndex})`
30
- );
31
- }
32
-
33
- const directionBit = value < 0 ? 1 : 0;
34
- return (directionBit << signBitIndex) | magnitude;
35
- }
36
-
37
- /**
38
- * Decode a sign-magnitude encoded value back to signed integer
39
- * Extracts sign bit and magnitude, then applies sign
40
- */
41
- function decodeSignMagnitude(
42
- encodedValue: number,
43
- signBitIndex: number
44
- ): number {
45
- const directionBit = (encodedValue >> signBitIndex) & 1;
46
- const magnitudeMask = (1 << signBitIndex) - 1;
47
- const magnitude = encodedValue & magnitudeMask;
48
- return directionBit ? -magnitude : magnitude;
49
- }
50
-
51
- /**
52
- * Device configuration for calibration
53
- * Despite the "SO100" name, this interface is now device-agnostic and configurable
54
- * for any robot using similar serial protocols (Feetech STS3215, etc.)
55
- */
56
- import type {
57
- SO100CalibrationConfig,
58
- CalibrationResults,
59
- } from "../types/calibration.js";
60
-
61
- /**
62
- * Initialize device communication
63
- * Common for both SO-100 leader and follower (same hardware)
64
- */
65
- export async function initializeDeviceCommunication(
66
- config: SO100CalibrationConfig
67
- ): Promise<void> {
68
- try {
69
- // Test ping to servo ID 1 (same protocol for all SO-100 devices)
70
- const pingPacket = Buffer.from([0xff, 0xff, 0x01, 0x02, 0x01, 0xfb]);
71
-
72
- if (!config.port || !config.port.isOpen) {
73
- throw new Error("Serial port not open");
74
- }
75
-
76
- await new Promise<void>((resolve, reject) => {
77
- config.port.write(pingPacket, (error) => {
78
- if (error) {
79
- reject(new Error(`Failed to send ping: ${error.message}`));
80
- } else {
81
- resolve();
82
- }
83
- });
84
- });
85
-
86
- try {
87
- await readData(config.port, 1000);
88
- } catch (error) {
89
- // Silent - no response expected for basic test
90
- }
91
- } catch (error) {
92
- throw new Error(
93
- `Serial communication test failed: ${
94
- error instanceof Error ? error.message : error
95
- }`
96
- );
97
- }
98
- }
99
-
100
- /**
101
- * Read current motor positions
102
- * Uses device-specific protocol - configurable for different robot types
103
- */
104
- export async function readMotorPositions(
105
- config: SO100CalibrationConfig,
106
- quiet: boolean = false
107
- ): Promise<number[]> {
108
- const motorPositions: number[] = [];
109
-
110
- for (let i = 0; i < config.motorIds.length; i++) {
111
- const motorId = config.motorIds[i];
112
- const motorName = config.motorNames[i];
113
-
114
- try {
115
- // Create Read Position packet using configurable address
116
- const packet = Buffer.from([
117
- 0xff,
118
- 0xff,
119
- motorId,
120
- 0x04,
121
- 0x02,
122
- config.protocol.presentPositionAddress, // Configurable address instead of hardcoded 0x38
123
- 0x02,
124
- 0x00,
125
- ]);
126
- const checksum =
127
- ~(
128
- motorId +
129
- 0x04 +
130
- 0x02 +
131
- config.protocol.presentPositionAddress +
132
- 0x02
133
- ) & 0xff;
134
- packet[7] = checksum;
135
-
136
- if (!config.port || !config.port.isOpen) {
137
- throw new Error("Serial port not open");
138
- }
139
-
140
- await new Promise<void>((resolve, reject) => {
141
- config.port.write(packet, (error) => {
142
- if (error) {
143
- reject(new Error(`Failed to send read packet: ${error.message}`));
144
- } else {
145
- resolve();
146
- }
147
- });
148
- });
149
-
150
- try {
151
- const response = await readData(config.port, 100); // Faster timeout for 30Hz performance
152
- if (response.length >= 7) {
153
- const id = response[2];
154
- const error = response[4];
155
- if (id === motorId && error === 0) {
156
- const position = response[5] | (response[6] << 8);
157
- motorPositions.push(position);
158
- } else {
159
- // Use half of max resolution as fallback instead of hardcoded 2047
160
- motorPositions.push(
161
- Math.floor((config.protocol.resolution - 1) / 2)
162
- );
163
- }
164
- } else {
165
- motorPositions.push(Math.floor((config.protocol.resolution - 1) / 2));
166
- }
167
- } catch (readError) {
168
- motorPositions.push(Math.floor((config.protocol.resolution - 1) / 2));
169
- }
170
- } catch (error) {
171
- motorPositions.push(Math.floor((config.protocol.resolution - 1) / 2));
172
- }
173
-
174
- // Minimal delay between servo reads for 30Hz performance
175
- await new Promise((resolve) => setTimeout(resolve, 2));
176
- }
177
-
178
- return motorPositions;
179
- }
180
-
181
- /**
182
- * Interactive calibration procedure
183
- * Same flow for both leader and follower, just different configurations
184
- */
185
- export async function performInteractiveCalibration(
186
- config: SO100CalibrationConfig
187
- ): Promise<CalibrationResults> {
188
- // Step 1: Set homing position
189
- await promptUser(
190
- `Move the SO-100 ${config.deviceType} to the MIDDLE of its range of motion and press ENTER...`
191
- );
192
-
193
- const homingOffsets = await setHomingOffsets(config);
194
-
195
- // Step 2: Record ranges of motion with live updates
196
- const { rangeMins, rangeMaxes } = await recordRangesOfMotion(config);
197
-
198
- // Step 3: Set special range for wrist_roll (full turn motor)
199
- rangeMins["wrist_roll"] = 0;
200
- rangeMaxes["wrist_roll"] = 4095;
201
-
202
- // Step 4: Write hardware position limits to motors (matching Python behavior)
203
- await writeHardwarePositionLimits(config, rangeMins, rangeMaxes);
204
-
205
- // Compile results in Python-compatible format
206
- const results: CalibrationResults = {};
207
-
208
- for (let i = 0; i < config.motorNames.length; i++) {
209
- const motorName = config.motorNames[i];
210
- const motorId = config.motorIds[i];
211
-
212
- results[motorName] = {
213
- id: motorId,
214
- drive_mode: config.driveModes[i],
215
- homing_offset: homingOffsets[motorName],
216
- range_min: rangeMins[motorName],
217
- range_max: rangeMaxes[motorName],
218
- };
219
- }
220
-
221
- return results;
222
- }
223
-
224
- /**
225
- * Set motor limits (device-specific)
226
- */
227
- export async function setMotorLimits(
228
- config: SO100CalibrationConfig
229
- ): Promise<void> {
230
- // Silent unless error - motor limits configured internally
231
- }
232
-
233
- /**
234
- * Verify calibration was successful
235
- */
236
- export async function verifyCalibration(
237
- config: SO100CalibrationConfig
238
- ): Promise<void> {
239
- // Silent unless error - calibration verification passed internally
240
- }
241
-
242
- /**
243
- * Reset homing offsets to 0 for all motors
244
- * Mirrors Python reset_calibration() - critical step before calculating new offsets
245
- * This ensures Present_Position reflects true physical position without existing offsets
246
- */
247
- async function resetHomingOffsets(
248
- config: SO100CalibrationConfig
249
- ): Promise<void> {
250
- for (let i = 0; i < config.motorIds.length; i++) {
251
- const motorId = config.motorIds[i];
252
- const motorName = config.motorNames[i];
253
-
254
- try {
255
- // Write 0 to Homing_Offset register using configurable address
256
- const homingOffsetValue = 0;
257
-
258
- // Create Write Homing_Offset packet using configurable address
259
- const packet = Buffer.from([
260
- 0xff,
261
- 0xff, // Header
262
- motorId, // Servo ID
263
- 0x05, // Length (Instruction + Address + Data + Checksum)
264
- 0x03, // Instruction: WRITE_DATA
265
- config.protocol.homingOffsetAddress, // Configurable address instead of hardcoded 0x1f
266
- homingOffsetValue & 0xff, // Data_L (low byte)
267
- (homingOffsetValue >> 8) & 0xff, // Data_H (high byte)
268
- 0x00, // Checksum (will calculate)
269
- ]);
270
-
271
- // Calculate checksum using configurable address
272
- const checksum =
273
- ~(
274
- motorId +
275
- 0x05 +
276
- 0x03 +
277
- config.protocol.homingOffsetAddress +
278
- (homingOffsetValue & 0xff) +
279
- ((homingOffsetValue >> 8) & 0xff)
280
- ) & 0xff;
281
- packet[8] = checksum;
282
-
283
- if (!config.port || !config.port.isOpen) {
284
- throw new Error("Serial port not open");
285
- }
286
-
287
- // Send reset packet
288
- await new Promise<void>((resolve, reject) => {
289
- config.port.write(packet, (error) => {
290
- if (error) {
291
- reject(
292
- new Error(
293
- `Failed to reset homing offset for ${motorName}: ${error.message}`
294
- )
295
- );
296
- } else {
297
- resolve();
298
- }
299
- });
300
- });
301
-
302
- // Wait for response (silent unless error)
303
- try {
304
- await readData(config.port, 200);
305
- } catch (error) {
306
- // Silent - response not required for successful operation
307
- }
308
- } catch (error) {
309
- throw new Error(
310
- `Failed to reset homing offset for ${motorName}: ${
311
- error instanceof Error ? error.message : error
312
- }`
313
- );
314
- }
315
-
316
- // Small delay between motor writes
317
- await new Promise((resolve) => setTimeout(resolve, 20));
318
- }
319
- }
320
-
321
- /**
322
- * Record homing offsets (current positions as center)
323
- * Mirrors Python bus.set_half_turn_homings()
324
- *
325
- * CRITICAL: Must reset existing homing offsets to 0 first (like Python does)
326
- * CRITICAL: Must WRITE the new homing offsets to motors immediately (like Python does)
327
- */
328
- async function setHomingOffsets(
329
- config: SO100CalibrationConfig
330
- ): Promise<{ [motor: string]: number }> {
331
- // CRITICAL: Reset existing homing offsets to 0 first (matching Python)
332
- await resetHomingOffsets(config);
333
-
334
- // Wait a moment for reset to take effect
335
- await new Promise((resolve) => setTimeout(resolve, 100));
336
-
337
- // Now read positions (which will be true physical positions)
338
- const currentPositions = await readMotorPositions(config);
339
- const homingOffsets: { [motor: string]: number } = {};
340
-
341
- for (let i = 0; i < config.motorNames.length; i++) {
342
- const motorName = config.motorNames[i];
343
- const position = currentPositions[i];
344
-
345
- // Generic formula: pos - int((max_res - 1) / 2) using configurable resolution
346
- const halfTurn = Math.floor((config.protocol.resolution - 1) / 2);
347
- homingOffsets[motorName] = position - halfTurn;
348
- }
349
-
350
- // CRITICAL: Write homing offsets to motors immediately (matching Python exactly)
351
- // Python does: for motor, offset in homing_offsets.items(): self.write("Homing_Offset", motor, offset)
352
- await writeHomingOffsetsToMotors(config, homingOffsets);
353
-
354
- return homingOffsets;
355
- }
356
-
357
- /**
358
- * Write homing offsets to motor registers immediately
359
- * Mirrors Python's immediate writing in set_half_turn_homings()
360
- */
361
- async function writeHomingOffsetsToMotors(
362
- config: SO100CalibrationConfig,
363
- homingOffsets: { [motor: string]: number }
364
- ): Promise<void> {
365
- for (let i = 0; i < config.motorIds.length; i++) {
366
- const motorId = config.motorIds[i];
367
- const motorName = config.motorNames[i];
368
- const homingOffset = homingOffsets[motorName];
369
-
370
- try {
371
- // Encode using sign-magnitude format (like Python)
372
- const encodedOffset = encodeSignMagnitude(
373
- homingOffset,
374
- config.protocol.signMagnitudeBit
375
- );
376
-
377
- // Create Write Homing_Offset packet
378
- const packet = Buffer.from([
379
- 0xff,
380
- 0xff, // Header
381
- motorId, // Servo ID
382
- 0x05, // Length
383
- 0x03, // Instruction: WRITE_DATA
384
- config.protocol.homingOffsetAddress, // Homing_Offset address
385
- encodedOffset & 0xff, // Data_L (low byte)
386
- (encodedOffset >> 8) & 0xff, // Data_H (high byte)
387
- 0x00, // Checksum (will calculate)
388
- ]);
389
-
390
- // Calculate checksum
391
- const checksum =
392
- ~(
393
- motorId +
394
- 0x05 +
395
- 0x03 +
396
- config.protocol.homingOffsetAddress +
397
- (encodedOffset & 0xff) +
398
- ((encodedOffset >> 8) & 0xff)
399
- ) & 0xff;
400
- packet[8] = checksum;
401
-
402
- if (!config.port || !config.port.isOpen) {
403
- throw new Error("Serial port not open");
404
- }
405
-
406
- // Send packet
407
- await new Promise<void>((resolve, reject) => {
408
- config.port.write(packet, (error) => {
409
- if (error) {
410
- reject(
411
- new Error(
412
- `Failed to write homing offset for ${motorName}: ${error.message}`
413
- )
414
- );
415
- } else {
416
- resolve();
417
- }
418
- });
419
- });
420
-
421
- // Wait for response (silent unless error)
422
- try {
423
- await readData(config.port, 200);
424
- } catch (error) {
425
- // Silent - response not required for successful operation
426
- }
427
- } catch (error) {
428
- throw new Error(
429
- `Failed to write homing offset for ${motorName}: ${
430
- error instanceof Error ? error.message : error
431
- }`
432
- );
433
- }
434
-
435
- // Small delay between motor writes
436
- await new Promise((resolve) => setTimeout(resolve, 20));
437
- }
438
- }
439
-
440
- /**
441
- * Record ranges of motion with live updating table
442
- * Mirrors Python bus.record_ranges_of_motion()
443
- */
444
- async function recordRangesOfMotion(config: SO100CalibrationConfig): Promise<{
445
- rangeMins: { [motor: string]: number };
446
- rangeMaxes: { [motor: string]: number };
447
- }> {
448
- console.log(
449
- "Move all joints sequentially through their entire ranges of motion."
450
- );
451
- console.log(
452
- "Positions will be recorded continuously. Press ENTER to stop...\n"
453
- );
454
-
455
- const rangeMins: { [motor: string]: number } = {};
456
- const rangeMaxes: { [motor: string]: number } = {};
457
-
458
- // Read actual current positions (matching Python exactly)
459
- // Python does: start_positions = self.sync_read("Present_Position", motors, normalize=False)
460
- // mins = start_positions.copy(); maxes = start_positions.copy()
461
- const startPositions = await readMotorPositions(config);
462
-
463
- for (let i = 0; i < config.motorNames.length; i++) {
464
- const motorName = config.motorNames[i];
465
- const startPosition = startPositions[i];
466
- rangeMins[motorName] = startPosition; // Use actual position, not hardcoded 2047
467
- rangeMaxes[motorName] = startPosition; // Use actual position, not hardcoded 2047
468
- }
469
-
470
- let recording = true;
471
- let readCount = 0;
472
-
473
- // Set up readline to detect Enter key
474
- const rl = readline.createInterface({
475
- input: process.stdin,
476
- output: process.stdout,
477
- });
478
-
479
- rl.on("line", () => {
480
- recording = false;
481
- rl.close();
482
- });
483
-
484
- // Continuous recording loop with live updates - THE LIVE UPDATING TABLE!
485
- while (recording) {
486
- try {
487
- const positions = await readMotorPositions(config); // Always quiet during live recording
488
- readCount++;
489
-
490
- // Update min/max ranges
491
- for (let i = 0; i < config.motorNames.length; i++) {
492
- const motorName = config.motorNames[i];
493
- const position = positions[i];
494
-
495
- if (position < rangeMins[motorName]) {
496
- rangeMins[motorName] = position;
497
- }
498
- if (position > rangeMaxes[motorName]) {
499
- rangeMaxes[motorName] = position;
500
- }
501
- }
502
-
503
- // Show real-time feedback every 3 reads for faster updates - LIVE TABLE UPDATE
504
- if (readCount % 3 === 0) {
505
- // Build the live table content
506
- let liveTable = `Readings: ${readCount}\n\n`;
507
- liveTable += "Motor Name Current Min Max Range\n";
508
- liveTable += "─".repeat(55) + "\n";
509
-
510
- for (let i = 0; i < config.motorNames.length; i++) {
511
- const motorName = config.motorNames[i];
512
- const current = positions[i];
513
- const min = rangeMins[motorName];
514
- const max = rangeMaxes[motorName];
515
- const range = max - min;
516
-
517
- liveTable += `${motorName.padEnd(15)} ${current
518
- .toString()
519
- .padStart(6)} ${min.toString().padStart(6)} ${max
520
- .toString()
521
- .padStart(6)} ${range.toString().padStart(8)}\n`;
522
- }
523
- liveTable += "\nMove joints through their full range...";
524
-
525
- // Update the display in place (no new console lines!)
526
- logUpdate(liveTable);
527
- }
528
-
529
- // Minimal delay for 30Hz reading rate (~33ms cycle time)
530
- await new Promise((resolve) => setTimeout(resolve, 10));
531
- } catch (error) {
532
- console.warn(
533
- `Read error: ${error instanceof Error ? error.message : error}`
534
- );
535
- await new Promise((resolve) => setTimeout(resolve, 100));
536
- }
537
- }
538
-
539
- // Stop live updating and return to normal console
540
- logUpdate.done();
541
-
542
- return { rangeMins, rangeMaxes };
543
- }
544
-
545
- /**
546
- * Prompt user for input (real implementation with readline)
547
- */
548
- async function promptUser(message: string): Promise<string> {
549
- const rl = readline.createInterface({
550
- input: process.stdin,
551
- output: process.stdout,
552
- });
553
-
554
- return new Promise((resolve) => {
555
- rl.question(message, (answer) => {
556
- rl.close();
557
- resolve(answer);
558
- });
559
- });
560
- }
561
-
562
- /**
563
- * Read data from serial port with timeout
564
- */
565
- async function readData(
566
- port: SerialPort,
567
- timeout: number = 5000
568
- ): Promise<Buffer> {
569
- if (!port || !port.isOpen) {
570
- throw new Error("Serial port not open");
571
- }
572
-
573
- return new Promise<Buffer>((resolve, reject) => {
574
- const timer = setTimeout(() => {
575
- reject(new Error("Read timeout"));
576
- }, timeout);
577
-
578
- port.once("data", (data: Buffer) => {
579
- clearTimeout(timer);
580
- resolve(data);
581
- });
582
- });
583
- }
584
-
585
- /**
586
- * Write hardware position limits to motors
587
- * Mirrors Python lerobot write_calibration() behavior where it writes:
588
- * - Min_Position_Limit register with calibration.range_min
589
- * - Max_Position_Limit register with calibration.range_max
590
- * This physically constrains the motors to the calibrated ranges
591
- */
592
- async function writeHardwarePositionLimits(
593
- config: SO100CalibrationConfig,
594
- rangeMins: { [motor: string]: number },
595
- rangeMaxes: { [motor: string]: number }
596
- ): Promise<void> {
597
- for (let i = 0; i < config.motorIds.length; i++) {
598
- const motorId = config.motorIds[i];
599
- const motorName = config.motorNames[i];
600
- const minLimit = rangeMins[motorName];
601
- const maxLimit = rangeMaxes[motorName];
602
-
603
- try {
604
- // Write Min_Position_Limit register
605
- await writeMotorRegister(
606
- config,
607
- motorId,
608
- config.protocol.minPositionLimitAddress,
609
- minLimit,
610
- `Min_Position_Limit for ${motorName}`
611
- );
612
-
613
- // Small delay between writes
614
- await new Promise((resolve) => setTimeout(resolve, 20));
615
-
616
- // Write Max_Position_Limit register
617
- await writeMotorRegister(
618
- config,
619
- motorId,
620
- config.protocol.maxPositionLimitAddress,
621
- maxLimit,
622
- `Max_Position_Limit for ${motorName}`
623
- );
624
-
625
- // Small delay between motors
626
- await new Promise((resolve) => setTimeout(resolve, 20));
627
- } catch (error) {
628
- throw new Error(
629
- `Failed to write position limits for ${motorName}: ${
630
- error instanceof Error ? error.message : error
631
- }`
632
- );
633
- }
634
- }
635
- }
636
-
637
- /**
638
- * Generic function to write a 2-byte value to a motor register
639
- * Used for both Min_Position_Limit and Max_Position_Limit
640
- */
641
- async function writeMotorRegister(
642
- config: SO100CalibrationConfig,
643
- motorId: number,
644
- registerAddress: number,
645
- value: number,
646
- description: string
647
- ): Promise<void> {
648
- // Create Write Register packet
649
- const packet = Buffer.from([
650
- 0xff,
651
- 0xff, // Header
652
- motorId, // Servo ID
653
- 0x05, // Length (Instruction + Address + Data + Checksum)
654
- 0x03, // Instruction: WRITE_DATA
655
- registerAddress, // Register address
656
- value & 0xff, // Data_L (low byte)
657
- (value >> 8) & 0xff, // Data_H (high byte)
658
- 0x00, // Checksum (will calculate)
659
- ]);
660
-
661
- // Calculate checksum
662
- const checksum =
663
- ~(
664
- motorId +
665
- 0x05 +
666
- 0x03 +
667
- registerAddress +
668
- (value & 0xff) +
669
- ((value >> 8) & 0xff)
670
- ) & 0xff;
671
- packet[8] = checksum;
672
-
673
- if (!config.port || !config.port.isOpen) {
674
- throw new Error("Serial port not open");
675
- }
676
-
677
- // Send packet
678
- await new Promise<void>((resolve, reject) => {
679
- config.port.write(packet, (error) => {
680
- if (error) {
681
- reject(new Error(`Failed to write ${description}: ${error.message}`));
682
- } else {
683
- resolve();
684
- }
685
- });
686
- });
687
-
688
- // Wait for response (silent unless error)
689
- try {
690
- await readData(config.port, 200);
691
- } catch (error) {
692
- // Silent - response not required for successful operation
693
- }
694
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/common/so100_config.ts DELETED
@@ -1,137 +0,0 @@
1
- /**
2
- * SO-100 device configurations
3
- * Defines the differences between leader and follower devices
4
- * Mirrors Python lerobot device configuration approach
5
- */
6
-
7
- import type { SO100CalibrationConfig } from "../types/calibration.js";
8
- import { SerialPort } from "serialport";
9
-
10
- /**
11
- * Common motor names for all SO-100 devices
12
- */
13
- const SO100_MOTOR_NAMES = [
14
- "shoulder_pan",
15
- "shoulder_lift",
16
- "elbow_flex",
17
- "wrist_flex",
18
- "wrist_roll",
19
- "gripper",
20
- ];
21
-
22
- /**
23
- * Common motor IDs for all SO-100 devices (STS3215 servos)
24
- */
25
- const SO100_MOTOR_IDS = [1, 2, 3, 4, 5, 6];
26
-
27
- /**
28
- * Protocol configuration for STS3215 motors used in SO-100 devices
29
- */
30
- interface STS3215Protocol {
31
- resolution: number;
32
- homingOffsetAddress: number;
33
- homingOffsetLength: number;
34
- presentPositionAddress: number;
35
- presentPositionLength: number;
36
- minPositionLimitAddress: number;
37
- minPositionLimitLength: number;
38
- maxPositionLimitAddress: number;
39
- maxPositionLimitLength: number;
40
- signMagnitudeBit: number; // Bit 11 is sign bit for Homing_Offset encoding
41
- }
42
-
43
- /**
44
- * STS3215 Protocol Configuration
45
- * These addresses and settings are specific to the STS3215 servo motors
46
- */
47
- export const STS3215_PROTOCOL: STS3215Protocol = {
48
- resolution: 4096, // 12-bit resolution (0-4095)
49
- homingOffsetAddress: 31, // Address for Homing_Offset register
50
- homingOffsetLength: 2, // 2 bytes for Homing_Offset
51
- presentPositionAddress: 56, // Address for Present_Position register
52
- presentPositionLength: 2, // 2 bytes for Present_Position
53
- minPositionLimitAddress: 9, // Address for Min_Position_Limit register
54
- minPositionLimitLength: 2, // 2 bytes for Min_Position_Limit
55
- maxPositionLimitAddress: 11, // Address for Max_Position_Limit register
56
- maxPositionLimitLength: 2, // 2 bytes for Max_Position_Limit
57
- signMagnitudeBit: 11, // Bit 11 is sign bit for Homing_Offset encoding
58
- } as const;
59
-
60
- /**
61
- * SO-100 Follower Configuration
62
- * Robot arm that performs tasks autonomously
63
- * Drive modes match Python lerobot exactly: all motors use drive_mode=0
64
- */
65
- export function createSO100FollowerConfig(
66
- port: SerialPort
67
- ): SO100CalibrationConfig {
68
- return {
69
- deviceType: "so100_follower",
70
- port,
71
- motorNames: SO100_MOTOR_NAMES,
72
- motorIds: SO100_MOTOR_IDS,
73
- protocol: STS3215_PROTOCOL,
74
-
75
- // Python lerobot uses drive_mode=0 for all motors (current format)
76
- driveModes: [0, 0, 0, 0, 0, 0],
77
-
78
- // Calibration modes (not used in current implementation, but kept for compatibility)
79
- calibModes: ["DEGREE", "DEGREE", "DEGREE", "DEGREE", "DEGREE", "LINEAR"],
80
-
81
- // Follower limits - these are not used in calibration file format
82
- limits: {
83
- position_min: [-180, -90, -90, -90, -90, -90],
84
- position_max: [180, 90, 90, 90, 90, 90],
85
- velocity_max: [100, 100, 100, 100, 100, 100],
86
- torque_max: [50, 50, 50, 50, 25, 25],
87
- },
88
- };
89
- }
90
-
91
- /**
92
- * SO-100 Leader Configuration
93
- * Teleoperator arm that humans use to control the follower
94
- * Drive modes match Python lerobot exactly: all motors use drive_mode=0
95
- */
96
- export function createSO100LeaderConfig(
97
- port: SerialPort
98
- ): SO100CalibrationConfig {
99
- return {
100
- deviceType: "so100_leader",
101
- port,
102
- motorNames: SO100_MOTOR_NAMES,
103
- motorIds: SO100_MOTOR_IDS,
104
- protocol: STS3215_PROTOCOL,
105
-
106
- // Python lerobot uses drive_mode=0 for all motors (current format)
107
- driveModes: [0, 0, 0, 0, 0, 0],
108
-
109
- // Same calibration modes as follower
110
- calibModes: ["DEGREE", "DEGREE", "DEGREE", "DEGREE", "DEGREE", "LINEAR"],
111
-
112
- // Leader limits - these are not used in calibration file format
113
- limits: {
114
- position_min: [-120, -60, -60, -60, -180, -45],
115
- position_max: [120, 60, 60, 60, 180, 45],
116
- velocity_max: [80, 80, 80, 80, 120, 60],
117
- torque_max: [30, 30, 30, 30, 20, 15],
118
- },
119
- };
120
- }
121
-
122
- /**
123
- * Get configuration for any SO-100 device type
124
- */
125
- export function getSO100Config(
126
- deviceType: "so100_follower" | "so100_leader",
127
- port: SerialPort
128
- ): SO100CalibrationConfig {
129
- switch (deviceType) {
130
- case "so100_follower":
131
- return createSO100FollowerConfig(port);
132
- case "so100_leader":
133
- return createSO100LeaderConfig(port);
134
- default:
135
- throw new Error(`Unknown SO-100 device type: ${deviceType}`);
136
- }
137
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/find_port.ts DELETED
@@ -1,125 +0,0 @@
1
- /**
2
- * Helper to find the USB port associated with your MotorsBus.
3
- *
4
- * Example:
5
- * ```
6
- * npx lerobot find-port
7
- * ```
8
- */
9
-
10
- import { SerialPort } from "serialport";
11
- import { createInterface } from "readline";
12
- import { platform } from "os";
13
- import { readdir } from "fs/promises";
14
- import { join } from "path";
15
-
16
- /**
17
- * Find all available serial ports on the system
18
- * Mirrors Python's find_available_ports() function
19
- */
20
- async function findAvailablePorts(): Promise<string[]> {
21
- if (platform() === "win32") {
22
- // List COM ports using serialport library (equivalent to pyserial)
23
- const ports = await SerialPort.list();
24
- return ports.map((port) => port.path);
25
- } else {
26
- // List /dev/tty* ports for Unix-based systems (Linux/macOS)
27
- try {
28
- const devFiles = await readdir("/dev");
29
- const ttyPorts = devFiles
30
- .filter((file) => file.startsWith("tty"))
31
- .map((file) => join("/dev", file));
32
- return ttyPorts;
33
- } catch (error) {
34
- // Fallback to serialport library if /dev reading fails
35
- const ports = await SerialPort.list();
36
- return ports.map((port) => port.path);
37
- }
38
- }
39
- }
40
-
41
- /**
42
- * Create readline interface for user input
43
- * Equivalent to Python's input() function
44
- */
45
- function createReadlineInterface() {
46
- return createInterface({
47
- input: process.stdin,
48
- output: process.stdout,
49
- });
50
- }
51
-
52
- /**
53
- * Prompt user for input and wait for response
54
- * Equivalent to Python's input() function
55
- */
56
- function waitForInput(prompt: string = ""): Promise<string> {
57
- const rl = createReadlineInterface();
58
- return new Promise((resolve) => {
59
- if (prompt) {
60
- process.stdout.write(prompt);
61
- }
62
- rl.on("line", (answer) => {
63
- rl.close();
64
- resolve(answer);
65
- });
66
- });
67
- }
68
-
69
- /**
70
- * Sleep for specified milliseconds
71
- * Equivalent to Python's time.sleep()
72
- */
73
- function sleep(ms: number): Promise<void> {
74
- return new Promise((resolve) => setTimeout(resolve, ms));
75
- }
76
-
77
- /**
78
- * Main find port function - direct port of Python find_port()
79
- * Maintains identical UX and messaging
80
- */
81
- export async function findPort(): Promise<void> {
82
- console.log("Finding all available ports for the MotorsBus.");
83
-
84
- const portsBefore = await findAvailablePorts();
85
- console.log("Ports before disconnecting:", portsBefore);
86
-
87
- console.log(
88
- "Remove the USB cable from your MotorsBus and press Enter when done."
89
- );
90
- await waitForInput();
91
-
92
- // Allow some time for port to be released (equivalent to Python's time.sleep(0.5))
93
- await sleep(500);
94
-
95
- const portsAfter = await findAvailablePorts();
96
- const portsDiff = portsBefore.filter((port) => !portsAfter.includes(port));
97
-
98
- if (portsDiff.length === 1) {
99
- const port = portsDiff[0];
100
- console.log(`The port of this MotorsBus is '${port}'`);
101
- console.log("Reconnect the USB cable.");
102
- } else if (portsDiff.length === 0) {
103
- throw new Error(
104
- `Could not detect the port. No difference was found (${JSON.stringify(
105
- portsDiff
106
- )}).`
107
- );
108
- } else {
109
- throw new Error(
110
- `Could not detect the port. More than one port was found (${JSON.stringify(
111
- portsDiff
112
- )}).`
113
- );
114
- }
115
- }
116
-
117
- /**
118
- * CLI entry point when called directly
119
- */
120
- if (import.meta.url === `file://${process.argv[1]}`) {
121
- findPort().catch((error) => {
122
- console.error(error.message);
123
- process.exit(1);
124
- });
125
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/robots/robot.ts DELETED
@@ -1,199 +0,0 @@
1
- /**
2
- * Base Robot class for Node.js platform
3
- * Uses serialport package for serial communication
4
- * Mirrors Python lerobot/common/robots/robot.py
5
- */
6
-
7
- import { SerialPort } from "serialport";
8
- import { mkdir, writeFile } from "fs/promises";
9
- import { existsSync, readFileSync, mkdirSync } from "fs";
10
- import { join } from "path";
11
- import type { RobotConfig } from "../types/robot-config.js";
12
- import { getCalibrationDir, ROBOTS } from "../utils/constants.js";
13
-
14
- export abstract class Robot {
15
- protected port: SerialPort | null = null;
16
- protected config: RobotConfig;
17
- protected calibrationDir: string;
18
- protected calibrationPath: string;
19
- protected name: string;
20
- protected calibration: any = {}; // Loaded calibration data
21
- protected isCalibrated: boolean = false;
22
-
23
- constructor(config: RobotConfig) {
24
- this.config = config;
25
- this.name = config.type;
26
-
27
- // Determine calibration directory
28
- // Mirrors Python: config.calibration_dir if config.calibration_dir else HF_LEROBOT_CALIBRATION / ROBOTS / self.name
29
- this.calibrationDir =
30
- config.calibration_dir || join(getCalibrationDir(), ROBOTS, this.name);
31
-
32
- // Use robot ID or type as filename
33
- const robotId = config.id || this.name;
34
- this.calibrationPath = join(this.calibrationDir, `${robotId}.json`);
35
-
36
- // Auto-load calibration if it exists (like Python version)
37
- this.loadCalibration();
38
- }
39
-
40
- /**
41
- * Connect to the robot
42
- * Mirrors Python robot.connect()
43
- */
44
- async connect(_calibrate: boolean = false): Promise<void> {
45
- try {
46
- this.port = new SerialPort({
47
- path: this.config.port,
48
- baudRate: 1000000, // Default baud rate for Feetech motors (SO-100) - matches Python lerobot
49
- dataBits: 8, // 8 data bits - matches Python serial.EIGHTBITS
50
- stopBits: 1, // 1 stop bit - matches Python default
51
- parity: "none", // No parity - matches Python default
52
- autoOpen: false,
53
- });
54
-
55
- // Open the port
56
- await new Promise<void>((resolve, reject) => {
57
- this.port!.open((error) => {
58
- if (error) {
59
- reject(
60
- new Error(
61
- `Failed to open port ${this.config.port}: ${error.message}`
62
- )
63
- );
64
- } else {
65
- resolve();
66
- }
67
- });
68
- });
69
- } catch (error) {
70
- throw new Error(`Could not connect to robot on port ${this.config.port}`);
71
- }
72
- }
73
-
74
- /**
75
- * Calibrate the robot
76
- * Must be implemented by subclasses
77
- */
78
- abstract calibrate(): Promise<void>;
79
-
80
- /**
81
- * Disconnect from the robot
82
- * Mirrors Python robot.disconnect()
83
- */
84
- async disconnect(): Promise<void> {
85
- if (this.port && this.port.isOpen) {
86
- // Handle torque disable if configured
87
- if (this.config.disable_torque_on_disconnect) {
88
- await this.disableTorque();
89
- }
90
-
91
- await new Promise<void>((resolve) => {
92
- this.port!.close(() => {
93
- resolve();
94
- });
95
- });
96
-
97
- this.port = null;
98
- }
99
- }
100
-
101
- /**
102
- * Save calibration data to JSON file
103
- * Mirrors Python's configuration saving
104
- */
105
- protected async saveCalibration(calibrationData: any): Promise<void> {
106
- // Ensure calibration directory exists
107
- try {
108
- mkdirSync(this.calibrationDir, { recursive: true });
109
- } catch (error) {
110
- // Directory might already exist, that's fine
111
- }
112
-
113
- // Save calibration data as JSON
114
- await writeFile(
115
- this.calibrationPath,
116
- JSON.stringify(calibrationData, null, 2)
117
- );
118
-
119
- console.log(`Configuration saved to: ${this.calibrationPath}`);
120
- }
121
-
122
- /**
123
- * Load calibration data from JSON file
124
- * Mirrors Python's _load_calibration()
125
- */
126
- protected loadCalibration(): void {
127
- try {
128
- if (existsSync(this.calibrationPath)) {
129
- const calibrationData = readFileSync(this.calibrationPath, "utf8");
130
- this.calibration = JSON.parse(calibrationData);
131
- this.isCalibrated = true;
132
- console.log(`βœ… Loaded calibration from: ${this.calibrationPath}`);
133
- } else {
134
- console.log(
135
- `⚠️ No calibration file found at: ${this.calibrationPath}`
136
- );
137
- this.isCalibrated = false;
138
- }
139
- } catch (error) {
140
- console.warn(
141
- `Failed to load calibration: ${
142
- error instanceof Error ? error.message : error
143
- }`
144
- );
145
- this.calibration = {};
146
- this.isCalibrated = false;
147
- }
148
- }
149
-
150
- /**
151
- * Send command to robot via serial port
152
- */
153
- protected async sendCommand(command: string): Promise<void> {
154
- if (!this.port || !this.port.isOpen) {
155
- throw new Error("Robot not connected");
156
- }
157
-
158
- return new Promise<void>((resolve, reject) => {
159
- this.port!.write(command, (error) => {
160
- if (error) {
161
- reject(new Error(`Failed to send command: ${error.message}`));
162
- } else {
163
- resolve();
164
- }
165
- });
166
- });
167
- }
168
-
169
- /**
170
- * Read data from robot
171
- */
172
- protected async readData(timeout: number = 5000): Promise<Buffer> {
173
- if (!this.port || !this.port.isOpen) {
174
- throw new Error("Robot not connected");
175
- }
176
-
177
- return new Promise<Buffer>((resolve, reject) => {
178
- const timer = setTimeout(() => {
179
- reject(new Error("Read timeout"));
180
- }, timeout);
181
-
182
- this.port!.once("data", (data: Buffer) => {
183
- clearTimeout(timer);
184
- resolve(data);
185
- });
186
- });
187
- }
188
-
189
- /**
190
- * Disable torque on disconnect (SO-100 specific)
191
- */
192
- protected async disableTorque(): Promise<void> {
193
- try {
194
- await this.sendCommand("TORQUE_DISABLE\r\n");
195
- } catch (error) {
196
- console.warn("Warning: Could not disable torque on disconnect");
197
- }
198
- }
199
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/robots/so100_follower.ts DELETED
@@ -1,614 +0,0 @@
1
- /**
2
- * SO-100 Follower Robot implementation for Node.js
3
- * Mirrors Python lerobot/common/robots/so100_follower/so100_follower.py
4
- */
5
-
6
- import { Robot } from "./robot.js";
7
- import type { RobotConfig } from "../types/robot-config.js";
8
- import * as readline from "readline";
9
-
10
- export class SO100Follower extends Robot {
11
- constructor(config: RobotConfig) {
12
- super(config);
13
-
14
- // Validate that this is an SO-100 follower config
15
- if (config.type !== "so100_follower") {
16
- throw new Error(
17
- `Invalid robot type: ${config.type}. Expected: so100_follower`
18
- );
19
- }
20
- }
21
-
22
- /**
23
- * Calibrate the SO-100 follower robot
24
- * NOTE: Calibration logic has been moved to shared/common/calibration.ts
25
- * This method is kept for backward compatibility but delegates to the main calibrate.ts
26
- */
27
- async calibrate(): Promise<void> {
28
- throw new Error(
29
- "Direct device calibration is deprecated. Use the main calibrate.ts orchestrator instead."
30
- );
31
- }
32
-
33
- /**
34
- * Initialize robot communication
35
- * For now, just test basic serial connectivity
36
- */
37
- private async initializeRobot(): Promise<void> {
38
- console.log("Initializing robot communication...");
39
-
40
- try {
41
- // For SO-100, we need to implement Feetech servo protocol
42
- // For now, just test that we can send/receive data
43
- console.log("Testing serial port connectivity...");
44
-
45
- // Try to ping servo ID 1 (shoulder_pan motor)
46
- // This is a very basic test - real implementation needs proper Feetech protocol
47
- const pingPacket = Buffer.from([0xff, 0xff, 0x01, 0x02, 0x01, 0xfb]); // Basic ping packet
48
-
49
- if (!this.port || !this.port.isOpen) {
50
- throw new Error("Serial port not open");
51
- }
52
-
53
- // Send ping packet
54
- await new Promise<void>((resolve, reject) => {
55
- this.port!.write(pingPacket, (error) => {
56
- if (error) {
57
- reject(new Error(`Failed to send ping: ${error.message}`));
58
- } else {
59
- resolve();
60
- }
61
- });
62
- });
63
-
64
- console.log("Ping packet sent successfully");
65
-
66
- // Try to read response with shorter timeout
67
- try {
68
- const response = await this.readData(1000); // 1 second timeout
69
- console.log(`Response received: ${response.length} bytes`);
70
- } catch (error) {
71
- console.log("No response received (expected for basic test)");
72
- }
73
- } catch (error) {
74
- throw new Error(
75
- `Serial communication test failed: ${
76
- error instanceof Error ? error.message : error
77
- }`
78
- );
79
- }
80
-
81
- console.log("Robot communication test completed.");
82
- }
83
-
84
- /**
85
- * Read current motor positions as a record with motor names
86
- * For teleoperation use
87
- */
88
- async getMotorPositions(): Promise<Record<string, number>> {
89
- const positions = await this.readMotorPositions();
90
- const motorNames = [
91
- "shoulder_pan",
92
- "shoulder_lift",
93
- "elbow_flex",
94
- "wrist_flex",
95
- "wrist_roll",
96
- "gripper",
97
- ];
98
-
99
- const result: Record<string, number> = {};
100
- for (let i = 0; i < motorNames.length; i++) {
101
- result[motorNames[i]] = positions[i];
102
- }
103
- return result;
104
- }
105
-
106
- /**
107
- * Get calibration data for teleoperation
108
- * Returns position limits and offsets from calibration file
109
- */
110
- getCalibrationLimits(): Record<string, { min: number; max: number }> {
111
- if (!this.isCalibrated || !this.calibration) {
112
- console.warn("No calibration data available, using default limits");
113
- // Default STS3215 limits as fallback
114
- return {
115
- shoulder_pan: { min: 985, max: 3085 },
116
- shoulder_lift: { min: 1200, max: 2800 },
117
- elbow_flex: { min: 1000, max: 3000 },
118
- wrist_flex: { min: 1100, max: 2900 },
119
- wrist_roll: { min: 0, max: 4095 }, // Full rotation motor
120
- gripper: { min: 1800, max: 2300 },
121
- };
122
- }
123
-
124
- // Extract limits from calibration data (matches Python format)
125
- const limits: Record<string, { min: number; max: number }> = {};
126
- for (const [motorName, calibData] of Object.entries(this.calibration)) {
127
- if (
128
- calibData &&
129
- typeof calibData === "object" &&
130
- "range_min" in calibData &&
131
- "range_max" in calibData
132
- ) {
133
- limits[motorName] = {
134
- min: Number(calibData.range_min),
135
- max: Number(calibData.range_max),
136
- };
137
- }
138
- }
139
-
140
- return limits;
141
- }
142
-
143
- /**
144
- * Set motor positions from a record with motor names
145
- * For teleoperation use
146
- */
147
- async setMotorPositions(positions: Record<string, number>): Promise<void> {
148
- const motorNames = [
149
- "shoulder_pan",
150
- "shoulder_lift",
151
- "elbow_flex",
152
- "wrist_flex",
153
- "wrist_roll",
154
- "gripper",
155
- ];
156
- const motorIds = [1, 2, 3, 4, 5, 6]; // SO-100 has servo IDs 1-6
157
-
158
- for (let i = 0; i < motorNames.length; i++) {
159
- const motorName = motorNames[i];
160
- const motorId = motorIds[i];
161
- const position = positions[motorName];
162
-
163
- if (position !== undefined) {
164
- await this.writeMotorPosition(motorId, position);
165
- }
166
- }
167
- }
168
-
169
- /**
170
- * Write position to a single motor
171
- * Implements STS3215 WRITE_DATA command for position control
172
- */
173
- private async writeMotorPosition(
174
- motorId: number,
175
- position: number
176
- ): Promise<void> {
177
- if (!this.port || !this.port.isOpen) {
178
- throw new Error("Serial port not open");
179
- }
180
-
181
- // Clamp position to valid range
182
- const clampedPosition = Math.max(0, Math.min(4095, Math.round(position)));
183
-
184
- // Create STS3215 Write Position packet
185
- // Format: [0xFF, 0xFF, ID, Length, Instruction, Address, Data_L, Data_H, Checksum]
186
- // Goal_Position address for STS3215 is 42 (0x2A), length 2 bytes
187
- const packet = Buffer.from([
188
- 0xff,
189
- 0xff, // Header
190
- motorId, // Servo ID
191
- 0x05, // Length (Instruction + Address + Data_L + Data_H + Checksum)
192
- 0x03, // Instruction: WRITE_DATA
193
- 0x2a, // Address: Goal_Position (42)
194
- clampedPosition & 0xff, // Data_L (low byte)
195
- (clampedPosition >> 8) & 0xff, // Data_H (high byte)
196
- 0x00, // Checksum (will calculate)
197
- ]);
198
-
199
- // Calculate checksum: ~(ID + Length + Instruction + Address + Data_L + Data_H) & 0xFF
200
- const checksum =
201
- ~(
202
- motorId +
203
- 0x05 +
204
- 0x03 +
205
- 0x2a +
206
- (clampedPosition & 0xff) +
207
- ((clampedPosition >> 8) & 0xff)
208
- ) & 0xff;
209
- packet[8] = checksum;
210
-
211
- // Send write position packet
212
- await new Promise<void>((resolve, reject) => {
213
- this.port!.write(packet, (error) => {
214
- if (error) {
215
- reject(new Error(`Failed to send write packet: ${error.message}`));
216
- } else {
217
- resolve();
218
- }
219
- });
220
- });
221
-
222
- // Small delay to allow servo to process command
223
- await new Promise((resolve) => setTimeout(resolve, 1));
224
- }
225
-
226
- /**
227
- * Read current motor positions
228
- * Implements basic STS3215 servo protocol to read actual positions
229
- */
230
- private async readMotorPositions(): Promise<number[]> {
231
- console.log("Reading motor positions...");
232
-
233
- const motorPositions: number[] = [];
234
- const motorIds = [1, 2, 3, 4, 5, 6]; // SO-100 has servo IDs 1-6
235
- const motorNames = [
236
- "shoulder_pan",
237
- "shoulder_lift",
238
- "elbow_flex",
239
- "wrist_flex",
240
- "wrist_roll",
241
- "gripper",
242
- ];
243
-
244
- // Try to read position from each servo using STS3215 protocol
245
- for (let i = 0; i < motorIds.length; i++) {
246
- const motorId = motorIds[i];
247
- const motorName = motorNames[i];
248
-
249
- try {
250
- console.log(` Reading ${motorName} (ID ${motorId})...`);
251
-
252
- // Create STS3215 Read Position packet
253
- // Format: [0xFF, 0xFF, ID, Length, Instruction, Address, DataLength, Checksum]
254
- // Present_Position address for STS3215 is 56 (0x38), length 2 bytes
255
- const packet = Buffer.from([
256
- 0xff,
257
- 0xff, // Header
258
- motorId, // Servo ID
259
- 0x04, // Length (Instruction + Address + DataLength + Checksum)
260
- 0x02, // Instruction: READ_DATA
261
- 0x38, // Address: Present_Position (56)
262
- 0x02, // Data Length: 2 bytes
263
- 0x00, // Checksum (will calculate)
264
- ]);
265
-
266
- // Calculate checksum: ~(ID + Length + Instruction + Address + DataLength) & 0xFF
267
- const checksum = ~(motorId + 0x04 + 0x02 + 0x38 + 0x02) & 0xff;
268
- packet[7] = checksum;
269
-
270
- if (!this.port || !this.port.isOpen) {
271
- throw new Error("Serial port not open");
272
- }
273
-
274
- // Send read position packet
275
- await new Promise<void>((resolve, reject) => {
276
- this.port!.write(packet, (error) => {
277
- if (error) {
278
- reject(new Error(`Failed to send read packet: ${error.message}`));
279
- } else {
280
- resolve();
281
- }
282
- });
283
- });
284
-
285
- // Try to read response (timeout after 500ms)
286
- try {
287
- const response = await this.readData(500);
288
-
289
- if (response.length >= 7) {
290
- // Parse response: [0xFF, 0xFF, ID, Length, Error, Data_L, Data_H, Checksum]
291
- const id = response[2];
292
- const error = response[4];
293
-
294
- if (id === motorId && error === 0) {
295
- // Extract 16-bit position from Data_L and Data_H
296
- const position = response[5] | (response[6] << 8);
297
- motorPositions.push(position);
298
-
299
- // Show calibrated range if available
300
- const calibratedLimits = this.getCalibrationLimits();
301
- const limits = calibratedLimits[motorName];
302
- const rangeText = limits
303
- ? `(${limits.min}-${limits.max} calibrated)`
304
- : `(0-4095 raw)`;
305
- console.log(` ${motorName}: ${position} ${rangeText}`);
306
- } else {
307
- console.warn(
308
- ` ${motorName}: Error response (error code: ${error})`
309
- );
310
- motorPositions.push(2047); // Use center position as fallback
311
- }
312
- } else {
313
- console.warn(` ${motorName}: Invalid response length`);
314
- motorPositions.push(2047); // Use center position as fallback
315
- }
316
- } catch (readError) {
317
- console.warn(
318
- ` ${motorName}: Read timeout - using fallback position`
319
- );
320
- motorPositions.push(2047); // Use center position as fallback
321
- }
322
- } catch (error) {
323
- console.warn(
324
- ` ${motorName}: Communication error - ${
325
- error instanceof Error ? error.message : error
326
- }`
327
- );
328
- motorPositions.push(2047); // Use center position as fallback
329
- }
330
-
331
- // Small delay between servo reads
332
- await new Promise((resolve) => setTimeout(resolve, 10));
333
- }
334
-
335
- console.log(`Motor positions: [${motorPositions.join(", ")}]`);
336
- return motorPositions;
337
- }
338
-
339
- /**
340
- * Set motor limits and safety parameters
341
- * TODO: Implement proper Feetech servo protocol
342
- */
343
- private async setMotorLimits(): Promise<any> {
344
- console.log("Setting motor limits...");
345
-
346
- // Set default limits for SO-100 (based on Python implementation)
347
- const limits = {
348
- position_min: [-180, -90, -90, -90, -90, -90],
349
- position_max: [180, 90, 90, 90, 90, 90],
350
- velocity_max: [100, 100, 100, 100, 100, 100],
351
- torque_max: [50, 50, 50, 50, 25, 25],
352
- };
353
-
354
- // For now, just return the limits without sending to robot
355
- // Real implementation needs Feetech servo protocol to set limits
356
- console.log("Motor limits configured (mock).");
357
- return limits;
358
- }
359
-
360
- /**
361
- * Interactive calibration process - matches Python lerobot calibration flow
362
- * Implements real calibration with user interaction
363
- */
364
- private async calibrateMotors(): Promise<any> {
365
- console.log("\n=== INTERACTIVE CALIBRATION ===");
366
- console.log("Starting SO-100 follower arm calibration...");
367
-
368
- // Step 1: Move to middle position and record homing offsets
369
- console.log("\nπŸ“ STEP 1: Set Homing Position");
370
- await this.promptUser(
371
- "Move the SO-100 to the MIDDLE of its range of motion and press ENTER..."
372
- );
373
-
374
- const homingOffsets = await this.setHomingOffsets();
375
-
376
- // Step 2: Record ranges of motion
377
- console.log("\nπŸ“ STEP 2: Record Joint Ranges");
378
- const { rangeMins, rangeMaxes } = await this.recordRangesOfMotion();
379
-
380
- // Step 3: Set special range for wrist_roll (full turn motor)
381
- console.log("\nπŸ”„ STEP 3: Configure Full-Turn Motor");
382
- console.log("Setting wrist_roll as full-turn motor (0-4095 range)");
383
- rangeMins["wrist_roll"] = 0;
384
- rangeMaxes["wrist_roll"] = 4095;
385
-
386
- // Step 4: Compile calibration results
387
- const motorNames = [
388
- "shoulder_pan",
389
- "shoulder_lift",
390
- "elbow_flex",
391
- "wrist_flex",
392
- "wrist_roll",
393
- "gripper",
394
- ];
395
- const results = [];
396
-
397
- for (let i = 0; i < motorNames.length; i++) {
398
- const motorId = i + 1; // Servo IDs are 1-6
399
- const motorName = motorNames[i];
400
-
401
- results.push({
402
- motor: motorId,
403
- name: motorName,
404
- status: "success",
405
- homing_offset: homingOffsets[motorName],
406
- range_min: rangeMins[motorName],
407
- range_max: rangeMaxes[motorName],
408
- range_size: rangeMaxes[motorName] - rangeMins[motorName],
409
- });
410
-
411
- console.log(
412
- `βœ… ${motorName} calibrated: range ${rangeMins[motorName]} to ${rangeMaxes[motorName]} (offset: ${homingOffsets[motorName]})`
413
- );
414
- }
415
-
416
- console.log("\nπŸŽ‰ Interactive calibration completed!");
417
- return results;
418
- }
419
-
420
- /**
421
- * Verify calibration was successful
422
- * TODO: Implement proper verification with Feetech servo protocol
423
- */
424
- private async verifyCalibration(): Promise<void> {
425
- console.log("Verifying calibration...");
426
-
427
- // For now, just mock successful verification
428
- // Real implementation should check:
429
- // 1. All motors respond to ping
430
- // 2. Position limits are set correctly
431
- // 3. Homing offsets are applied
432
- // 4. Motors can move to test positions
433
-
434
- console.log("Calibration verification passed (mock).");
435
- }
436
-
437
- /**
438
- * Prompt user for input (like Python's input() function)
439
- */
440
- private async promptUser(message: string): Promise<string> {
441
- const rl = readline.createInterface({
442
- input: process.stdin,
443
- output: process.stdout,
444
- });
445
-
446
- return new Promise((resolve) => {
447
- rl.question(message, (answer) => {
448
- rl.close();
449
- resolve(answer);
450
- });
451
- });
452
- }
453
-
454
- /**
455
- * Record homing offsets (current positions as center)
456
- * Mirrors Python bus.set_half_turn_homings()
457
- */
458
- private async setHomingOffsets(): Promise<{ [motor: string]: number }> {
459
- console.log("Recording current positions as homing offsets...");
460
-
461
- const currentPositions = await this.readMotorPositions();
462
- const motorNames = [
463
- "shoulder_pan",
464
- "shoulder_lift",
465
- "elbow_flex",
466
- "wrist_flex",
467
- "wrist_roll",
468
- "gripper",
469
- ];
470
- const homingOffsets: { [motor: string]: number } = {};
471
-
472
- for (let i = 0; i < motorNames.length; i++) {
473
- const motorName = motorNames[i];
474
- const position = currentPositions[i];
475
- // Calculate homing offset (half turn offset from current position)
476
- const maxRes = 4095; // STS3215 resolution
477
- homingOffsets[motorName] = position - Math.floor(maxRes / 2);
478
- console.log(
479
- ` ${motorName}: offset ${homingOffsets[motorName]} (current pos: ${position})`
480
- );
481
- }
482
-
483
- return homingOffsets;
484
- }
485
-
486
- /**
487
- * Record ranges of motion by continuously reading positions
488
- * Mirrors Python bus.record_ranges_of_motion()
489
- */
490
- private async recordRangesOfMotion(): Promise<{
491
- rangeMins: { [motor: string]: number };
492
- rangeMaxes: { [motor: string]: number };
493
- }> {
494
- console.log("\n=== RECORDING RANGES OF MOTION ===");
495
- console.log(
496
- "Move all joints sequentially through their entire ranges of motion."
497
- );
498
- console.log(
499
- "Positions will be recorded continuously. Press ENTER to stop...\n"
500
- );
501
-
502
- const motorNames = [
503
- "shoulder_pan",
504
- "shoulder_lift",
505
- "elbow_flex",
506
- "wrist_flex",
507
- "wrist_roll",
508
- "gripper",
509
- ];
510
- const rangeMins: { [motor: string]: number } = {};
511
- const rangeMaxes: { [motor: string]: number } = {};
512
-
513
- // Initialize with current positions
514
- const initialPositions = await this.readMotorPositions();
515
- for (let i = 0; i < motorNames.length; i++) {
516
- const motorName = motorNames[i];
517
- const position = initialPositions[i];
518
- rangeMins[motorName] = position;
519
- rangeMaxes[motorName] = position;
520
- }
521
-
522
- let recording = true;
523
- let readCount = 0;
524
-
525
- // Set up readline to detect Enter key
526
- const rl = readline.createInterface({
527
- input: process.stdin,
528
- output: process.stdout,
529
- });
530
-
531
- rl.on("line", () => {
532
- recording = false;
533
- rl.close();
534
- });
535
-
536
- console.log("Recording started... (move the robot joints now)");
537
-
538
- // Continuous recording loop
539
- while (recording) {
540
- try {
541
- const positions = await this.readMotorPositions();
542
- readCount++;
543
-
544
- // Update min/max ranges
545
- for (let i = 0; i < motorNames.length; i++) {
546
- const motorName = motorNames[i];
547
- const position = positions[i];
548
-
549
- if (position < rangeMins[motorName]) {
550
- rangeMins[motorName] = position;
551
- }
552
- if (position > rangeMaxes[motorName]) {
553
- rangeMaxes[motorName] = position;
554
- }
555
- }
556
-
557
- // Show real-time feedback every 10 reads
558
- if (readCount % 10 === 0) {
559
- console.clear(); // Clear screen for live update
560
- console.log("=== LIVE POSITION RECORDING ===");
561
- console.log(`Readings: ${readCount} | Press ENTER to stop\n`);
562
-
563
- console.log("Motor Name Current Min Max Range");
564
- console.log("─".repeat(55));
565
-
566
- for (let i = 0; i < motorNames.length; i++) {
567
- const motorName = motorNames[i];
568
- const current = positions[i];
569
- const min = rangeMins[motorName];
570
- const max = rangeMaxes[motorName];
571
- const range = max - min;
572
-
573
- console.log(
574
- `${motorName.padEnd(15)} ${current.toString().padStart(6)} ${min
575
- .toString()
576
- .padStart(6)} ${max.toString().padStart(6)} ${range
577
- .toString()
578
- .padStart(8)}`
579
- );
580
- }
581
- console.log("\nMove joints through their full range...");
582
- }
583
-
584
- // Small delay to avoid overwhelming the serial port
585
- await new Promise((resolve) => setTimeout(resolve, 50));
586
- } catch (error) {
587
- console.warn(
588
- `Read error: ${error instanceof Error ? error.message : error}`
589
- );
590
- await new Promise((resolve) => setTimeout(resolve, 100));
591
- }
592
- }
593
-
594
- console.log(`\nRecording stopped after ${readCount} readings.`);
595
- console.log("\nFinal ranges recorded:");
596
-
597
- for (const motorName of motorNames) {
598
- const min = rangeMins[motorName];
599
- const max = rangeMaxes[motorName];
600
- const range = max - min;
601
- console.log(` ${motorName}: ${min} to ${max} (range: ${range})`);
602
- }
603
-
604
- return { rangeMins, rangeMaxes };
605
- }
606
- }
607
-
608
- /**
609
- * Factory function to create SO-100 follower robot
610
- * Mirrors Python's make_robot_from_config pattern
611
- */
612
- export function createSO100Follower(config: RobotConfig): SO100Follower {
613
- return new SO100Follower(config);
614
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/teleoperate.ts DELETED
@@ -1,316 +0,0 @@
1
- /**
2
- * Robot teleoperation using keyboard control
3
- *
4
- * Example:
5
- * ```
6
- * npx lerobot teleoperate --robot.type=so100_follower --robot.port=COM4 --teleop.type=keyboard
7
- * ```
8
- */
9
-
10
- import { createSO100Follower } from "./robots/so100_follower.js";
11
- import { KeyboardController } from "./utils/keyboard-teleop.js";
12
- import type { TeleoperateConfig } from "./types/teleoperation.js";
13
-
14
- /**
15
- * Main teleoperate function
16
- * Mirrors Python lerobot teleoperate.py structure
17
- */
18
- export async function teleoperate(config: TeleoperateConfig): Promise<void> {
19
- // Validate configuration
20
- if (!config.robot) {
21
- throw new Error("Robot configuration is required");
22
- }
23
-
24
- if (!config.teleop || config.teleop.type !== "keyboard") {
25
- throw new Error("Only keyboard teleoperation is currently supported");
26
- }
27
-
28
- const stepSize = config.step_size || 25;
29
- const duration = config.duration_s;
30
-
31
- let robot;
32
- let keyboardController;
33
-
34
- try {
35
- // Create robot
36
- switch (config.robot.type) {
37
- case "so100_follower":
38
- robot = createSO100Follower(config.robot);
39
- break;
40
- default:
41
- throw new Error(`Unsupported robot type: ${config.robot.type}`);
42
- }
43
-
44
- console.log(
45
- `Connecting to robot: ${config.robot.type} on ${config.robot.port}`
46
- );
47
- if (config.robot.id) {
48
- console.log(`Robot ID: ${config.robot.id}`);
49
- }
50
-
51
- await robot.connect(false); // calibrate=false
52
- console.log("Robot connected successfully.");
53
-
54
- // Show calibration status
55
- const isCalibrated = (robot as any).isCalibrated;
56
- if (isCalibrated) {
57
- console.log(
58
- `βœ… Loaded calibration for: ${config.robot.id || config.robot.type}`
59
- );
60
- } else {
61
- console.log(
62
- `⚠️ No calibration found for: ${
63
- config.robot.id || config.robot.type
64
- } (using defaults)`
65
- );
66
- console.log(
67
- " Run 'npx lerobot calibrate' first for optimal performance!"
68
- );
69
- }
70
-
71
- // Create keyboard controller
72
- keyboardController = new KeyboardController(robot, stepSize);
73
-
74
- console.log("");
75
- console.log("Starting keyboard teleoperation...");
76
- console.log("Controls:");
77
- console.log(" ↑↓ Arrow Keys: Shoulder Lift");
78
- console.log(" ←→ Arrow Keys: Shoulder Pan");
79
- console.log(" W/S: Elbow Flex");
80
- console.log(" A/D: Wrist Flex");
81
- console.log(" Q/E: Wrist Roll");
82
- console.log(" Space: Gripper Toggle");
83
- console.log(" ESC: Emergency Stop");
84
- console.log(" Ctrl+C: Exit");
85
- console.log("");
86
- console.log("Press any control key to begin...");
87
- console.log("");
88
-
89
- // Start teleoperation control loop
90
- await teleoperationLoop(keyboardController, robot, duration || null);
91
- } catch (error) {
92
- // Ensure we disconnect even if there's an error
93
- if (keyboardController) {
94
- try {
95
- await keyboardController.stop();
96
- } catch (stopError) {
97
- console.warn("Warning: Failed to stop keyboard controller properly");
98
- }
99
- }
100
- if (robot) {
101
- try {
102
- await robot.disconnect();
103
- } catch (disconnectError) {
104
- console.warn("Warning: Failed to disconnect robot properly");
105
- }
106
- }
107
- throw error;
108
- }
109
- }
110
-
111
- /**
112
- * Main teleoperation control loop
113
- */
114
- async function teleoperationLoop(
115
- keyboardController: KeyboardController,
116
- robot: any,
117
- duration: number | null
118
- ): Promise<void> {
119
- console.log("Initializing teleoperation...");
120
-
121
- // Start keyboard controller
122
- await keyboardController.start();
123
-
124
- const startTime = performance.now();
125
-
126
- // Set up graceful shutdown
127
- let running = true;
128
- process.on("SIGINT", () => {
129
- console.log("\nShutting down gracefully...");
130
- running = false;
131
- });
132
-
133
- try {
134
- // Just wait for the keyboard controller to handle everything
135
- while (running) {
136
- // Check duration limit
137
- if (duration && performance.now() - startTime >= duration * 1000) {
138
- console.log(`\nDuration limit reached (${duration}s). Stopping...`);
139
- break;
140
- }
141
-
142
- // Small delay to prevent busy waiting
143
- await new Promise((resolve) => setTimeout(resolve, 100));
144
- }
145
- } finally {
146
- console.log("\nStopping teleoperation...");
147
- await keyboardController.stop();
148
- await robot.disconnect();
149
- console.log("Teleoperation stopped.");
150
- }
151
- }
152
-
153
- /**
154
- * Parse command line arguments in Python argparse style
155
- * Handles --robot.type=so100_follower --teleop.type=keyboard format
156
- */
157
- export function parseArgs(args: string[]): TeleoperateConfig {
158
- const config: Partial<TeleoperateConfig> = {};
159
-
160
- for (const arg of args) {
161
- if (arg.startsWith("--robot.")) {
162
- if (!config.robot) {
163
- config.robot = { type: "so100_follower", port: "" };
164
- }
165
-
166
- const [key, value] = arg.substring(8).split("=");
167
- switch (key) {
168
- case "type":
169
- if (value !== "so100_follower") {
170
- throw new Error(`Unsupported robot type: ${value}`);
171
- }
172
- config.robot.type = value as "so100_follower";
173
- break;
174
- case "port":
175
- config.robot.port = value;
176
- break;
177
- case "id":
178
- config.robot.id = value;
179
- break;
180
- default:
181
- throw new Error(`Unknown robot parameter: ${key}`);
182
- }
183
- } else if (arg.startsWith("--teleop.")) {
184
- if (!config.teleop) {
185
- config.teleop = { type: "keyboard" };
186
- }
187
-
188
- const [key, value] = arg.substring(9).split("=");
189
- switch (key) {
190
- case "type":
191
- if (value !== "keyboard") {
192
- throw new Error(`Unsupported teleoperator type: ${value}`);
193
- }
194
- config.teleop.type = value as "keyboard";
195
- break;
196
- default:
197
- throw new Error(`Unknown teleoperator parameter: ${key}`);
198
- }
199
- } else if (arg.startsWith("--fps=")) {
200
- config.fps = parseInt(arg.substring(6));
201
- if (isNaN(config.fps) || config.fps <= 0) {
202
- throw new Error("FPS must be a positive number");
203
- }
204
- } else if (arg.startsWith("--step_size=")) {
205
- config.step_size = parseInt(arg.substring(12));
206
- if (isNaN(config.step_size) || config.step_size <= 0) {
207
- throw new Error("Step size must be a positive number");
208
- }
209
- } else if (arg.startsWith("--duration_s=")) {
210
- config.duration_s = parseInt(arg.substring(13));
211
- if (isNaN(config.duration_s) || config.duration_s <= 0) {
212
- throw new Error("Duration must be a positive number");
213
- }
214
- } else if (arg === "--help" || arg === "-h") {
215
- showUsage();
216
- process.exit(0);
217
- } else if (!arg.startsWith("--")) {
218
- // Skip non-option arguments
219
- continue;
220
- } else {
221
- throw new Error(`Unknown argument: ${arg}`);
222
- }
223
- }
224
-
225
- // Validate required fields
226
- if (!config.robot?.port) {
227
- throw new Error("Robot port is required (--robot.port=PORT)");
228
- }
229
- if (!config.teleop?.type) {
230
- throw new Error("Teleoperator type is required (--teleop.type=keyboard)");
231
- }
232
-
233
- return config as TeleoperateConfig;
234
- }
235
-
236
- /**
237
- * Show usage information matching Python argparse output
238
- */
239
- function showUsage(): void {
240
- console.log("Usage: lerobot teleoperate [options]");
241
- console.log("");
242
- console.log("Control a robot using keyboard input");
243
- console.log("");
244
- console.log("Options:");
245
- console.log(" --robot.type=TYPE Robot type (so100_follower)");
246
- console.log(
247
- " --robot.port=PORT Robot serial port (e.g., COM4, /dev/ttyUSB0)"
248
- );
249
- console.log(" --robot.id=ID Robot identifier");
250
- console.log(" --teleop.type=TYPE Teleoperator type (keyboard)");
251
- console.log(
252
- " --fps=FPS Control loop frame rate (default: 60)"
253
- );
254
- console.log(
255
- " --step_size=SIZE Position step size per keypress (default: 10)"
256
- );
257
- console.log(" --duration_s=SECONDS Teleoperation duration in seconds");
258
- console.log(" -h, --help Show this help message");
259
- console.log("");
260
- console.log("Keyboard Controls:");
261
- console.log(" ↑↓ Arrow Keys Shoulder Lift");
262
- console.log(" ←→ Arrow Keys Shoulder Pan");
263
- console.log(" W/S Elbow Flex");
264
- console.log(" A/D Wrist Flex");
265
- console.log(" Q/E Wrist Roll");
266
- console.log(" Space Gripper Toggle");
267
- console.log(" ESC Emergency Stop");
268
- console.log(" Ctrl+C Exit");
269
- console.log("");
270
- console.log("Examples:");
271
- console.log(
272
- " lerobot teleoperate --robot.type=so100_follower --robot.port=COM4 --teleop.type=keyboard"
273
- );
274
- console.log(
275
- " lerobot teleoperate --robot.type=so100_follower --robot.port=COM4 --teleop.type=keyboard --fps=30 --step_size=50"
276
- );
277
- console.log("");
278
- console.log("Use 'lerobot find-port' to discover available ports.");
279
- }
280
-
281
- /**
282
- * CLI entry point when called directly
283
- * Mirrors Python's if __name__ == "__main__": pattern
284
- */
285
- export async function main(args: string[]): Promise<void> {
286
- try {
287
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
288
- showUsage();
289
- return;
290
- }
291
-
292
- const config = parseArgs(args);
293
- await teleoperate(config);
294
- } catch (error) {
295
- if (error instanceof Error) {
296
- console.error("Error:", error.message);
297
- } else {
298
- console.error("Error:", error);
299
- }
300
-
301
- console.error("");
302
- console.error("Please verify:");
303
- console.error("1. The robot is connected to the specified port");
304
- console.error("2. No other application is using the port");
305
- console.error("3. You have permission to access the port");
306
- console.error("");
307
- console.error("Use 'lerobot find-port' to discover available ports.");
308
-
309
- process.exit(1);
310
- }
311
- }
312
-
313
- if (import.meta.url === `file://${process.argv[1]}`) {
314
- const args = process.argv.slice(2);
315
- main(args);
316
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/teleoperators/so100_leader.ts DELETED
@@ -1,41 +0,0 @@
1
- /**
2
- * SO-100 Leader Teleoperator implementation for Node.js
3
- *
4
- * Minimal implementation - calibration logic moved to shared/common/calibration.ts
5
- * This class only handles connection management and basic device operations
6
- */
7
-
8
- import { Teleoperator } from "./teleoperator.js";
9
- import type { TeleoperatorConfig } from "../types/teleoperator-config.js";
10
-
11
- export class SO100Leader extends Teleoperator {
12
- constructor(config: TeleoperatorConfig) {
13
- super(config);
14
-
15
- // Validate that this is an SO-100 leader config
16
- if (config.type !== "so100_leader") {
17
- throw new Error(
18
- `Invalid teleoperator type: ${config.type}. Expected: so100_leader`
19
- );
20
- }
21
- }
22
-
23
- /**
24
- * Calibrate the SO-100 leader teleoperator
25
- * NOTE: Calibration logic has been moved to shared/common/calibration.ts
26
- * This method is kept for backward compatibility but delegates to the main calibrate.ts
27
- */
28
- async calibrate(): Promise<void> {
29
- throw new Error(
30
- "Direct device calibration is deprecated. Use the main calibrate.ts orchestrator instead."
31
- );
32
- }
33
- }
34
-
35
- /**
36
- * Factory function to create SO-100 leader teleoperator
37
- * Mirrors Python's make_teleoperator_from_config pattern
38
- */
39
- export function createSO100Leader(config: TeleoperatorConfig): SO100Leader {
40
- return new SO100Leader(config);
41
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/teleoperators/teleoperator.ts DELETED
@@ -1,148 +0,0 @@
1
- /**
2
- * Base Teleoperator class for Node.js platform
3
- * Uses serialport package for serial communication
4
- * Mirrors Python lerobot/common/teleoperators/teleoperator.py
5
- */
6
-
7
- import { SerialPort } from "serialport";
8
- import { mkdir, writeFile } from "fs/promises";
9
- import { join } from "path";
10
- import type { TeleoperatorConfig } from "../types/teleoperator-config.js";
11
- import { getCalibrationDir, TELEOPERATORS } from "../utils/constants.js";
12
-
13
- export abstract class Teleoperator {
14
- protected port: SerialPort | null = null;
15
- protected config: TeleoperatorConfig;
16
- protected calibrationDir: string;
17
- protected calibrationPath: string;
18
- protected name: string;
19
-
20
- constructor(config: TeleoperatorConfig) {
21
- this.config = config;
22
- this.name = config.type;
23
-
24
- // Determine calibration directory
25
- // Mirrors Python: config.calibration_dir if config.calibration_dir else HF_LEROBOT_CALIBRATION / TELEOPERATORS / self.name
26
- this.calibrationDir =
27
- config.calibration_dir ||
28
- join(getCalibrationDir(), TELEOPERATORS, this.name);
29
-
30
- // Use teleoperator ID or type as filename
31
- const teleopId = config.id || this.name;
32
- this.calibrationPath = join(this.calibrationDir, `${teleopId}.json`);
33
- }
34
-
35
- /**
36
- * Connect to the teleoperator
37
- * Mirrors Python teleoperator.connect()
38
- */
39
- async connect(_calibrate: boolean = false): Promise<void> {
40
- try {
41
- this.port = new SerialPort({
42
- path: this.config.port,
43
- baudRate: 1000000, // Correct baud rate for Feetech motors (SO-100) - matches Python lerobot
44
- dataBits: 8, // 8 data bits - matches Python serial.EIGHTBITS
45
- stopBits: 1, // 1 stop bit - matches Python default
46
- parity: "none", // No parity - matches Python default
47
- autoOpen: false,
48
- });
49
-
50
- // Open the port
51
- await new Promise<void>((resolve, reject) => {
52
- this.port!.open((error) => {
53
- if (error) {
54
- reject(
55
- new Error(
56
- `Failed to open port ${this.config.port}: ${error.message}`
57
- )
58
- );
59
- } else {
60
- resolve();
61
- }
62
- });
63
- });
64
- } catch (error) {
65
- throw new Error(
66
- `Could not connect to teleoperator on port ${this.config.port}`
67
- );
68
- }
69
- }
70
-
71
- /**
72
- * Calibrate the teleoperator
73
- * Must be implemented by subclasses
74
- */
75
- abstract calibrate(): Promise<void>;
76
-
77
- /**
78
- * Disconnect from the teleoperator
79
- * Mirrors Python teleoperator.disconnect()
80
- */
81
- async disconnect(): Promise<void> {
82
- if (this.port && this.port.isOpen) {
83
- await new Promise<void>((resolve) => {
84
- this.port!.close(() => {
85
- resolve();
86
- });
87
- });
88
-
89
- this.port = null;
90
- }
91
- }
92
-
93
- /**
94
- * Save calibration data to JSON file
95
- * Mirrors Python's configuration saving
96
- */
97
- protected async saveCalibration(calibrationData: any): Promise<void> {
98
- // Ensure calibration directory exists
99
- await mkdir(this.calibrationDir, { recursive: true });
100
-
101
- // Save calibration data as JSON
102
- await writeFile(
103
- this.calibrationPath,
104
- JSON.stringify(calibrationData, null, 2)
105
- );
106
-
107
- console.log(`Configuration saved to: ${this.calibrationPath}`);
108
- }
109
-
110
- /**
111
- * Send command to teleoperator via serial port
112
- */
113
- protected async sendCommand(command: string): Promise<void> {
114
- if (!this.port || !this.port.isOpen) {
115
- throw new Error("Teleoperator not connected");
116
- }
117
-
118
- return new Promise<void>((resolve, reject) => {
119
- this.port!.write(command, (error) => {
120
- if (error) {
121
- reject(new Error(`Failed to send command: ${error.message}`));
122
- } else {
123
- resolve();
124
- }
125
- });
126
- });
127
- }
128
-
129
- /**
130
- * Read data from teleoperator
131
- */
132
- protected async readData(timeout: number = 5000): Promise<Buffer> {
133
- if (!this.port || !this.port.isOpen) {
134
- throw new Error("Teleoperator not connected");
135
- }
136
-
137
- return new Promise<Buffer>((resolve, reject) => {
138
- const timer = setTimeout(() => {
139
- reject(new Error("Read timeout"));
140
- }, timeout);
141
-
142
- this.port!.once("data", (data: Buffer) => {
143
- clearTimeout(timer);
144
- resolve(data);
145
- });
146
- });
147
- }
148
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/types/calibration.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * Calibration types for Node.js implementation
3
- */
4
-
5
- import type { SerialPort } from "serialport";
6
-
7
- export interface SO100CalibrationConfig {
8
- deviceType: "so100_follower" | "so100_leader";
9
- port: SerialPort;
10
- motorNames: string[];
11
- motorIds: number[]; // Device-specific motor IDs (e.g., [1,2,3,4,5,6] for SO-100)
12
- driveModes: number[];
13
- calibModes: string[];
14
-
15
- // Protocol-specific configuration
16
- protocol: {
17
- resolution: number; // Motor resolution (e.g., 4096 for STS3215)
18
- homingOffsetAddress: number; // Register address for homing offset (e.g., 31 for STS3215)
19
- homingOffsetLength: number; // Length in bytes for homing offset register
20
- presentPositionAddress: number; // Register address for present position (e.g., 56 for STS3215)
21
- presentPositionLength: number; // Length in bytes for present position register
22
- minPositionLimitAddress: number; // Register address for min position limit (e.g., 9 for STS3215)
23
- minPositionLimitLength: number; // Length in bytes for min position limit register
24
- maxPositionLimitAddress: number; // Register address for max position limit (e.g., 11 for STS3215)
25
- maxPositionLimitLength: number; // Length in bytes for max position limit register
26
- signMagnitudeBit: number; // Sign bit index for homing offset encoding (e.g., 11 for STS3215)
27
- };
28
-
29
- limits: {
30
- position_min: number[];
31
- position_max: number[];
32
- velocity_max: number[];
33
- torque_max: number[];
34
- };
35
- }
36
-
37
- export interface CalibrationResults {
38
- [motorName: string]: {
39
- id: number;
40
- drive_mode: number;
41
- homing_offset: number;
42
- range_min: number;
43
- range_max: number;
44
- };
45
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/types/robot-config.ts DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * Robot configuration types for Node.js implementation
3
- */
4
-
5
- export interface RobotConfig {
6
- type: "so100_follower";
7
- port: string;
8
- id?: string;
9
- calibration_dir?: string;
10
- // SO-100 specific options
11
- disable_torque_on_disconnect?: boolean;
12
- max_relative_target?: number | null;
13
- use_degrees?: boolean;
14
- }
15
-
16
- export interface CalibrateConfig {
17
- robot?: RobotConfig;
18
- teleop?: TeleoperatorConfig;
19
- }
20
-
21
- // Re-export from teleoperator-config for convenience
22
- import type { TeleoperatorConfig } from "./teleoperator-config.js";
23
- export type { TeleoperatorConfig };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/types/teleoperation.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Teleoperation types for Node.js implementation
3
- */
4
-
5
- import type { RobotConfig } from "./robot-config.js";
6
-
7
- export interface TeleoperateConfig {
8
- robot: RobotConfig;
9
- teleop: KeyboardTeleoperationConfig;
10
- fps?: number; // Default: 60
11
- step_size?: number; // Default: 10 (motor position units)
12
- duration_s?: number | null; // Default: null (infinite)
13
- }
14
-
15
- export interface KeyboardTeleoperationConfig {
16
- type: "keyboard"; // Only keyboard for now, expandable later
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/types/teleoperator-config.ts DELETED
@@ -1,11 +0,0 @@
1
- /**
2
- * Teleoperator configuration types for Node.js implementation
3
- */
4
-
5
- export interface TeleoperatorConfig {
6
- type: "so100_leader";
7
- port: string;
8
- id?: string;
9
- calibration_dir?: string;
10
- // SO-100 leader specific options
11
- }
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/utils/constants.ts DELETED
@@ -1,48 +0,0 @@
1
- /**
2
- * Constants for lerobot.js
3
- * Mirrors Python lerobot/common/constants.py
4
- */
5
-
6
- import { homedir } from "os";
7
- import { join } from "path";
8
-
9
- // Device types
10
- export const ROBOTS = "robots";
11
- export const TELEOPERATORS = "teleoperators";
12
-
13
- /**
14
- * Get HF Home directory
15
- * Equivalent to Python's huggingface_hub.constants.HF_HOME
16
- */
17
- export function getHfHome(): string {
18
- if (process.env.HF_HOME) {
19
- return process.env.HF_HOME;
20
- }
21
-
22
- const homeDir = homedir();
23
- return join(homeDir, ".cache", "huggingface");
24
- }
25
-
26
- /**
27
- * Get HF lerobot home directory
28
- * Equivalent to Python's HF_LEROBOT_HOME
29
- */
30
- export function getHfLerobotHome(): string {
31
- if (process.env.HF_LEROBOT_HOME) {
32
- return process.env.HF_LEROBOT_HOME;
33
- }
34
-
35
- return join(getHfHome(), "lerobot");
36
- }
37
-
38
- /**
39
- * Get calibration directory
40
- * Equivalent to Python's HF_LEROBOT_CALIBRATION
41
- */
42
- export function getCalibrationDir(): string {
43
- if (process.env.HF_LEROBOT_CALIBRATION) {
44
- return process.env.HF_LEROBOT_CALIBRATION;
45
- }
46
-
47
- return join(getHfLerobotHome(), "calibration");
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/node/utils/keyboard-teleop.ts DELETED
@@ -1,284 +0,0 @@
1
- /**
2
- * Keyboard teleoperation controller for Node.js terminal
3
- * Handles raw keyboard input and robot position control using the keypress package.
4
- */
5
-
6
- import * as readline from "readline";
7
- import { SO100Follower } from "../robots/so100_follower.js";
8
-
9
- /**
10
- * Keyboard controller for robot teleoperation
11
- * Handles terminal keyboard input and robot position updates
12
- */
13
- export class KeyboardController {
14
- private robot: SO100Follower;
15
- private stepSize: number;
16
- private currentPositions: Record<string, number> = {};
17
- private motorNames = [
18
- "shoulder_pan",
19
- "shoulder_lift",
20
- "elbow_flex",
21
- "wrist_flex",
22
- "wrist_roll",
23
- "gripper",
24
- ];
25
- private running = false;
26
- private gripperState = false; // Toggle state for gripper
27
-
28
- constructor(robot: SO100Follower, stepSize: number = 25) {
29
- this.robot = robot;
30
- this.stepSize = stepSize;
31
- }
32
-
33
- /**
34
- * Start keyboard teleoperation
35
- * Sets up raw keyboard input and initializes robot positions
36
- */
37
- async start(): Promise<void> {
38
- console.log("Initializing keyboard controller...");
39
-
40
- // Initialize current positions from robot
41
- try {
42
- this.currentPositions = await this.readRobotPositions();
43
- } catch (error) {
44
- console.warn(
45
- "Could not read initial robot positions, using calibrated centers"
46
- );
47
- // Initialize with calibrated center positions if available, otherwise use middle positions
48
- const calibratedLimits = this.robot.getCalibrationLimits();
49
- this.motorNames.forEach((motor) => {
50
- const limits = calibratedLimits[motor];
51
- const centerPosition = limits
52
- ? Math.floor((limits.min + limits.max) / 2)
53
- : 2047;
54
- this.currentPositions[motor] = centerPosition;
55
- });
56
- }
57
-
58
- // Set up raw keyboard input
59
- this.setupKeyboardInput();
60
- this.running = true;
61
-
62
- console.log("Keyboard controller ready. Use controls to move robot.");
63
- }
64
-
65
- /**
66
- * Stop keyboard teleoperation
67
- * Cleans up keyboard input handling
68
- */
69
- async stop(): Promise<void> {
70
- this.running = false;
71
-
72
- // Reset terminal to normal mode
73
- if (process.stdin.setRawMode) {
74
- process.stdin.setRawMode(false);
75
- }
76
- process.stdin.removeAllListeners("keypress");
77
-
78
- console.log("Keyboard controller stopped.");
79
- }
80
-
81
- /**
82
- * Get current robot positions
83
- */
84
- async getCurrentPositions(): Promise<Record<string, number>> {
85
- return { ...this.currentPositions };
86
- }
87
-
88
- /**
89
- * Set up keyboard input handling
90
- * Uses readline for cross-platform keyboard input
91
- */
92
- private setupKeyboardInput(): void {
93
- // Set up raw mode for immediate key response
94
- if (process.stdin.setRawMode) {
95
- process.stdin.setRawMode(true);
96
- }
97
- process.stdin.resume();
98
- process.stdin.setEncoding("utf8");
99
-
100
- // Handle keyboard input
101
- process.stdin.on("data", (key: string) => {
102
- if (!this.running) return;
103
-
104
- this.handleKeyPress(key);
105
- });
106
- }
107
-
108
- /**
109
- * Handle individual key presses
110
- * Maps keys to robot motor movements
111
- */
112
- private async handleKeyPress(key: string): Promise<void> {
113
- let positionChanged = false;
114
- const newPositions = { ...this.currentPositions };
115
-
116
- // Handle arrow keys first (they start with ESC but are multi-byte sequences)
117
- if (key.startsWith("\u001b[")) {
118
- const arrowKey = key.slice(2);
119
- switch (arrowKey) {
120
- case "A": // Up arrow
121
- newPositions.shoulder_lift += this.stepSize;
122
- positionChanged = true;
123
- break;
124
- case "B": // Down arrow
125
- newPositions.shoulder_lift -= this.stepSize;
126
- positionChanged = true;
127
- break;
128
- case "C": // Right arrow
129
- newPositions.shoulder_pan += this.stepSize;
130
- positionChanged = true;
131
- break;
132
- case "D": // Left arrow
133
- newPositions.shoulder_pan -= this.stepSize;
134
- positionChanged = true;
135
- break;
136
- }
137
- } else {
138
- // Handle single character keys
139
- const keyCode = key.charCodeAt(0);
140
-
141
- switch (keyCode) {
142
- // Standalone ESC key (emergency stop)
143
- case 27:
144
- if (key.length === 1) {
145
- console.log("\nπŸ›‘ EMERGENCY STOP!");
146
- await this.emergencyStop();
147
- return;
148
- }
149
- break;
150
-
151
- // Regular character keys
152
- case 119: // 'w'
153
- newPositions.elbow_flex += this.stepSize;
154
- positionChanged = true;
155
- break;
156
- case 115: // 's'
157
- newPositions.elbow_flex -= this.stepSize;
158
- positionChanged = true;
159
- break;
160
- case 97: // 'a'
161
- newPositions.wrist_flex -= this.stepSize;
162
- positionChanged = true;
163
- break;
164
- case 100: // 'd'
165
- newPositions.wrist_flex += this.stepSize;
166
- positionChanged = true;
167
- break;
168
- case 113: // 'q'
169
- newPositions.wrist_roll -= this.stepSize;
170
- positionChanged = true;
171
- break;
172
- case 101: // 'e'
173
- newPositions.wrist_roll += this.stepSize;
174
- positionChanged = true;
175
- break;
176
- case 32: // Space
177
- // Toggle gripper
178
- this.gripperState = !this.gripperState;
179
- newPositions.gripper = this.gripperState ? 2300 : 1800;
180
- positionChanged = true;
181
- break;
182
-
183
- // Ctrl+C
184
- case 3:
185
- console.log("\nExiting...");
186
- process.exit(0);
187
- }
188
- }
189
-
190
- if (positionChanged) {
191
- // Apply position limits using calibration
192
- this.enforcePositionLimits(newPositions);
193
-
194
- // Update robot positions - only send changed motors for better performance
195
- try {
196
- await this.writeRobotPositions(newPositions);
197
- this.currentPositions = newPositions;
198
- } catch (error) {
199
- console.warn(
200
- `Failed to update robot positions: ${
201
- error instanceof Error ? error.message : error
202
- }`
203
- );
204
- }
205
- }
206
- }
207
-
208
- /**
209
- * Read current positions from robot
210
- * Uses SO100Follower position reading methods
211
- */
212
- private async readRobotPositions(): Promise<Record<string, number>> {
213
- try {
214
- return await this.robot.getMotorPositions();
215
- } catch (error) {
216
- console.warn(
217
- `Failed to read robot positions: ${
218
- error instanceof Error ? error.message : error
219
- }`
220
- );
221
- // Return default positions as fallback
222
- const positions: Record<string, number> = {};
223
- this.motorNames.forEach((motor, index) => {
224
- positions[motor] = 2047; // STS3215 middle position
225
- });
226
- return positions;
227
- }
228
- }
229
-
230
- /**
231
- * Write positions to robot - optimized to only send changed motors
232
- * This was the key to the smooth performance in the working version
233
- */
234
- private async writeRobotPositions(
235
- newPositions: Record<string, number>
236
- ): Promise<void> {
237
- // Only send commands for motors that actually changed
238
- const changedPositions: Record<string, number> = {};
239
- let hasChanges = false;
240
-
241
- for (const [motor, newPosition] of Object.entries(newPositions)) {
242
- if (Math.abs(this.currentPositions[motor] - newPosition) > 0.5) {
243
- changedPositions[motor] = newPosition;
244
- hasChanges = true;
245
- }
246
- }
247
-
248
- if (hasChanges) {
249
- await this.robot.setMotorPositions(changedPositions);
250
- }
251
- }
252
-
253
- /**
254
- * Enforce position limits based on calibration data
255
- * Uses actual calibrated limits instead of hardcoded defaults
256
- */
257
- private enforcePositionLimits(positions: Record<string, number>): void {
258
- // Get calibrated limits from robot
259
- const calibratedLimits = this.robot.getCalibrationLimits();
260
-
261
- for (const [motor, position] of Object.entries(positions)) {
262
- const limits = calibratedLimits[motor];
263
- if (limits) {
264
- positions[motor] = Math.max(limits.min, Math.min(limits.max, position));
265
- }
266
- }
267
- }
268
-
269
- /**
270
- * Emergency stop - halt all robot movement
271
- */
272
- private async emergencyStop(): Promise<void> {
273
- try {
274
- // Stop all robot movement
275
- // TODO: Implement emergency stop in SO100Follower
276
- console.log("Emergency stop executed.");
277
- await this.stop();
278
- process.exit(0);
279
- } catch (error) {
280
- console.error("Emergency stop failed:", error);
281
- process.exit(1);
282
- }
283
- }
284
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lerobot/web/robots/robot.ts DELETED
@@ -1,171 +0,0 @@
1
- /**
2
- * Base Robot class for Web platform
3
- * Uses Web Serial API for serial communication
4
- */
5
-
6
- import type { RobotConfig } from "../../node/types/robot-config.js";
7
-
8
- // Web Serial API type declarations (minimal for our needs)
9
- declare global {
10
- interface SerialPort {
11
- open(options: { baudRate: number }): Promise<void>;
12
- close(): Promise<void>;
13
- readable: ReadableStream<Uint8Array> | null;
14
- writable: WritableStream<Uint8Array> | null;
15
- }
16
- }
17
-
18
- export abstract class Robot {
19
- protected port: SerialPort | null = null;
20
- protected config: RobotConfig;
21
- protected name: string;
22
- protected reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
23
- protected writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
24
-
25
- constructor(config: RobotConfig) {
26
- this.config = config;
27
- this.name = config.type;
28
- }
29
-
30
- /**
31
- * Connect to the robot using Web Serial API
32
- * Requires user interaction to select port
33
- */
34
- async connect(_calibrate: boolean = false): Promise<void> {
35
- try {
36
- // Request port from user (requires user interaction)
37
- this.port = await navigator.serial.requestPort();
38
-
39
- // Open the port with correct SO-100 baudRate
40
- await this.port.open({ baudRate: 1000000 }); // Correct baudRate for Feetech motors (SO-100)
41
-
42
- // Set up readable and writable streams
43
- if (this.port.readable) {
44
- this.reader = this.port.readable.getReader();
45
- }
46
-
47
- if (this.port.writable) {
48
- this.writer = this.port.writable.getWriter();
49
- }
50
- } catch (error) {
51
- throw new Error(
52
- `Could not connect to robot: ${
53
- error instanceof Error ? error.message : error
54
- }`
55
- );
56
- }
57
- }
58
-
59
- /**
60
- * Calibrate the robot
61
- * Must be implemented by subclasses
62
- */
63
- abstract calibrate(): Promise<void>;
64
-
65
- /**
66
- * Disconnect from the robot
67
- */
68
- async disconnect(): Promise<void> {
69
- if (this.reader) {
70
- await this.reader.cancel();
71
- this.reader.releaseLock();
72
- this.reader = null;
73
- }
74
-
75
- if (this.writer) {
76
- await this.writer.close();
77
- this.writer = null;
78
- }
79
-
80
- if (this.port) {
81
- await this.port.close();
82
- this.port = null;
83
- }
84
- }
85
-
86
- /**
87
- * Save calibration data to browser storage
88
- * Uses localStorage as fallback, IndexedDB preferred for larger data
89
- */
90
- protected async saveCalibration(calibrationData: any): Promise<void> {
91
- const robotId = this.config.id || this.name;
92
- const key = `lerobot_calibration_${this.name}_${robotId}`;
93
-
94
- try {
95
- // Save to localStorage for now (could be enhanced to use File System Access API)
96
- localStorage.setItem(key, JSON.stringify(calibrationData));
97
-
98
- // Optionally trigger download
99
- this.downloadCalibration(calibrationData, robotId);
100
-
101
- console.log(`Configuration saved to browser storage and downloaded.`);
102
- } catch (error) {
103
- this.downloadCalibration(calibrationData, robotId);
104
- console.log(`Configuration downloaded as file.`);
105
- }
106
- }
107
-
108
- /**
109
- * Download calibration data as JSON file
110
- */
111
- private downloadCalibration(calibrationData: any, robotId: string): void {
112
- const dataStr = JSON.stringify(calibrationData, null, 2);
113
- const dataBlob = new Blob([dataStr], { type: "application/json" });
114
-
115
- const url = URL.createObjectURL(dataBlob);
116
- const link = document.createElement("a");
117
- link.href = url;
118
- link.download = `${robotId}_calibration.json`;
119
-
120
- document.body.appendChild(link);
121
- link.click();
122
- document.body.removeChild(link);
123
- URL.revokeObjectURL(url);
124
- }
125
-
126
- /**
127
- * Send command to robot via Web Serial API
128
- */
129
- protected async sendCommand(command: string): Promise<void> {
130
- if (!this.writer) {
131
- throw new Error("Robot not connected");
132
- }
133
-
134
- const encoder = new TextEncoder();
135
- const data = encoder.encode(command);
136
- await this.writer.write(data);
137
- }
138
-
139
- /**
140
- * Read data from robot with timeout
141
- */
142
- protected async readData(timeout: number = 5000): Promise<Uint8Array> {
143
- if (!this.reader) {
144
- throw new Error("Robot not connected");
145
- }
146
-
147
- const timeoutPromise = new Promise<never>((_, reject) => {
148
- setTimeout(() => reject(new Error("Read timeout")), timeout);
149
- });
150
-
151
- const readPromise = this.reader.read().then((result) => {
152
- if (result.done) {
153
- throw new Error("Stream closed");
154
- }
155
- return result.value;
156
- });
157
-
158
- return Promise.race([readPromise, timeoutPromise]);
159
- }
160
-
161
- /**
162
- * Disable torque on disconnect (SO-100 specific)
163
- */
164
- protected async disableTorque(): Promise<void> {
165
- try {
166
- await this.sendCommand("TORQUE_DISABLE\r\n");
167
- } catch (error) {
168
- console.warn("Warning: Could not disable torque on disconnect");
169
- }
170
- }
171
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vite.config.ts CHANGED
@@ -1,13 +1,21 @@
1
  import { defineConfig } from "vite";
2
  import react from "@vitejs/plugin-react";
3
  import { resolve } from "path";
 
4
 
5
  export default defineConfig(({ mode }) => {
 
 
 
6
  const baseConfig = {
7
  plugins: [],
8
  resolve: {
9
  alias: {
10
  "@": resolve(__dirname, "./src"),
 
 
 
 
11
  },
12
  },
13
  };
 
1
  import { defineConfig } from "vite";
2
  import react from "@vitejs/plugin-react";
3
  import { resolve } from "path";
4
+ import { existsSync } from "fs";
5
 
6
  export default defineConfig(({ mode }) => {
7
+ // Check if we're in a workspace environment (has packages/web/src)
8
+ const isWorkspace = existsSync(resolve(__dirname, "./packages/web/src"));
9
+
10
  const baseConfig = {
11
  plugins: [],
12
  resolve: {
13
  alias: {
14
  "@": resolve(__dirname, "./src"),
15
+ // Only add workspace alias if in workspace environment
16
+ ...(isWorkspace && {
17
+ "@lerobot/web": resolve(__dirname, "./packages/web/src"),
18
+ }),
19
  },
20
  },
21
  };