NERDDISCO commited on
Commit
bdc1ac8
·
unverified ·
1 Parent(s): f470f4b

feat: added node support (#8)

Browse files

* feat: change everything

* feat: update example; added proper docs; aligned both web and node packages

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +6 -4
  2. docs/conventions.md +195 -51
  3. docs/getting_started_with_so100.md +2 -2
  4. docs/planning/006_packages_node.md +541 -0
  5. examples/cyberpunk-standalone/src/components/calibration-view.tsx +3 -3
  6. examples/cyberpunk-standalone/src/components/docs-section.tsx +2 -2
  7. examples/cyberpunk-standalone/src/lib/unified-storage.ts +3 -3
  8. examples/cyberpunk-standalone/src/types/robot.ts +1 -1
  9. examples/node-quick-start/.gitignore +30 -0
  10. examples/node-quick-start/README.md +130 -0
  11. examples/node-quick-start/package.json +36 -0
  12. examples/node-quick-start/pnpm-lock.yaml +1174 -0
  13. examples/node-quick-start/src/demo-calibrate.ts +125 -0
  14. examples/node-quick-start/src/demo-find-port.ts +90 -0
  15. examples/node-quick-start/src/demo-teleoperate.ts +207 -0
  16. examples/node-quick-start/src/main.ts +183 -0
  17. examples/node-quick-start/test-homing-offsets.json +44 -0
  18. tsconfig.cli.json → examples/node-quick-start/tsconfig.json +13 -6
  19. examples/node-quick-start/vite.config.ts +39 -0
  20. package.json +5 -9
  21. packages/cli/.gitignore +36 -0
  22. packages/cli/README.md +191 -0
  23. packages/cli/package.json +52 -0
  24. packages/cli/pnpm-lock.yaml +1167 -0
  25. packages/cli/src/cli.ts +441 -0
  26. packages/cli/src/index.ts +13 -0
  27. packages/cli/tsconfig.json +20 -0
  28. packages/cli/vite.config.ts +34 -0
  29. packages/node/.eslintrc.json +23 -0
  30. packages/node/.gitignore +36 -0
  31. packages/node/README.md +408 -0
  32. packages/node/package.json +76 -0
  33. packages/node/pnpm-lock.yaml +2396 -0
  34. packages/node/src/calibrate.ts +288 -0
  35. packages/node/src/find_port.ts +299 -0
  36. packages/node/src/index.ts +64 -0
  37. packages/node/src/release_motors.ts +70 -0
  38. packages/node/src/robots/so100_config.ts +98 -0
  39. packages/node/src/teleoperate.ts +223 -0
  40. packages/node/src/teleoperators/base-teleoperator.ts +62 -0
  41. packages/node/src/teleoperators/direct-teleoperator.ts +112 -0
  42. packages/node/src/teleoperators/index.ts +14 -0
  43. packages/node/src/teleoperators/keyboard-teleoperator.ts +292 -0
  44. packages/node/src/types/calibration.ts +48 -0
  45. packages/node/src/types/port-discovery.ts +47 -0
  46. packages/node/src/types/robot-config.ts +40 -0
  47. packages/node/src/types/robot-connection.ts +61 -0
  48. packages/node/src/types/teleoperation.ts +95 -0
  49. {src/lerobot/node → packages/node/src}/utils/constants.ts +2 -6
  50. packages/node/src/utils/motor-calibration.ts +188 -0
README.md CHANGED
@@ -5,15 +5,17 @@
5
  ## Install
6
 
7
  ```bash
8
- # Web library (available now)
9
  npm install @lerobot/web
10
 
11
- # Node.js library (coming soon)
12
- # npm install @lerobot/node
13
  ```
14
 
15
  ## Resources
16
 
17
  - **LeRobot.js**: [Introduction post on Hugging Face](https://huggingface.co/blog/NERDDISCO/lerobotjs)
18
- - **Documentation**: See [`@lerobot/web` README](./packages/web/README.md) for complete API reference
 
 
19
  - **Live Demo**: Try it online at [huggingface.co/spaces/NERDDISCO/LeRobot.js](https://huggingface.co/spaces/NERDDISCO/LeRobot.js)
 
5
  ## Install
6
 
7
  ```bash
8
+ # Web library
9
  npm install @lerobot/web
10
 
11
+ # Node.js library
12
+ npm install @lerobot/node
13
  ```
14
 
15
  ## Resources
16
 
17
  - **LeRobot.js**: [Introduction post on Hugging Face](https://huggingface.co/blog/NERDDISCO/lerobotjs)
18
+ - **Documentation**:
19
+ - [`@lerobot/web` README](./packages/web/README.md) - Browser (WebSerial + WebUSB)
20
+ - [`@lerobot/node` README](./packages/node/README.md) - Node.js (Serialport)
21
  - **Live Demo**: Try it online at [huggingface.co/spaces/NERDDISCO/LeRobot.js](https://huggingface.co/spaces/NERDDISCO/LeRobot.js)
docs/conventions.md CHANGED
@@ -11,6 +11,7 @@
11
  ## Core Rules
12
 
13
  - **Never Start/Stop Dev Server**: The development server is already managed by the user - never run commands to start, stop, or restart the server
 
14
  - **Python lerobot Faithfulness**: Maintain exact UX/API compatibility with Python lerobot - commands, terminology, and workflows must match identically
15
  - **Serial API Separation**: Always use `serialport` package for Node.js and Web Serial API for browsers - never mix or bridge these incompatible APIs
16
  - **Minimal Console Output**: Only show essential information - reduce cognitive load for users
@@ -74,7 +75,7 @@
74
 
75
  - **Identical Commands**: `npx lerobot find-port` matches `python -m lerobot.find_port`
76
  - **Same Terminology**: Use "MotorsBus", not "robot arms" - keep Python's exact wording
77
- - **Matching Output**: Error messages, prompts, and flow identical to Python version
78
  - **Familiar Workflows**: Python lerobot users should feel immediately at home
79
  - **CLI Compatibility**: Direct migration path from Python CLI
80
 
@@ -193,7 +194,7 @@ lerobot/
193
  - **Integration Tests**: Test component interactions
194
  - **E2E Tests**: Playwright for full workflow testing
195
  - **Hardware Tests**: Mock/stub hardware interfaces for CI
196
- - **UX Compatibility Tests**: Verify outputs match Python version
197
 
198
  ## Package Structure
199
 
@@ -218,6 +219,69 @@ lerobot/
218
  - **Hardware**: Platform-specific libraries for device access
219
  - **Development**: Vitest, ESLint, Prettier
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  ## Platform-Specific Implementation
222
 
223
  ### Node.js Implementation (Python-Compatible Foundation)
@@ -239,6 +303,45 @@ lerobot/
239
  - **Port Discovery**: Programmatic port enumeration without user dialogs
240
  - **Process Management**: Direct process control and system integration
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  ### Web Implementation (Modern Robotics Interface)
243
 
244
  **Web provides superior robotics UX by building on Node.js's proven hardware protocols**
@@ -583,83 +686,124 @@ const STS3215_REGISTERS = {
583
 
584
  **This sequence debugging took extensive analysis to solve. Future implementations MUST follow this exact pattern to maintain Python compatibility.**
585
 
586
- #### CRITICAL: Smooth Motor Control Recipe (PROVEN WORKING)
 
 
 
 
587
 
588
- **These patterns provide buttery-smooth, responsive motor control. Deviating from this recipe causes stuttering, lag, or poor responsiveness.**
589
 
590
- ##### 🚀 Performance Optimizations (KEEP THESE!)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
 
592
- **1. Optimal Step Size**
593
 
594
- - **✅ PERFECT**: `25` units per keypress (responsive but not jumpy)
595
- - **❌ WRONG**: `5` units (too sluggish) or `100` units (too aggressive)
 
596
 
597
- **2. Minimal Motor Communication Delay**
598
 
599
- - **✅ PERFECT**: `1ms` delay between motor commands
600
- - **❌ WRONG**: `5ms+` delays cause stuttering
601
 
602
- **3. Smart Motor Updates (CRITICAL FOR SMOOTHNESS)**
603
 
604
- - **✅ PERFECT**: Only send commands for motors that actually changed
605
- - **✅ PERFECT**: Use `0.5` unit threshold to detect meaningful changes
606
- - **❌ WRONG**: Send ALL motor positions every time (causes serial bus conflicts)
607
 
608
- **4. Change Detection Threshold**
609
 
610
- - **✅ PERFECT**: `0.5` units prevents micro-movements and unnecessary commands
611
- - **❌ WRONG**: `0.1` units (too sensitive) or no threshold (constant spam)
612
 
613
- ##### 🎯 Teleoperation Loop Best Practices
614
 
615
- **1. Eliminate Display Spam**
616
 
617
- - **✅ PERFECT**: Minimal loop with just duration checks and 100ms delay
618
- - **❌ WRONG**: Constant position reading and display updates (causes 90ms+ lag)
 
619
 
620
- **2. Event-Driven Keyboard Input**
621
 
622
- - **✅ PERFECT**: Use `process.stdin.on("data")` for immediate response
623
- - **❌ WRONG**: Polling-based input with timers (adds delay)
624
 
625
- ##### 🔧 Hardware Communication Patterns
 
626
 
627
- **1. Discrete Step-Based Control**
628
 
629
- - **✅ PERFECT**: Immediate position updates on keypress
630
- - **❌ WRONG**: Continuous/velocity-based control (causes complexity and lag)
631
 
632
- **2. Direct Motor Position Writing**
633
 
634
- - **✅ PERFECT**: Simple, immediate motor updates with position limits
635
- - **❌ WRONG**: Complex interpolation, target positions, multiple update cycles
636
 
637
- ##### 🎮 Proven Working Values
638
 
639
- **Key Configuration Values:**
 
 
 
 
640
 
641
- - `stepSize = 25` (default in teleoperate.ts and keyboard_teleop.ts)
642
- - `1ms` motor communication delay (so100_follower.ts)
643
- - `0.5` unit change detection threshold
644
- - `100ms` teleoperation loop delay
645
 
646
- ##### ⚠️ Performance Killers (NEVER DO THESE)
 
 
 
 
 
647
 
648
- 1. **❌ Display Updates in Main Loop**: Causes 90ms+ loop times
649
- 2. **❌ Continuous/Velocity Control**: Adds complexity without benefit for keyboard input
650
- 3. **❌ All-Motor Updates**: Sends unnecessary commands, overwhelms serial bus
651
- 4. **❌ Long Communication Delays**: 5ms+ delays cause stuttering
652
- 5. **❌ Complex Interpolation**: Adds latency for simple step-based control
653
- 6. **❌ No Change Detection**: Spams motors with identical positions
654
 
655
- ##### 📊 Performance Metrics (When It's Working Right)
656
 
657
- - **Keypress Response**: Immediate (< 10ms)
658
- - **Motor Update**: Single command per changed motor
659
- - **Loop Time**: < 5ms (when not reading positions)
660
- - **User Experience**: "Buttery smooth", "fucking working and super perfect"
 
661
 
662
- **Golden Rule**: When you achieve smooth control, NEVER change the step size, delays, or update patterns without extensive testing. These values were optimized through real hardware testing.
663
 
664
  ## Clean Library Architecture (Critical Lessons)
665
 
 
11
  ## Core Rules
12
 
13
  - **Never Start/Stop Dev Server**: The development server is already managed by the user - never run commands to start, stop, or restart the server
14
+ - **Package Manager**: Always use `pnpm` for package management - never use `npm` or `yarn` in documentation, scripts, or commands
15
  - **Python lerobot Faithfulness**: Maintain exact UX/API compatibility with Python lerobot - commands, terminology, and workflows must match identically
16
  - **Serial API Separation**: Always use `serialport` package for Node.js and Web Serial API for browsers - never mix or bridge these incompatible APIs
17
  - **Minimal Console Output**: Only show essential information - reduce cognitive load for users
 
75
 
76
  - **Identical Commands**: `npx lerobot find-port` matches `python -m lerobot.find_port`
77
  - **Same Terminology**: Use "MotorsBus", not "robot arms" - keep Python's exact wording
78
+ - **Matching Output**: Error messages, prompts, and flow identical to Python lerobot
79
  - **Familiar Workflows**: Python lerobot users should feel immediately at home
80
  - **CLI Compatibility**: Direct migration path from Python CLI
81
 
 
194
  - **Integration Tests**: Test component interactions
195
  - **E2E Tests**: Playwright for full workflow testing
196
  - **Hardware Tests**: Mock/stub hardware interfaces for CI
197
+ - **UX Compatibility Tests**: Verify outputs match Python lerobot
198
 
199
  ## Package Structure
200
 
 
219
  - **Hardware**: Platform-specific libraries for device access
220
  - **Development**: Vitest, ESLint, Prettier
221
 
222
+ ### Package Architecture Standards
223
+
224
+ **Multi-Platform Package Structure:**
225
+
226
+ ```
227
+ packages/
228
+ ├── web/ # Browser-focused package (@lerobot/web)
229
+ │ ├── src/
230
+ │ ├── package.json # Web Serial API, browser dependencies
231
+ │ └── README.md # Browser-specific examples
232
+ ├── node/ # Node.js-focused package (@lerobot/node)
233
+ │ ├── src/
234
+ │ ├── package.json # serialport dependency, library only
235
+ │ └── README.md # Node.js library examples
236
+ └── cli/ # CLI package (lerobot)
237
+ ├── src/
238
+ ├── package.json # CLI binary, depends on @lerobot/node
239
+ └── README.md # Python lerobot compatible commands
240
+ ```
241
+
242
+ **API Consistency Rules:**
243
+
244
+ - Identical function signatures across packages where possible
245
+ - Platform-specific adaptations in types and implementations only
246
+ - Shared constants and protocols via dedicated utils
247
+ - Cross-platform compatibility for data formats (calibration files, etc.)
248
+
249
+ **CLI Architecture & Separation of Concerns:**
250
+
251
+ - **Library (`@lerobot/node`)**: Pure programmatic API for Node.js applications
252
+
253
+ - `findPort()` returns robot connections programmatically
254
+ - No interactive prompts, CLI output, or user input handling
255
+ - Matches `@lerobot/web` API design for consistency
256
+
257
+ - **CLI (`lerobot`)**: Python lerobot compatible command-line interface
258
+
259
+ - Separate package in `packages/cli/` with `npx lerobot` binary
260
+ - Uses `@lerobot/node` library internally for all functionality
261
+ - Handles interactive prompts, user input, and CLI-specific UX
262
+ - Interactive by default - no flags required for standard workflows
263
+ - Identical command syntax and behavior to Python lerobot
264
+
265
+ - **Architectural Principle**: Libraries provide capabilities, CLIs provide experience
266
+ - Interactive behavior belongs in CLI commands, not library functions
267
+ - Library users get clean APIs, CLI users get Python-compatible workflows
268
+
269
+ ### Local Package References
270
+
271
+ When creating examples that depend on workspace packages, use `file:` references:
272
+
273
+ ```json
274
+ {
275
+ "dependencies": {
276
+ "@lerobot/web": "file:../../packages/web",
277
+ "@lerobot/node": "file:../../packages/node",
278
+ "lerobot": "file:../../packages/cli"
279
+ }
280
+ }
281
+ ```
282
+
283
+ **Never use `workspace:*` in examples** - this is only for the root workspace `package.json`.
284
+
285
  ## Platform-Specific Implementation
286
 
287
  ### Node.js Implementation (Python-Compatible Foundation)
 
303
  - **Port Discovery**: Programmatic port enumeration without user dialogs
304
  - **Process Management**: Direct process control and system integration
305
 
306
+ #### Node.js Serial Communication (Critical Implementation Details)
307
+
308
+ **Event-Driven Communication (Proven Working Approach):**
309
+
310
+ - **Event-Based Reading**: Use `port.once('data')` with timeout promises
311
+ - **Never Use Wrapper Polling**: Avoid `port.read(timeout)` wrappers - they add latency and unreliability
312
+ - **Direct SerialPort Access**: Expose underlying `SerialPort` instance for event listening
313
+
314
+ **Timing Constants for STS3215 Motors:**
315
+
316
+ ```typescript
317
+ const STS3215_PROTOCOL = {
318
+ WRITE_TO_READ_DELAY: 0, // No delay before read - immediate event listening
319
+ RETRY_DELAY: 50, // Base retry delay (multiplied by attempt number)
320
+ INTER_MOTOR_DELAY: 10, // Small delay between motor operations
321
+ MAX_RETRIES: 3,
322
+ };
323
+ ```
324
+
325
+ **Progressive Timeout Pattern:**
326
+
327
+ ```typescript
328
+ // Timeout increases with retry attempts: 100ms, 200ms, 300ms
329
+ const timeout = 100 * attempts;
330
+ const response = await new Promise((resolve, reject) => {
331
+ const timer = setTimeout(() => reject(new Error("Read timeout")), timeout);
332
+ underlyingPort.once("data", (data) => {
333
+ clearTimeout(timer);
334
+ resolve(new Uint8Array(data));
335
+ });
336
+ });
337
+ ```
338
+
339
+ **Connection Architecture:**
340
+
341
+ - **Only `findPort()` Creates Connections**: No other high-level function should create new serial connections
342
+ - **Initialized Port Returns**: `findPort()` must return ready-to-use, initialized ports
343
+ - **Connection Reuse**: All functions (`calibrate`, `teleoperate`, `releaseMotors`) use existing connection from `findPort`
344
+
345
  ### Web Implementation (Modern Robotics Interface)
346
 
347
  **Web provides superior robotics UX by building on Node.js's proven hardware protocols**
 
686
 
687
  **This sequence debugging took extensive analysis to solve. Future implementations MUST follow this exact pattern to maintain Python compatibility.**
688
 
689
+ #### CRITICAL: Node.js Teleoperation Patterns (PROVEN WORKING)
690
+
691
+ **These patterns provide smooth, responsive teleoperation in Node.js. Deviating from this recipe causes delays, stuttering, or poor user experience.**
692
+
693
+ ##### 🚀 Node.js Keyboard Control Architecture (FINAL SOLUTION)
694
 
695
+ **The Challenge: Node.js stdin vs Browser Keyboard Events**
696
 
697
+ - **Browser**: Has real `keydown` and `keyup` events → perfect control
698
+ - **Node.js**: Only has `keypress` events → must simulate keyup with timeouts
699
+ - **OS Keyboard Repeat**: ~250-500ms delay between first press and repeat stream
700
+
701
+ **✅ PROVEN WORKING SOLUTION:**
702
+
703
+ ```typescript
704
+ // Optimal configuration values (DO NOT CHANGE without extensive testing)
705
+ export const KEYBOARD_TELEOPERATOR_DEFAULTS = {
706
+ stepSize: 8, // Match browser demo step size
707
+ updateRate: 120, // High frequency for smooth movement (120 Hz)
708
+ keyTimeout: 150, // Balance single taps vs continuous movement
709
+ } as const;
710
+ ```
711
+
712
+ **1. Hybrid Movement Pattern**
713
+
714
+ - **✅ PERFECT**: Immediate movement on first keypress + continuous interval updates
715
+ - **❌ WRONG**: Only immediate movement (no continuous) or only interval movement (has delay)
716
+
717
+ ```typescript
718
+ // On keypress: Move immediately + start/refresh continuous movement
719
+ private handleKeyboardInput(key: string): void {
720
+ const keyName = this.mapKeyToName(key);
721
+ if (keyName && this.keyboardControls[keyName]) {
722
+ if (this.keyStates[keyName]) {
723
+ // Key repeat - just refresh timestamp
724
+ this.keyStates[keyName].timestamp = Date.now();
725
+ } else {
726
+ // New keypress - immediate movement + start continuous
727
+ this.updateKeyState(keyName, true);
728
+ this.moveMotorForKey(keyName); // ← IMMEDIATE, no delay
729
+ }
730
+ }
731
+ }
732
+ ```
733
 
734
+ **2. Optimal Key Timeout Balance**
735
 
736
+ - **✅ PERFECT**: `150ms` - Good single taps, minimal continuous gap
737
+ - **❌ WRONG**: `50ms` (single taps too short) or `600ms` (single taps too long)
738
+ - **Why 150ms**: Bridges most OS keyboard repeat delay without making single taps sluggish
739
 
740
+ **3. High-Frequency Updates**
741
 
742
+ - **✅ PERFECT**: `120 Hz` update rate for smooth continuous movement
743
+ - **❌ WRONG**: `60 Hz` (visible stuttering) or `200+ Hz` (unnecessary CPU load)
744
 
745
+ ##### 🎯 Development Workflow Optimization
746
 
747
+ **Package Development Without Constant Rebuilding:**
 
 
748
 
749
+ **✅ PERFECT Development Setup:**
750
 
751
+ 1. **Terminal 1**: `cd packages/node && pnpm dev` (watch mode)
752
+ 2. **Terminal 2**: `cd packages/cli && pnpm dev teleoperate ...` (direct TypeScript execution)
753
 
754
+ **❌ WRONG**: Constantly running `pnpm build` and clearing `node_modules`
755
 
756
+ **Why This Works:**
757
 
758
+ - Node package rebuilds automatically on changes
759
+ - CLI dev mode uses `vite-node` to run TypeScript directly
760
+ - No package caching issues, immediate feedback
761
 
762
+ ##### 🔧 CLI Architecture Lessons
763
 
764
+ **1. No User-Facing Configuration**
 
765
 
766
+ - **✅ PERFECT**: `stepSize` handled internally by teleoperator defaults
767
+ - **❌ WRONG**: Exposing `--step-size` CLI parameter (users don't understand motor units)
768
 
769
+ **2. Python lerobot Parameter Compatibility**
770
 
771
+ - **✅ PERFECT**: `--robot.type`, `--robot.port`, `--robot.id`, `--teleop.type`
772
+ - **❌ WRONG**: Different parameter names or structure than Python lerobot
773
 
774
+ **3. Library vs CLI Separation**
775
 
776
+ - **✅ PERFECT**: Library provides capabilities, CLI provides Python-compatible UX
777
+ - **❌ WRONG**: Library handling CLI concerns or CLI reimplementing library logic
778
 
779
+ ##### 🎮 Performance Characteristics (When Working Right)
780
 
781
+ - **First Keypress Response**: Immediate (0ms delay)
782
+ - **Continuous Movement**: Smooth 120 Hz updates
783
+ - **Single Tap Duration**: ~150ms (1-2 motor movements)
784
+ - **Key Repeat Transition**: Seamless (no gap)
785
+ - **User Experience**: "Almost perfect", "way better", "smooth movement"
786
 
787
+ ##### ⚠️ Node.js Teleoperation Anti-Patterns (NEVER DO THESE)
 
 
 
788
 
789
+ 1. **❌ Only Timeout-Based Movement**: Causes initial delay on every keypress
790
+ 2. **❌ Only Immediate Movement**: No continuous movement when holding keys
791
+ 3. **❌ Long Key Timeouts (>300ms)**: Makes single taps feel sluggish
792
+ 4. **❌ Short Key Timeouts (<100ms)**: Breaks continuous movement due to OS repeat delay
793
+ 5. **❌ Low Update Rates (<100Hz)**: Visible stuttering during continuous movement
794
+ 6. **❌ Exposing stepSize to CLI**: Users can't meaningfully configure motor position units
795
 
796
+ ##### 📊 Debugging Keyboard Issues
 
 
 
 
 
797
 
798
+ **Symptoms and Solutions:**
799
 
800
+ - **"Initial delay when holding key"** → OS keyboard repeat delay, increase keyTimeout
801
+ - **"Single taps move too far"** keyTimeout too long, reduce to 150ms or less
802
+ - **"Stuttering during continuous movement"** updateRate too low, increase to 120Hz
803
+ - **"No continuous movement"** → keyTimeout too short, increase above OS repeat delay
804
+ - **"Immediate movement missing"** → Must call `moveMotorForKey()` on first keypress
805
 
806
+ **Golden Rule**: The 150ms keyTimeout + 120Hz updateRate + immediate first movement pattern was achieved through extensive testing. Don't change these values without thorough hardware validation.
807
 
808
  ## Clean Library Architecture (Critical Lessons)
809
 
docs/getting_started_with_so100.md CHANGED
@@ -34,13 +34,13 @@ Reconnect the USB cable.
34
  ### check follower arm
35
 
36
  ```
37
- python -m lerobot.check_motors --robot.port=COM4
38
  ```
39
 
40
  ### check leader arm
41
 
42
  ```
43
- python -m lerobot.check_motors --teleop.port=COM3
44
  ```
45
 
46
  **If you see this - you're lucky! Skip to calibration:**
 
34
  ### check follower arm
35
 
36
  ```
37
+ python -m lerobot.setup_motors --robot.port=COM4 --robot.type=so100_follower
38
  ```
39
 
40
  ### check leader arm
41
 
42
  ```
43
+ python -m lerobot.setup_motors --teleop.port=COM3 --robot.type=so100_leader
44
  ```
45
 
46
  **If you see this - you're lucky! Skip to calibration:**
docs/planning/006_packages_node.md ADDED
@@ -0,0 +1,541 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # User Story 006: Node.js Package Architecture
2
+
3
+ ## Story
4
+
5
+ **As a** robotics developer building server-side applications, CLI tools, and desktop robotics software
6
+ **I want** to use lerobot.js functionality directly from Node.js with the same API as the web version
7
+ **So that** I can build Node.js applications, command-line tools, and desktop software without browser constraints while maintaining familiar APIs
8
+
9
+ ## Background
10
+
11
+ We have successfully implemented `packages/web` that provides `findPort`, `calibrate`, `releaseMotors`, and `teleoperate` functionality using Web APIs (Web Serial, Web USB). This package is published as `@lerobot/web` and provides a clean, typed API for browser-based robotics applications.
12
+
13
+ We also have existing Node.js code in `src/lerobot/node` that was working but abandoned when we focused on getting the web version right. Now that the web version is stable and proven, we want to create a proper `packages/node` package that:
14
+
15
+ 1. **Mirrors the Web API**: Provides the same function signatures and behavior as `@lerobot/web`
16
+ 2. **Uses Node.js APIs**: Leverages `serialport` instead of Web Serial for hardware communication
17
+ 3. **Python lerobot Faithfulness**: Maintains exact compatibility with Python lerobot CLI commands and behavior
18
+ 4. **Server-Side Ready**: Enables robotics applications in Node.js servers, CLI tools, and desktop applications
19
+ 5. **Reuses Proven Logic**: Builds on existing `src/lerobot/node` code that was already working
20
+
21
+ This will enable developers to use the same lerobot.js API in both browser and Node.js environments, choosing the appropriate platform based on their application needs.
22
+
23
+ ## Acceptance Criteria
24
+
25
+ ### Core Functionality
26
+
27
+ - [ ] **Same API Surface**: Mirror `@lerobot/web` API with identical function signatures where possible
28
+ - [ ] **Four Core Functions**: Implement `findPort`, `calibrate`, `releaseMotors`, and `teleoperate`
29
+ - [ ] **SerialPort Integration**: Use `serialport` package instead of Web Serial API
30
+ - [ ] **TypeScript Support**: Full TypeScript coverage with strict type checking
31
+ - [ ] **NPM Package**: Published as `@lerobot/node` with proper package.json
32
+
33
+ ### Platform Requirements
34
+
35
+ - [ ] **Node.js 18+**: Support current LTS and newer versions
36
+ - [ ] **Cross-Platform**: Work on Windows, macOS, and Linux
37
+ - [ ] **ES Modules**: Use ES module format for consistency with web package
38
+ - [ ] **CLI Integration**: Enable `npx lerobot` commands using this package
39
+ - [ ] **No Browser Dependencies**: No Web API dependencies or browser-specific code
40
+
41
+ ### API Alignment
42
+
43
+ - [ ] **Same Types**: Reuse or mirror types from `@lerobot/web` where appropriate
44
+ - [ ] **Same Exports**: Mirror the export structure of `@lerobot/web/index.ts`
45
+ - [ ] **Same Behavior**: Identical behavior for shared functionality (calibration algorithms, motor control)
46
+ - [ ] **Platform-Specific Adaptations**: Handle Node.js-specific differences (file system, process management)
47
+
48
+ ### Code Quality
49
+
50
+ - [ ] **Reuse Existing Code**: Build on proven `src/lerobot/node` implementations
51
+ - [ ] **No Code Duplication**: Share logic with web package where possible (copy for now, per requirements)
52
+ - [ ] **Clean Architecture**: Follow the same patterns as `packages/web`
53
+ - [ ] **Comprehensive Testing**: Unit tests for all core functionality
54
+
55
+ ## Expected User Flow
56
+
57
+ ### Installation and Usage
58
+
59
+ ```bash
60
+ # Install the Node.js package
61
+ npm install @lerobot/node
62
+
63
+ # Use in Node.js applications
64
+ import { findPort, calibrate, teleoperate } from "@lerobot/node";
65
+ ```
66
+
67
+ ### Find Port (Node.js)
68
+
69
+ ```typescript
70
+ // Node.js - programmatic usage
71
+ import { findPort } from "@lerobot/node";
72
+
73
+ const portProcess = await findPort();
74
+ const availablePorts = await portProcess.getAvailablePorts();
75
+ console.log("Available ports:", availablePorts);
76
+
77
+ // Interactive mode (CLI-like) - matches Python lerobot exactly
78
+ const portProcess = await findPort({
79
+ interactive: true, // shows "disconnect cable" prompts like Python
80
+ });
81
+ const detectedPort = await portProcess.detectPort();
82
+ ```
83
+
84
+ ### Calibration (Node.js)
85
+
86
+ ```typescript
87
+ // Node.js - same API as web
88
+ import { calibrate } from "@lerobot/node";
89
+
90
+ const calibrationProcess = await calibrate({
91
+ robot: {
92
+ type: "so100_follower",
93
+ port: "/dev/ttyUSB0", // or "COM4" on Windows
94
+ robotId: "my_follower_arm",
95
+ },
96
+ onLiveUpdate: (data) => {
97
+ console.log("Live calibration data:", data);
98
+ },
99
+ });
100
+
101
+ const results = await calibrationProcess.result;
102
+ console.log("Calibration completed:", results);
103
+ ```
104
+
105
+ ### Teleoperation (Node.js)
106
+
107
+ ```typescript
108
+ // Node.js - same API as web
109
+ import { teleoperate } from "@lerobot/node";
110
+
111
+ const teleoperationProcess = await teleoperate({
112
+ robot: {
113
+ type: "so100_follower",
114
+ port: "/dev/ttyUSB0",
115
+ robotId: "my_follower_arm",
116
+ },
117
+ teleop: {
118
+ type: "keyboard",
119
+ stepSize: 25,
120
+ },
121
+ calibrationData: loadedCalibrationData,
122
+ onStateUpdate: (state) => {
123
+ console.log("Robot state:", state);
124
+ },
125
+ });
126
+
127
+ teleoperationProcess.start();
128
+ ```
129
+
130
+ ### Release Motors (Node.js)
131
+
132
+ ```typescript
133
+ // Node.js - same API as web
134
+ import { releaseMotors } from "@lerobot/node";
135
+
136
+ await releaseMotors({
137
+ robot: {
138
+ type: "so100_follower",
139
+ port: "/dev/ttyUSB0",
140
+ robotId: "my_follower_arm",
141
+ },
142
+ });
143
+ ```
144
+
145
+ ### CLI Integration
146
+
147
+ ```bash
148
+ # Python lerobot compatibility - same commands work
149
+ npx lerobot find-port
150
+ npx lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0
151
+ npx lerobot teleoperate --robot.type=so100_follower --robot.port=/dev/ttyUSB0
152
+
153
+ # Global installation also works
154
+ npm install -g @lerobot/node
155
+ lerobot find-port
156
+ lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0
157
+ ```
158
+
159
+ ## Implementation Details
160
+
161
+ ### File Structure
162
+
163
+ ```
164
+ packages/node/
165
+ ├── package.json # NPM package configuration
166
+ ├── tsconfig.build.json # TypeScript build configuration
167
+ ├── README.md # Package documentation
168
+ ├── CHANGELOG.md # Version history
169
+ └── src/
170
+ ├── index.ts # Main exports (mirror web package)
171
+ ├── find_port.ts # Port discovery using serialport
172
+ ├── calibrate.ts # Calibration using Node.js APIs
173
+ ├── teleoperate.ts # Teleoperation using Node.js APIs
174
+ ├── release_motors.ts # Motor release using Node.js APIs
175
+ ├── types/
176
+ │ ├── robot-connection.ts # Robot connection types
177
+ │ ├── port-discovery.ts # Port discovery types
178
+ │ ├── calibration.ts # Calibration types
179
+ │ ├── teleoperation.ts # Teleoperation types
180
+ │ └── robot-config.ts # Robot configuration types
181
+ ├── utils/
182
+ │ ├── serial-port-wrapper.ts # SerialPort wrapper
183
+ │ ├── motor-communication.ts # Motor communication utilities
184
+ │ ├── motor-calibration.ts # Calibration utilities
185
+ │ └── sts3215-protocol.ts # Protocol constants
186
+ ├── robots/
187
+ │ └── so100_config.ts # SO-100 configuration
188
+ └── teleoperators/
189
+ ├── index.ts # Teleoperator exports
190
+ ├── base-teleoperator.ts # Base teleoperator class
191
+ └── keyboard-teleoperator.ts # Keyboard teleoperator
192
+ ```
193
+
194
+ ### Key Dependencies
195
+
196
+ #### Core Dependencies
197
+
198
+ - **serialport**: Node.js serial communication (replaces Web Serial API)
199
+ - **chalk**: Terminal colors and formatting
200
+ - **commander**: CLI argument parsing
201
+
202
+ #### Development Dependencies
203
+
204
+ - **typescript**: TypeScript compiler
205
+ - **@types/node**: Node.js type definitions
206
+ - **vitest**: Testing framework
207
+
208
+ ### Migration Strategy
209
+
210
+ #### Phase 1: Package Setup
211
+
212
+ - [ ] Create `packages/node` directory structure
213
+ - [ ] Set up package.json with proper exports
214
+ - [ ] Configure TypeScript build process
215
+ - [ ] Set up testing infrastructure
216
+
217
+ #### Phase 2: Core Function Migration
218
+
219
+ - [ ] Migrate `src/lerobot/node/find_port.ts` to `packages/node/src/find_port.ts`
220
+ - [ ] Migrate `src/lerobot/node/calibrate.ts` to `packages/node/src/calibrate.ts`
221
+ - [ ] Migrate `src/lerobot/node/teleoperate.ts` to `packages/node/src/teleoperate.ts`
222
+ - [ ] Create `release_motors.ts` using existing motor communication code
223
+
224
+ #### Phase 3: API Alignment
225
+
226
+ - [ ] Ensure all functions match `@lerobot/web` signatures
227
+ - [ ] Copy and adapt types from `packages/web/src/types/`
228
+ - [ ] Update utilities to use serialport instead of Web Serial
229
+ - [ ] Test API compatibility with existing web examples
230
+
231
+ #### Phase 4: Testing and Documentation
232
+
233
+ - [ ] Create comprehensive tests for all functions
234
+ - [ ] Update documentation and examples
235
+ - [ ] Validate Python lerobot CLI compatibility
236
+ - [ ] Test cross-platform compatibility
237
+
238
+ ### Core Functions to Implement
239
+
240
+ #### Package Exports (Mirror Web Package)
241
+
242
+ ```typescript
243
+ // packages/node/src/index.ts
244
+ export { calibrate } from "./calibrate.js";
245
+ export { teleoperate } from "./teleoperate.js";
246
+ export { findPort } from "./find_port.js";
247
+ export { releaseMotors } from "./release_motors.js";
248
+
249
+ // Types (mirror web package)
250
+ export type {
251
+ RobotConnection,
252
+ RobotConfig,
253
+ SerialPort,
254
+ SerialPortInfo,
255
+ SerialOptions,
256
+ } from "./types/robot-connection.js";
257
+
258
+ export type {
259
+ FindPortConfig,
260
+ FindPortProcess,
261
+ } from "./types/port-discovery.js";
262
+
263
+ export type {
264
+ CalibrateConfig,
265
+ CalibrationResults,
266
+ LiveCalibrationData,
267
+ CalibrationProcess,
268
+ } from "./types/calibration.js";
269
+
270
+ export type {
271
+ MotorConfig,
272
+ TeleoperationState,
273
+ TeleoperationProcess,
274
+ TeleoperateConfig,
275
+ TeleoperatorConfig,
276
+ } from "./types/teleoperation.js";
277
+
278
+ // Node.js utilities
279
+ export { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js";
280
+ export { createSO100Config } from "./robots/so100_config.js";
281
+ ```
282
+
283
+ #### SerialPort Wrapper (Node.js)
284
+
285
+ ```typescript
286
+ // packages/node/src/utils/serial-port-wrapper.ts
287
+ import { SerialPort } from "serialport";
288
+
289
+ export class NodeSerialPortWrapper {
290
+ private port: SerialPort;
291
+ private isConnected: boolean = false;
292
+
293
+ constructor(path: string, options: any = {}) {
294
+ this.port = new SerialPort({
295
+ path,
296
+ baudRate: options.baudRate || 1000000,
297
+ dataBits: options.dataBits || 8,
298
+ parity: options.parity || "none",
299
+ stopBits: options.stopBits || 1,
300
+ autoOpen: false,
301
+ });
302
+ }
303
+
304
+ async initialize(): Promise<void> {
305
+ return new Promise((resolve, reject) => {
306
+ this.port.open((err) => {
307
+ if (err) {
308
+ reject(err);
309
+ } else {
310
+ this.isConnected = true;
311
+ resolve();
312
+ }
313
+ });
314
+ });
315
+ }
316
+
317
+ async writeAndRead(data: Uint8Array): Promise<Uint8Array> {
318
+ return new Promise((resolve, reject) => {
319
+ this.port.write(Buffer.from(data), (err) => {
320
+ if (err) {
321
+ reject(err);
322
+ return;
323
+ }
324
+
325
+ // Wait for response
326
+ setTimeout(() => {
327
+ this.port.read((readErr, readData) => {
328
+ if (readErr) {
329
+ reject(readErr);
330
+ } else {
331
+ resolve(new Uint8Array(readData || []));
332
+ }
333
+ });
334
+ }, 10); // 10ms delay for response
335
+ });
336
+ });
337
+ }
338
+
339
+ async close(): Promise<void> {
340
+ return new Promise((resolve) => {
341
+ this.port.close(() => {
342
+ this.isConnected = false;
343
+ resolve();
344
+ });
345
+ });
346
+ }
347
+ }
348
+ ```
349
+
350
+ #### Find Port Implementation
351
+
352
+ ```typescript
353
+ // packages/node/src/find_port.ts - Build on existing code
354
+ import { SerialPort } from "serialport";
355
+
356
+ export interface FindPortConfig {
357
+ interactive?: boolean;
358
+ }
359
+
360
+ export interface FindPortProcess {
361
+ getAvailablePorts(): Promise<string[]>;
362
+ detectPort(): Promise<string>; // Interactive cable detection like Python
363
+ }
364
+
365
+ export async function findPort(
366
+ config: FindPortConfig = {}
367
+ ): Promise<FindPortProcess> {
368
+ const { interactive = false } = config;
369
+
370
+ return {
371
+ async getAvailablePorts(): Promise<string[]> {
372
+ // Use existing implementation from src/lerobot/node/find_port.ts
373
+ const ports = await SerialPort.list();
374
+ return ports.map((port) => port.path);
375
+ },
376
+
377
+ async detectPort(): Promise<string> {
378
+ if (interactive) {
379
+ // Existing Python-compatible implementation from src/lerobot/node/find_port.ts
380
+ // Shows "disconnect cable" prompts and detects port automatically
381
+ console.log("Finding all available ports for the MotorsBus.");
382
+
383
+ const portsBefore = await this.getAvailablePorts();
384
+ console.log(
385
+ "Remove the USB cable from your MotorsBus and press Enter when done."
386
+ );
387
+ // ... wait for user input ...
388
+
389
+ const portsAfter = await this.getAvailablePorts();
390
+ const portsDiff = portsBefore.filter(
391
+ (port) => !portsAfter.includes(port)
392
+ );
393
+
394
+ if (portsDiff.length === 1) {
395
+ return portsDiff[0];
396
+ } else {
397
+ throw new Error("Could not detect port");
398
+ }
399
+ } else {
400
+ // Programmatic mode - return first available port
401
+ const ports = await this.getAvailablePorts();
402
+ return ports[0];
403
+ }
404
+ },
405
+ };
406
+ }
407
+ ```
408
+
409
+ ### Technical Considerations
410
+
411
+ #### API Compatibility with Web Package
412
+
413
+ The Node.js package should maintain the same API surface as the web package where possible:
414
+
415
+ ```typescript
416
+ // Same function signatures
417
+ await calibrate(config); // Both packages
418
+ await teleoperate(config); // Both packages
419
+ await findPort(config); // Both packages
420
+ await releaseMotors(config); // Both packages
421
+ ```
422
+
423
+ #### Platform-Specific Adaptations
424
+
425
+ **File System Access:**
426
+
427
+ - Node.js: Direct file system access for calibration data
428
+ - Web: localStorage/IndexedDB for calibration data
429
+
430
+ **Process Management:**
431
+
432
+ - Node.js: Process signals, stdin/stdout handling
433
+ - Web: Browser events, DOM keyboard handling
434
+
435
+ **Error Handling:**
436
+
437
+ - Node.js: Process exit codes, console.error
438
+ - Web: User-friendly error dialogs
439
+
440
+ #### Python lerobot CLI Compatibility
441
+
442
+ The Node.js package must maintain exact Python lerobot CLI compatibility:
443
+
444
+ ```bash
445
+ # These commands must work identically
446
+ npx lerobot find-port
447
+ npx lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0
448
+ npx lerobot teleoperate --robot.type=so100_follower --robot.port=/dev/ttyUSB0
449
+ ```
450
+
451
+ #### Calibration Data Storage Location
452
+
453
+ The CLI should store calibration data in the same location as Python lerobot:
454
+
455
+ ```bash
456
+ # Default location (matches Python lerobot)
457
+ ~/.cache/huggingface/lerobot/calibration/robots/
458
+ ```
459
+
460
+ This ensures calibration files are compatible between Python lerobot and Node.js lerobot:
461
+
462
+ ```typescript
463
+ // Use HF_HOME environment variable like Python lerobot
464
+ const HF_HOME =
465
+ process.env.HF_HOME || path.join(os.homedir(), ".cache", "huggingface");
466
+ const CALIBRATION_DIR = path.join(HF_HOME, "lerobot", "calibration", "robots");
467
+ ```
468
+
469
+ ### Package Configuration
470
+
471
+ #### package.json
472
+
473
+ ```json
474
+ {
475
+ "name": "@lerobot/node",
476
+ "version": "0.1.0",
477
+ "description": "Node.js-based robotics control using SerialPort",
478
+ "type": "module",
479
+ "main": "./dist/index.js",
480
+ "types": "./dist/index.d.ts",
481
+ "bin": {
482
+ "lerobot": "./dist/cli.js"
483
+ },
484
+ "exports": {
485
+ ".": {
486
+ "import": "./dist/index.js",
487
+ "types": "./dist/index.d.ts"
488
+ },
489
+ "./calibrate": {
490
+ "import": "./dist/calibrate.js",
491
+ "types": "./dist/calibrate.d.ts"
492
+ },
493
+ "./teleoperate": {
494
+ "import": "./dist/teleoperate.js",
495
+ "types": "./dist/teleoperate.d.ts"
496
+ },
497
+ "./find-port": {
498
+ "import": "./dist/find_port.js",
499
+ "types": "./dist/find_port.d.ts"
500
+ }
501
+ },
502
+ "files": ["dist/**/*", "README.md"],
503
+ "keywords": [
504
+ "robotics",
505
+ "serialport",
506
+ "hardware-control",
507
+ "nodejs",
508
+ "typescript"
509
+ ],
510
+ "scripts": {
511
+ "build": "tsc --project tsconfig.build.json",
512
+ "prepublishOnly": "npm run build"
513
+ },
514
+ "dependencies": {
515
+ "serialport": "^12.0.0",
516
+ "chalk": "^5.3.0",
517
+ "commander": "^11.0.0"
518
+ },
519
+ "peerDependencies": {
520
+ "typescript": ">=4.5.0"
521
+ },
522
+ "engines": {
523
+ "node": ">=18.0.0"
524
+ }
525
+ }
526
+ ```
527
+
528
+ ## Definition of Done
529
+
530
+ - [ ] **Package Structure**: Complete `packages/node` directory with proper NPM package setup
531
+ - [ ] **API Mirror**: All four core functions (`findPort`, `calibrate`, `releaseMotors`, `teleoperate`) implemented with same API as web package
532
+ - [ ] **SerialPort Integration**: All hardware communication uses `serialport` package instead of Web Serial
533
+ - [ ] **Type Safety**: Full TypeScript coverage with strict type checking
534
+ - [ ] **Code Migration**: Existing `src/lerobot/node` code successfully migrated and enhanced
535
+ - [ ] **Cross-Platform**: Works on Windows, macOS, and Linux with Node.js 18+
536
+ - [ ] **CLI Integration**: `npx lerobot` commands work using the Node.js package
537
+ - [ ] **Python Compatibility**: CLI commands match Python lerobot behavior exactly
538
+ - [ ] **NPM Ready**: Package published as `@lerobot/node` with proper versioning
539
+ - [ ] **Documentation**: Complete README with usage examples and API documentation
540
+ - [ ] **Testing**: Comprehensive test suite covering all core functionality
541
+ - [ ] **No Regressions**: All existing Node.js functionality preserved and enhanced
examples/cyberpunk-standalone/src/components/calibration-view.tsx CHANGED
@@ -19,7 +19,7 @@ import {
19
  releaseMotors,
20
  type CalibrationProcess,
21
  type LiveCalibrationData,
22
- type WebCalibrationResults,
23
  type RobotConnection,
24
  } from "@lerobot/web";
25
  import {
@@ -42,7 +42,7 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
42
  const [calibrationProcess, setCalibrationProcess] =
43
  useState<CalibrationProcess | null>(null);
44
  const [calibrationResults, setCalibrationResults] =
45
- useState<WebCalibrationResults | null>(null);
46
  const { toast } = useToast();
47
 
48
  // Load existing calibration data from unified storage
@@ -165,7 +165,7 @@ export function CalibrationView({ robot }: CalibrationViewProps) {
165
  readCount: Object.keys(liveData || {}).length > 0 ? 100 : 0,
166
  };
167
 
168
- // Use the result directly as WebCalibrationResults
169
  saveCalibrationData(robot.serialNumber, result, metadata);
170
  }
171
 
 
19
  releaseMotors,
20
  type CalibrationProcess,
21
  type LiveCalibrationData,
22
+ type CalibrationResults,
23
  type RobotConnection,
24
  } from "@lerobot/web";
25
  import {
 
42
  const [calibrationProcess, setCalibrationProcess] =
43
  useState<CalibrationProcess | null>(null);
44
  const [calibrationResults, setCalibrationResults] =
45
+ useState<CalibrationResults | null>(null);
46
  const { toast } = useToast();
47
 
48
  // Load existing calibration data from unified storage
 
165
  readCount: Object.keys(liveData || {}).length > 0 ? 100 : 0,
166
  };
167
 
168
+ // Use the result directly as CalibrationResults
169
  saveCalibrationData(robot.serialNumber, result, metadata);
170
  }
171
 
examples/cyberpunk-standalone/src/components/docs-section.tsx CHANGED
@@ -259,8 +259,8 @@ const calibrationData = await calibrationProcess.result;`}
259
  </h5>
260
  <ul className="mt-1 ml-4 space-y-1 text-sm text-muted-foreground">
261
  <li>
262
- • <code>result: Promise&lt;WebCalibrationResults&gt;</code>{" "}
263
- - Python-compatible format
264
  </li>
265
  <li>
266
  • <code>stop(): void</code> - Stop calibration process
 
259
  </h5>
260
  <ul className="mt-1 ml-4 space-y-1 text-sm text-muted-foreground">
261
  <li>
262
+ • <code>result: Promise&lt;CalibrationResults&gt;</code> -
263
+ Python-compatible format
264
  </li>
265
  <li>
266
  • <code>stop(): void</code> - Stop calibration process
examples/cyberpunk-standalone/src/lib/unified-storage.ts CHANGED
@@ -3,7 +3,7 @@
3
  * Manages device persistence using localStorage with serial numbers as keys
4
  */
5
 
6
- import type { WebCalibrationResults } from "@lerobot/web";
7
 
8
  export interface DeviceInfo {
9
  serialNumber: string;
@@ -25,7 +25,7 @@ export interface CalibrationMetadata {
25
 
26
  export interface UnifiedRobotData {
27
  device_info: DeviceInfo;
28
- calibration?: WebCalibrationResults & {
29
  device_type?: string;
30
  device_id?: string;
31
  calibrated_at?: string;
@@ -65,7 +65,7 @@ export function saveUnifiedRobotData(
65
 
66
  export function saveCalibrationData(
67
  serialNumber: string,
68
- calibrationData: WebCalibrationResults,
69
  metadata: CalibrationMetadata
70
  ): void {
71
  try {
 
3
  * Manages device persistence using localStorage with serial numbers as keys
4
  */
5
 
6
+ import type { CalibrationResults } from "@lerobot/web";
7
 
8
  export interface DeviceInfo {
9
  serialNumber: string;
 
25
 
26
  export interface UnifiedRobotData {
27
  device_info: DeviceInfo;
28
+ calibration?: CalibrationResults & {
29
  device_type?: string;
30
  device_id?: string;
31
  calibrated_at?: string;
 
65
 
66
  export function saveCalibrationData(
67
  serialNumber: string,
68
+ calibrationData: CalibrationResults,
69
  metadata: CalibrationMetadata
70
  ): void {
71
  try {
examples/cyberpunk-standalone/src/types/robot.ts CHANGED
@@ -1,6 +1,6 @@
1
  export type {
2
  RobotConnection,
3
  LiveCalibrationData,
4
- WebCalibrationResults,
5
  TeleoperationState,
6
  } from "@lerobot/web";
 
1
  export type {
2
  RobotConnection,
3
  LiveCalibrationData,
4
+ CalibrationResults,
5
  TeleoperationState,
6
  } from "@lerobot/web";
examples/node-quick-start/.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build outputs
5
+ dist/
6
+ *.tsbuildinfo
7
+
8
+ # Environment files
9
+ .env
10
+ .env.local
11
+
12
+ # IDE files
13
+ .vscode/
14
+ .idea/
15
+ *.swp
16
+ *.swo
17
+
18
+ # OS files
19
+ .DS_Store
20
+ Thumbs.db
21
+
22
+ # Logs
23
+ *.log
24
+ npm-debug.log*
25
+
26
+ # Runtime data
27
+ pids
28
+ *.pid
29
+ *.seed
30
+ *.pid.lock
examples/node-quick-start/README.md ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node.js Quick Start Example (Vite)
2
+
3
+ This example demonstrates the complete workflow of using `@lerobot/node` to control robotics hardware from Node.js applications, using Vite for fast development and building. Follows the same pattern as the web Quick Start guide.
4
+
5
+ ## What This Example Does
6
+
7
+ 1. **Find Port**: Discovers and connects to your robot hardware
8
+ 2. **Release Motors**: Puts motors in free-movement mode for setup
9
+ 3. **Calibrate**: Records motor ranges and sets homing positions
10
+ 4. **Teleoperate**: Enables keyboard control of the robot
11
+
12
+ ## Hardware Requirements
13
+
14
+ - SO-100 robotic arm with STS3215 servos
15
+ - USB connection to your computer
16
+ - Compatible with Windows (COM ports), macOS, and Linux (/dev/tty\* ports)
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ # Install dependencies (from project root)
22
+ pnpm install
23
+ ```
24
+
25
+ ## Running the Examples
26
+
27
+ ### Full Workflow Demo
28
+
29
+ ```bash
30
+ # Complete robot setup workflow (interactive)
31
+ pnpm demo:full-workflow
32
+ ```
33
+
34
+ ### Individual Component Demos
35
+
36
+ ```bash
37
+ # Test port discovery
38
+ pnpm demo:find-port
39
+
40
+ # Test calibration (requires connected robot)
41
+ pnpm demo:calibrate
42
+
43
+ # Test keyboard control (requires calibrated robot)
44
+ pnpm demo:teleoperate
45
+ ```
46
+
47
+ ## Example Usage
48
+
49
+ The main demo (`src/main.ts`) shows the complete workflow:
50
+
51
+ ```typescript
52
+ import { findPort, connectPort, releaseMotors, calibrate, teleoperate } from "@lerobot/node";
53
+
54
+ // 1. Find available robots
55
+ const findProcess = await findPort();
56
+ const robots = await findProcess.result;
57
+
58
+ // 2. Connect to first robot found
59
+ const robot = await connectPort(robots[0].path, "so100_follower", "my_robot_arm");
60
+
61
+ // 3. Release motors for manual positioning
62
+ await releaseMotors(robot);
63
+
64
+ // 4. Calibrate motor ranges
65
+ const calibrationProcess = await calibrate({
66
+ robot,
67
+ onProgress: (message) => console.log(message),
68
+ });
69
+
70
+ // 5. Control robot with keyboard
71
+ const teleop = await teleoperate({
72
+ robot,
73
+ teleop: { type: "keyboard" },
74
+ });
75
+ ```
76
+
77
+ ## CLI Commands
78
+
79
+ You can also use the CLI directly:
80
+
81
+ ```bash
82
+ # Find available ports
83
+ npx lerobot find-port
84
+
85
+ # Calibrate robot
86
+ npx lerobot calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot
87
+
88
+ # Control robot
89
+ npx lerobot teleoperate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot
90
+ ```
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ # Run with Vite Node (faster development with hot reload)
96
+ pnpm dev
97
+
98
+ # Build with Vite and run compiled version
99
+ pnpm build && pnpm start
100
+ ```
101
+
102
+ ## Safety Notes
103
+
104
+ ⚠️ **Important Safety Guidelines:**
105
+
106
+ - Always ensure robot is in a safe position before running examples
107
+ - Keep emergency stop accessible (ESC key during teleoperation)
108
+ - Start with small movements to test calibration
109
+ - Ensure robot has adequate workspace clearance
110
+
111
+ ## Troubleshooting
112
+
113
+ **Port Not Found:**
114
+
115
+ - Check USB connection
116
+ - Verify robot is powered on
117
+ - Try different USB ports/cables
118
+ - On Linux: Check user permissions for serial ports
119
+
120
+ **Calibration Issues:**
121
+
122
+ - Ensure motors are released and can move freely
123
+ - Move each joint through its full range slowly
124
+ - Avoid forcing motors past mechanical limits
125
+
126
+ **Control Problems:**
127
+
128
+ - Verify calibration completed successfully
129
+ - Check that calibration file was saved
130
+ - Restart the teleoperation if motors don't respond
examples/node-quick-start/package.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "node-quick-start",
3
+ "version": "0.1.0",
4
+ "description": "Node.js Quick Start example for @lerobot/node package",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite-node src/main.ts",
8
+ "build": "vite build",
9
+ "start": "node dist/main.js",
10
+ "demo:find-port": "vite-node src/demo-find-port.ts",
11
+ "demo:calibrate": "vite-node src/demo-calibrate.ts",
12
+ "demo:teleoperate": "vite-node src/demo-teleoperate.ts",
13
+ "demo:full-workflow": "vite-node src/main.ts"
14
+ },
15
+ "dependencies": {
16
+ "@lerobot/node": "file:../../packages/node"
17
+ },
18
+ "devDependencies": {
19
+ "vite": "^6.3.5",
20
+ "vite-node": "^2.0.0",
21
+ "typescript": "^5.3.0",
22
+ "@types/node": "^18.0.0"
23
+ },
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "keywords": [
28
+ "robotics",
29
+ "lerobot",
30
+ "nodejs",
31
+ "example",
32
+ "demo"
33
+ ],
34
+ "author": "LeRobot.js Team",
35
+ "license": "Apache-2.0"
36
+ }
examples/node-quick-start/pnpm-lock.yaml ADDED
@@ -0,0 +1,1174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ '@lerobot/node':
12
+ specifier: file:../../packages/node
13
+ version: file:../../packages/node([email protected])
14
+ devDependencies:
15
+ '@types/node':
16
+ specifier: ^18.0.0
17
+ version: 18.19.120
18
+ typescript:
19
+ specifier: ^5.3.0
20
+ version: 5.8.3
21
+ vite:
22
+ specifier: ^6.3.5
23
+ version: 6.3.5(@types/[email protected])([email protected])
24
+ vite-node:
25
+ specifier: ^2.0.0
26
+ version: 2.1.9(@types/[email protected])
27
+
28
+ packages:
29
+
30
+ '@esbuild/[email protected]':
31
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
32
+ engines: {node: '>=12'}
33
+ cpu: [ppc64]
34
+ os: [aix]
35
+
36
+ '@esbuild/[email protected]':
37
+ resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
38
+ engines: {node: '>=18'}
39
+ cpu: [ppc64]
40
+ os: [aix]
41
+
42
+ '@esbuild/[email protected]':
43
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
44
+ engines: {node: '>=12'}
45
+ cpu: [arm64]
46
+ os: [android]
47
+
48
+ '@esbuild/[email protected]':
49
+ resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==}
50
+ engines: {node: '>=18'}
51
+ cpu: [arm64]
52
+ os: [android]
53
+
54
+ '@esbuild/[email protected]':
55
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
56
+ engines: {node: '>=12'}
57
+ cpu: [arm]
58
+ os: [android]
59
+
60
+ '@esbuild/[email protected]':
61
+ resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==}
62
+ engines: {node: '>=18'}
63
+ cpu: [arm]
64
+ os: [android]
65
+
66
+ '@esbuild/[email protected]':
67
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
68
+ engines: {node: '>=12'}
69
+ cpu: [x64]
70
+ os: [android]
71
+
72
+ '@esbuild/[email protected]':
73
+ resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==}
74
+ engines: {node: '>=18'}
75
+ cpu: [x64]
76
+ os: [android]
77
+
78
+ '@esbuild/[email protected]':
79
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
80
+ engines: {node: '>=12'}
81
+ cpu: [arm64]
82
+ os: [darwin]
83
+
84
+ '@esbuild/[email protected]':
85
+ resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==}
86
+ engines: {node: '>=18'}
87
+ cpu: [arm64]
88
+ os: [darwin]
89
+
90
+ '@esbuild/[email protected]':
91
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
92
+ engines: {node: '>=12'}
93
+ cpu: [x64]
94
+ os: [darwin]
95
+
96
+ '@esbuild/[email protected]':
97
+ resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==}
98
+ engines: {node: '>=18'}
99
+ cpu: [x64]
100
+ os: [darwin]
101
+
102
+ '@esbuild/[email protected]':
103
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
104
+ engines: {node: '>=12'}
105
+ cpu: [arm64]
106
+ os: [freebsd]
107
+
108
+ '@esbuild/[email protected]':
109
+ resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==}
110
+ engines: {node: '>=18'}
111
+ cpu: [arm64]
112
+ os: [freebsd]
113
+
114
+ '@esbuild/[email protected]':
115
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
116
+ engines: {node: '>=12'}
117
+ cpu: [x64]
118
+ os: [freebsd]
119
+
120
+ '@esbuild/[email protected]':
121
+ resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==}
122
+ engines: {node: '>=18'}
123
+ cpu: [x64]
124
+ os: [freebsd]
125
+
126
+ '@esbuild/[email protected]':
127
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
128
+ engines: {node: '>=12'}
129
+ cpu: [arm64]
130
+ os: [linux]
131
+
132
+ '@esbuild/[email protected]':
133
+ resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==}
134
+ engines: {node: '>=18'}
135
+ cpu: [arm64]
136
+ os: [linux]
137
+
138
+ '@esbuild/[email protected]':
139
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
140
+ engines: {node: '>=12'}
141
+ cpu: [arm]
142
+ os: [linux]
143
+
144
+ '@esbuild/[email protected]':
145
+ resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==}
146
+ engines: {node: '>=18'}
147
+ cpu: [arm]
148
+ os: [linux]
149
+
150
+ '@esbuild/[email protected]':
151
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
152
+ engines: {node: '>=12'}
153
+ cpu: [ia32]
154
+ os: [linux]
155
+
156
+ '@esbuild/[email protected]':
157
+ resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==}
158
+ engines: {node: '>=18'}
159
+ cpu: [ia32]
160
+ os: [linux]
161
+
162
+ '@esbuild/[email protected]':
163
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
164
+ engines: {node: '>=12'}
165
+ cpu: [loong64]
166
+ os: [linux]
167
+
168
+ '@esbuild/[email protected]':
169
+ resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==}
170
+ engines: {node: '>=18'}
171
+ cpu: [loong64]
172
+ os: [linux]
173
+
174
+ '@esbuild/[email protected]':
175
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
176
+ engines: {node: '>=12'}
177
+ cpu: [mips64el]
178
+ os: [linux]
179
+
180
+ '@esbuild/[email protected]':
181
+ resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==}
182
+ engines: {node: '>=18'}
183
+ cpu: [mips64el]
184
+ os: [linux]
185
+
186
+ '@esbuild/[email protected]':
187
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
188
+ engines: {node: '>=12'}
189
+ cpu: [ppc64]
190
+ os: [linux]
191
+
192
+ '@esbuild/[email protected]':
193
+ resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==}
194
+ engines: {node: '>=18'}
195
+ cpu: [ppc64]
196
+ os: [linux]
197
+
198
+ '@esbuild/[email protected]':
199
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
200
+ engines: {node: '>=12'}
201
+ cpu: [riscv64]
202
+ os: [linux]
203
+
204
+ '@esbuild/[email protected]':
205
+ resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==}
206
+ engines: {node: '>=18'}
207
+ cpu: [riscv64]
208
+ os: [linux]
209
+
210
+ '@esbuild/[email protected]':
211
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
212
+ engines: {node: '>=12'}
213
+ cpu: [s390x]
214
+ os: [linux]
215
+
216
+ '@esbuild/[email protected]':
217
+ resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==}
218
+ engines: {node: '>=18'}
219
+ cpu: [s390x]
220
+ os: [linux]
221
+
222
+ '@esbuild/[email protected]':
223
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
224
+ engines: {node: '>=12'}
225
+ cpu: [x64]
226
+ os: [linux]
227
+
228
+ '@esbuild/[email protected]':
229
+ resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==}
230
+ engines: {node: '>=18'}
231
+ cpu: [x64]
232
+ os: [linux]
233
+
234
+ '@esbuild/[email protected]':
235
+ resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==}
236
+ engines: {node: '>=18'}
237
+ cpu: [arm64]
238
+ os: [netbsd]
239
+
240
+ '@esbuild/[email protected]':
241
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
242
+ engines: {node: '>=12'}
243
+ cpu: [x64]
244
+ os: [netbsd]
245
+
246
+ '@esbuild/[email protected]':
247
+ resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==}
248
+ engines: {node: '>=18'}
249
+ cpu: [x64]
250
+ os: [netbsd]
251
+
252
+ '@esbuild/[email protected]':
253
+ resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==}
254
+ engines: {node: '>=18'}
255
+ cpu: [arm64]
256
+ os: [openbsd]
257
+
258
+ '@esbuild/[email protected]':
259
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
260
+ engines: {node: '>=12'}
261
+ cpu: [x64]
262
+ os: [openbsd]
263
+
264
+ '@esbuild/[email protected]':
265
+ resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==}
266
+ engines: {node: '>=18'}
267
+ cpu: [x64]
268
+ os: [openbsd]
269
+
270
+ '@esbuild/[email protected]':
271
+ resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==}
272
+ engines: {node: '>=18'}
273
+ cpu: [arm64]
274
+ os: [openharmony]
275
+
276
+ '@esbuild/[email protected]':
277
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
278
+ engines: {node: '>=12'}
279
+ cpu: [x64]
280
+ os: [sunos]
281
+
282
+ '@esbuild/[email protected]':
283
+ resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==}
284
+ engines: {node: '>=18'}
285
+ cpu: [x64]
286
+ os: [sunos]
287
+
288
+ '@esbuild/[email protected]':
289
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
290
+ engines: {node: '>=12'}
291
+ cpu: [arm64]
292
+ os: [win32]
293
+
294
+ '@esbuild/[email protected]':
295
+ resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==}
296
+ engines: {node: '>=18'}
297
+ cpu: [arm64]
298
+ os: [win32]
299
+
300
+ '@esbuild/[email protected]':
301
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
302
+ engines: {node: '>=12'}
303
+ cpu: [ia32]
304
+ os: [win32]
305
+
306
+ '@esbuild/[email protected]':
307
+ resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==}
308
+ engines: {node: '>=18'}
309
+ cpu: [ia32]
310
+ os: [win32]
311
+
312
+ '@esbuild/[email protected]':
313
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
314
+ engines: {node: '>=12'}
315
+ cpu: [x64]
316
+ os: [win32]
317
+
318
+ '@esbuild/[email protected]':
319
+ resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==}
320
+ engines: {node: '>=18'}
321
+ cpu: [x64]
322
+ os: [win32]
323
+
324
+ '@lerobot/node@file:../../packages/node':
325
+ resolution: {directory: ../../packages/node, type: directory}
326
+ engines: {node: '>=18.0.0'}
327
+ peerDependencies:
328
+ typescript: '>=4.5.0'
329
+
330
+ '@rollup/[email protected]':
331
+ resolution: {integrity: sha512-9f3nSTFI2ivfxc7/tHBHcJ8pRnp8ROrELvsVprlQPVvcZ+j5zztYd+PTJGpyIOAdTvNwNrpCXswKSeoQcyGjMQ==}
332
+ cpu: [arm]
333
+ os: [android]
334
+
335
+ '@rollup/[email protected]':
336
+ resolution: {integrity: sha512-tFZSEhqJ8Yrpe50TzOdeoYi72gi/jsnT7y8Qrozf3cNu28WX+s6I3XzEPUAqoaT9SAS8Xz9AzGTFlxxCH/w20w==}
337
+ cpu: [arm64]
338
+ os: [android]
339
+
340
+ '@rollup/[email protected]':
341
+ resolution: {integrity: sha512-+DikIIs+p6yU2hF51UaWG8BnHbq90X0QIOt5zqSKSZxY+G3qqdLih214e9InJal21af2PuuxkDectetGfbVPJw==}
342
+ cpu: [arm64]
343
+ os: [darwin]
344
+
345
+ '@rollup/[email protected]':
346
+ resolution: {integrity: sha512-5a+NofhdEB/WimSlFMskbFQn1vqz1FWryYpA99trmZGO6qEmiS0IsX6w4B3d91U878Q2ZQdiaFF1gxX4P147og==}
347
+ cpu: [x64]
348
+ os: [darwin]
349
+
350
+ '@rollup/[email protected]':
351
+ resolution: {integrity: sha512-igr/RlKPS3OCy4jD3XBmAmo3UAcNZkJSubRsw1JeM8bAbwf15k/3eMZXD91bnjheijJiOJcga3kfCLKjV8IXNg==}
352
+ cpu: [arm64]
353
+ os: [freebsd]
354
+
355
+ '@rollup/[email protected]':
356
+ resolution: {integrity: sha512-MdigWzPSHlQzB1xZ+MdFDWTAH+kcn7UxjEBoOKuaso7z1DRlnAnrknB1mTtNOQ+GdPI8xgExAGwHeqQjntR0Cg==}
357
+ cpu: [x64]
358
+ os: [freebsd]
359
+
360
+ '@rollup/[email protected]':
361
+ resolution: {integrity: sha512-dmZseE0ZwA/4yy1+BwFrDqFTjjNg24GO9xSrb1weVbt6AFkhp5pz1gVS7IMtfIvoWy8yp6q/zN0bKnefRUImvQ==}
362
+ cpu: [arm]
363
+ os: [linux]
364
+
365
+ '@rollup/[email protected]':
366
+ resolution: {integrity: sha512-fzhfn6p9Cfm3W8UrWKIa4l7Wfjs/KGdgaswMBBE3KY3Ta43jg2XsPrAtfezHpsRk0Nx+TFuS3hZk/To2N5kFPQ==}
367
+ cpu: [arm]
368
+ os: [linux]
369
+
370
+ '@rollup/[email protected]':
371
+ resolution: {integrity: sha512-vVDD+iPDPmJQ5nAQ5Tifq3ywdv60FartglFI8VOCK+hcU9aoG0qlQTsDJP97O5yiTaTqlneZWoARMcVC5nyUoQ==}
372
+ cpu: [arm64]
373
+ os: [linux]
374
+
375
+ '@rollup/[email protected]':
376
+ resolution: {integrity: sha512-0d0jx08fzDHCzXqrtCMEEyxKU0SvJrWmUjUDE2/KDQ2UDJql0tfiwYvEx1oHELClKO8CNdE+AGJj+RqXscZpdQ==}
377
+ cpu: [arm64]
378
+ os: [linux]
379
+
380
+ '@rollup/[email protected]':
381
+ resolution: {integrity: sha512-XBYu9oW9eKJadWn8M7hkTZsD4yG+RrsTrVEgyKwb4L72cpJjRbRboTG9Lg9fec8MxJp/cfTHAocg4mnismQR8A==}
382
+ cpu: [loong64]
383
+ os: [linux]
384
+
385
+ '@rollup/[email protected]':
386
+ resolution: {integrity: sha512-wJaRvcT17PoOK6Ggcfo3nouFlybHvARBS4jzT0PC/lg17fIJHcDS2fZz3sD+iA4nRlho2zE6OGbU0HvwATdokQ==}
387
+ cpu: [ppc64]
388
+ os: [linux]
389
+
390
+ '@rollup/[email protected]':
391
+ resolution: {integrity: sha512-GZ5bkMFteAGkcmh8x0Ok4LSa+L62Ez0tMsHPX6JtR0wl4Xc3bQcrFHDiR5DGLEDFtGrXih4Nd/UDaFqs968/wA==}
392
+ cpu: [riscv64]
393
+ os: [linux]
394
+
395
+ '@rollup/[email protected]':
396
+ resolution: {integrity: sha512-7CjPw6FflFsVOUfWOrVrREiV3IYXG4RzZ1ZQUaT3BtSK8YXN6x286o+sruPZJESIaPebYuFowmg54ZdrkVBYog==}
397
+ cpu: [riscv64]
398
+ os: [linux]
399
+
400
+ '@rollup/[email protected]':
401
+ resolution: {integrity: sha512-nmvnl0ZiuysltcB/cKjUh40Rx4FbSyueERDsl2FLvLYr6pCgSsvGr3SocUT84svSpmloS7f1DRWqtRha74Gi1w==}
402
+ cpu: [s390x]
403
+ os: [linux]
404
+
405
+ '@rollup/[email protected]':
406
+ resolution: {integrity: sha512-Cv+moII5C8RM6gZbR3cb21o6rquVDZrN2o81maROg1LFzBz2dZUwIQSxFA8GtGZ/F2KtsqQ2z3eFPBb6akvQNg==}
407
+ cpu: [x64]
408
+ os: [linux]
409
+
410
+ '@rollup/[email protected]':
411
+ resolution: {integrity: sha512-PHcMG8DZTM9RCIjp8QIfN0VYtX0TtBPnWOTRurFhoCDoi9zptUZL2k7pCs+5rgut7JAiUsYy+huyhVKPcmxoog==}
412
+ cpu: [x64]
413
+ os: [linux]
414
+
415
+ '@rollup/[email protected]':
416
+ resolution: {integrity: sha512-1SI/Rd47e8aQJeFWMDg16ET+fjvCcD/CzeaRmIEPmb05hx+3cCcwIF4ebUag4yTt/D1peE+Mgp0+Po3M358cAA==}
417
+ cpu: [arm64]
418
+ os: [win32]
419
+
420
+ '@rollup/[email protected]':
421
+ resolution: {integrity: sha512-JwOCYxmumFDfDhx4kNyz6kTVK3gWzBIvVdMNzQMRDubcoGRDniOOmo6DDNP42qwZx3Bp9/6vWJ+kNzNqXoHmeA==}
422
+ cpu: [ia32]
423
+ os: [win32]
424
+
425
+ '@rollup/[email protected]':
426
+ resolution: {integrity: sha512-IPMIfrfkG1GaEXi+JSsQEx8x9b4b+hRZXO7KYc2pKio3zO2/VDXDs6B9Ts/nnO+25Fk1tdAVtUn60HKKPPzDig==}
427
+ cpu: [x64]
428
+ os: [win32]
429
+
430
+ '@serialport/[email protected]':
431
+ resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
432
+ engines: {node: '>=12.0.0'}
433
+
434
+ '@serialport/[email protected]':
435
+ resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
436
+ engines: {node: '>=16.0.0'}
437
+
438
+ '@serialport/[email protected]':
439
+ resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
440
+ engines: {node: ^12.22 || ^14.13 || >=16}
441
+
442
+ '@serialport/[email protected]':
443
+ resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
444
+ engines: {node: '>=12.0.0'}
445
+
446
+ '@serialport/[email protected]':
447
+ resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
448
+ engines: {node: '>=12.0.0'}
449
+
450
+ '@serialport/[email protected]':
451
+ resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
452
+ engines: {node: '>=12.0.0'}
453
+
454
+ '@serialport/[email protected]':
455
+ resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
456
+ engines: {node: '>=12.0.0'}
457
+
458
+ '@serialport/[email protected]':
459
+ resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
460
+ engines: {node: '>=12.0.0'}
461
+
462
+ '@serialport/[email protected]':
463
+ resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
464
+ engines: {node: '>=8.6.0'}
465
+
466
+ '@serialport/[email protected]':
467
+ resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
468
+ engines: {node: '>=12.0.0'}
469
+
470
+ '@serialport/[email protected]':
471
+ resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
472
+ engines: {node: '>=12.0.0'}
473
+
474
+ '@serialport/[email protected]':
475
+ resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
476
+ engines: {node: '>=12.0.0'}
477
+
478
+ '@serialport/[email protected]':
479
+ resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
480
+ engines: {node: '>=12.0.0'}
481
+
482
+ '@serialport/[email protected]':
483
+ resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
484
+ engines: {node: '>=12.0.0'}
485
+
486
+ '@serialport/[email protected]':
487
+ resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
488
+ engines: {node: '>=12.0.0'}
489
+
490
+ '@serialport/[email protected]':
491
+ resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
492
+ engines: {node: '>=12.0.0'}
493
+
494
+ '@types/[email protected]':
495
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
496
+
497
+ '@types/[email protected]':
498
+ resolution: {integrity: sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==}
499
+
500
501
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
502
+ engines: {node: '>=8'}
503
+
504
505
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
506
+ engines: {node: '>=6.0'}
507
+ peerDependencies:
508
+ supports-color: '*'
509
+ peerDependenciesMeta:
510
+ supports-color:
511
+ optional: true
512
+
513
514
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
515
+ engines: {node: '>=6.0'}
516
+ peerDependencies:
517
+ supports-color: '*'
518
+ peerDependenciesMeta:
519
+ supports-color:
520
+ optional: true
521
+
522
523
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
524
+
525
526
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
527
+ engines: {node: '>=12'}
528
+ hasBin: true
529
+
530
531
+ resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
532
+ engines: {node: '>=18'}
533
+ hasBin: true
534
+
535
536
+ resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
537
+ peerDependencies:
538
+ picomatch: ^3 || ^4
539
+ peerDependenciesMeta:
540
+ picomatch:
541
+ optional: true
542
+
543
544
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
545
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
546
+ os: [darwin]
547
+
548
549
+ resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
550
+
551
552
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
553
+
554
555
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
556
+
557
558
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
559
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
560
+ hasBin: true
561
+
562
563
+ resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
564
+
565
566
+ resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
567
+ hasBin: true
568
+
569
570
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
571
+
572
573
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
574
+
575
576
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
577
+ engines: {node: '>=12'}
578
+
579
580
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
581
+ engines: {node: ^10 || ^12 || >=14}
582
+
583
584
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
585
+
586
587
+ resolution: {integrity: sha512-ONmkT3Ud3IfW15nl7l4qAZko5/2iZ5ALVBDh02ZSZ5IGVLJSYkRcRa3iB58VyEIyoofs9m2xdVrm+lTi97+3pw==}
588
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
589
+ hasBin: true
590
+
591
592
+ resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
593
+ engines: {node: '>=16.0.0'}
594
+
595
596
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
597
+ engines: {node: '>=0.10.0'}
598
+
599
600
+ resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
601
+ engines: {node: '>=12.0.0'}
602
+
603
604
+ resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==}
605
+ engines: {node: '>=18.0.0'}
606
+ hasBin: true
607
+
608
609
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
610
+ engines: {node: '>=14.17'}
611
+ hasBin: true
612
+
613
614
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
615
+
616
617
+ resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
618
+ engines: {node: ^18.0.0 || >=20.0.0}
619
+ hasBin: true
620
+
621
622
+ resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
623
+ engines: {node: ^18.0.0 || >=20.0.0}
624
+ hasBin: true
625
+ peerDependencies:
626
+ '@types/node': ^18.0.0 || >=20.0.0
627
+ less: '*'
628
+ lightningcss: ^1.21.0
629
+ sass: '*'
630
+ sass-embedded: '*'
631
+ stylus: '*'
632
+ sugarss: '*'
633
+ terser: ^5.4.0
634
+ peerDependenciesMeta:
635
+ '@types/node':
636
+ optional: true
637
+ less:
638
+ optional: true
639
+ lightningcss:
640
+ optional: true
641
+ sass:
642
+ optional: true
643
+ sass-embedded:
644
+ optional: true
645
+ stylus:
646
+ optional: true
647
+ sugarss:
648
+ optional: true
649
+ terser:
650
+ optional: true
651
+
652
653
+ resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
654
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
655
+ hasBin: true
656
+ peerDependencies:
657
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
658
+ jiti: '>=1.21.0'
659
+ less: '*'
660
+ lightningcss: ^1.21.0
661
+ sass: '*'
662
+ sass-embedded: '*'
663
+ stylus: '*'
664
+ sugarss: '*'
665
+ terser: ^5.16.0
666
+ tsx: ^4.8.1
667
+ yaml: ^2.4.2
668
+ peerDependenciesMeta:
669
+ '@types/node':
670
+ optional: true
671
+ jiti:
672
+ optional: true
673
+ less:
674
+ optional: true
675
+ lightningcss:
676
+ optional: true
677
+ sass:
678
+ optional: true
679
+ sass-embedded:
680
+ optional: true
681
+ stylus:
682
+ optional: true
683
+ sugarss:
684
+ optional: true
685
+ terser:
686
+ optional: true
687
+ tsx:
688
+ optional: true
689
+ yaml:
690
+ optional: true
691
+
692
+ snapshots:
693
+
694
+ '@esbuild/[email protected]':
695
+ optional: true
696
+
697
+ '@esbuild/[email protected]':
698
+ optional: true
699
+
700
+ '@esbuild/[email protected]':
701
+ optional: true
702
+
703
+ '@esbuild/[email protected]':
704
+ optional: true
705
+
706
+ '@esbuild/[email protected]':
707
+ optional: true
708
+
709
+ '@esbuild/[email protected]':
710
+ optional: true
711
+
712
+ '@esbuild/[email protected]':
713
+ optional: true
714
+
715
+ '@esbuild/[email protected]':
716
+ optional: true
717
+
718
+ '@esbuild/[email protected]':
719
+ optional: true
720
+
721
+ '@esbuild/[email protected]':
722
+ optional: true
723
+
724
+ '@esbuild/[email protected]':
725
+ optional: true
726
+
727
+ '@esbuild/[email protected]':
728
+ optional: true
729
+
730
+ '@esbuild/[email protected]':
731
+ optional: true
732
+
733
+ '@esbuild/[email protected]':
734
+ optional: true
735
+
736
+ '@esbuild/[email protected]':
737
+ optional: true
738
+
739
+ '@esbuild/[email protected]':
740
+ optional: true
741
+
742
+ '@esbuild/[email protected]':
743
+ optional: true
744
+
745
+ '@esbuild/[email protected]':
746
+ optional: true
747
+
748
+ '@esbuild/[email protected]':
749
+ optional: true
750
+
751
+ '@esbuild/[email protected]':
752
+ optional: true
753
+
754
+ '@esbuild/[email protected]':
755
+ optional: true
756
+
757
+ '@esbuild/[email protected]':
758
+ optional: true
759
+
760
+ '@esbuild/[email protected]':
761
+ optional: true
762
+
763
+ '@esbuild/[email protected]':
764
+ optional: true
765
+
766
+ '@esbuild/[email protected]':
767
+ optional: true
768
+
769
+ '@esbuild/[email protected]':
770
+ optional: true
771
+
772
+ '@esbuild/[email protected]':
773
+ optional: true
774
+
775
+ '@esbuild/[email protected]':
776
+ optional: true
777
+
778
+ '@esbuild/[email protected]':
779
+ optional: true
780
+
781
+ '@esbuild/[email protected]':
782
+ optional: true
783
+
784
+ '@esbuild/[email protected]':
785
+ optional: true
786
+
787
+ '@esbuild/[email protected]':
788
+ optional: true
789
+
790
+ '@esbuild/[email protected]':
791
+ optional: true
792
+
793
+ '@esbuild/[email protected]':
794
+ optional: true
795
+
796
+ '@esbuild/[email protected]':
797
+ optional: true
798
+
799
+ '@esbuild/[email protected]':
800
+ optional: true
801
+
802
+ '@esbuild/[email protected]':
803
+ optional: true
804
+
805
+ '@esbuild/[email protected]':
806
+ optional: true
807
+
808
+ '@esbuild/[email protected]':
809
+ optional: true
810
+
811
+ '@esbuild/[email protected]':
812
+ optional: true
813
+
814
+ '@esbuild/[email protected]':
815
+ optional: true
816
+
817
+ '@esbuild/[email protected]':
818
+ optional: true
819
+
820
+ '@esbuild/[email protected]':
821
+ optional: true
822
+
823
+ '@esbuild/[email protected]':
824
+ optional: true
825
+
826
+ '@esbuild/[email protected]':
827
+ optional: true
828
+
829
+ '@esbuild/[email protected]':
830
+ optional: true
831
+
832
+ '@esbuild/[email protected]':
833
+ optional: true
834
+
835
+ '@esbuild/[email protected]':
836
+ optional: true
837
+
838
+ '@esbuild/[email protected]':
839
+ optional: true
840
+
841
+ '@lerobot/node@file:../../packages/node([email protected])':
842
+ dependencies:
843
+ serialport: 12.0.0
844
+ typescript: 5.8.3
845
+ transitivePeerDependencies:
846
+ - supports-color
847
+
848
+ '@rollup/[email protected]':
849
+ optional: true
850
+
851
+ '@rollup/[email protected]':
852
+ optional: true
853
+
854
+ '@rollup/[email protected]':
855
+ optional: true
856
+
857
+ '@rollup/[email protected]':
858
+ optional: true
859
+
860
+ '@rollup/[email protected]':
861
+ optional: true
862
+
863
+ '@rollup/[email protected]':
864
+ optional: true
865
+
866
+ '@rollup/[email protected]':
867
+ optional: true
868
+
869
+ '@rollup/[email protected]':
870
+ optional: true
871
+
872
+ '@rollup/[email protected]':
873
+ optional: true
874
+
875
+ '@rollup/[email protected]':
876
+ optional: true
877
+
878
+ '@rollup/[email protected]':
879
+ optional: true
880
+
881
+ '@rollup/[email protected]':
882
+ optional: true
883
+
884
+ '@rollup/[email protected]':
885
+ optional: true
886
+
887
+ '@rollup/[email protected]':
888
+ optional: true
889
+
890
+ '@rollup/[email protected]':
891
+ optional: true
892
+
893
+ '@rollup/[email protected]':
894
+ optional: true
895
+
896
+ '@rollup/[email protected]':
897
+ optional: true
898
+
899
+ '@rollup/[email protected]':
900
+ optional: true
901
+
902
+ '@rollup/[email protected]':
903
+ optional: true
904
+
905
+ '@rollup/[email protected]':
906
+ optional: true
907
+
908
+ '@serialport/[email protected]':
909
+ dependencies:
910
+ '@serialport/bindings-interface': 1.2.2
911
+ debug: 4.3.4
912
+ transitivePeerDependencies:
913
+ - supports-color
914
+
915
+ '@serialport/[email protected]':
916
+ dependencies:
917
+ '@serialport/bindings-interface': 1.2.2
918
+ '@serialport/parser-readline': 11.0.0
919
+ debug: 4.3.4
920
+ node-addon-api: 7.0.0
921
+ node-gyp-build: 4.6.0
922
+ transitivePeerDependencies:
923
+ - supports-color
924
+
925
+ '@serialport/[email protected]': {}
926
+
927
+ '@serialport/[email protected]': {}
928
+
929
+ '@serialport/[email protected]': {}
930
+
931
+ '@serialport/[email protected]': {}
932
+
933
+ '@serialport/[email protected]': {}
934
+
935
+ '@serialport/[email protected]': {}
936
+
937
+ '@serialport/[email protected]': {}
938
+
939
+ '@serialport/[email protected]':
940
+ dependencies:
941
+ '@serialport/parser-delimiter': 11.0.0
942
+
943
+ '@serialport/[email protected]':
944
+ dependencies:
945
+ '@serialport/parser-delimiter': 12.0.0
946
+
947
+ '@serialport/[email protected]': {}
948
+
949
+ '@serialport/[email protected]': {}
950
+
951
+ '@serialport/[email protected]': {}
952
+
953
+ '@serialport/[email protected]': {}
954
+
955
+ '@serialport/[email protected]':
956
+ dependencies:
957
+ '@serialport/bindings-interface': 1.2.2
958
+ debug: 4.3.4
959
+ transitivePeerDependencies:
960
+ - supports-color
961
+
962
+ '@types/[email protected]': {}
963
+
964
+ '@types/[email protected]':
965
+ dependencies:
966
+ undici-types: 5.26.5
967
+
968
969
+
970
971
+ dependencies:
972
+ ms: 2.1.2
973
+
974
975
+ dependencies:
976
+ ms: 2.1.3
977
+
978
979
+
980
981
+ optionalDependencies:
982
+ '@esbuild/aix-ppc64': 0.21.5
983
+ '@esbuild/android-arm': 0.21.5
984
+ '@esbuild/android-arm64': 0.21.5
985
+ '@esbuild/android-x64': 0.21.5
986
+ '@esbuild/darwin-arm64': 0.21.5
987
+ '@esbuild/darwin-x64': 0.21.5
988
+ '@esbuild/freebsd-arm64': 0.21.5
989
+ '@esbuild/freebsd-x64': 0.21.5
990
+ '@esbuild/linux-arm': 0.21.5
991
+ '@esbuild/linux-arm64': 0.21.5
992
+ '@esbuild/linux-ia32': 0.21.5
993
+ '@esbuild/linux-loong64': 0.21.5
994
+ '@esbuild/linux-mips64el': 0.21.5
995
+ '@esbuild/linux-ppc64': 0.21.5
996
+ '@esbuild/linux-riscv64': 0.21.5
997
+ '@esbuild/linux-s390x': 0.21.5
998
+ '@esbuild/linux-x64': 0.21.5
999
+ '@esbuild/netbsd-x64': 0.21.5
1000
+ '@esbuild/openbsd-x64': 0.21.5
1001
+ '@esbuild/sunos-x64': 0.21.5
1002
+ '@esbuild/win32-arm64': 0.21.5
1003
+ '@esbuild/win32-ia32': 0.21.5
1004
+ '@esbuild/win32-x64': 0.21.5
1005
+
1006
1007
+ optionalDependencies:
1008
+ '@esbuild/aix-ppc64': 0.25.8
1009
+ '@esbuild/android-arm': 0.25.8
1010
+ '@esbuild/android-arm64': 0.25.8
1011
+ '@esbuild/android-x64': 0.25.8
1012
+ '@esbuild/darwin-arm64': 0.25.8
1013
+ '@esbuild/darwin-x64': 0.25.8
1014
+ '@esbuild/freebsd-arm64': 0.25.8
1015
+ '@esbuild/freebsd-x64': 0.25.8
1016
+ '@esbuild/linux-arm': 0.25.8
1017
+ '@esbuild/linux-arm64': 0.25.8
1018
+ '@esbuild/linux-ia32': 0.25.8
1019
+ '@esbuild/linux-loong64': 0.25.8
1020
+ '@esbuild/linux-mips64el': 0.25.8
1021
+ '@esbuild/linux-ppc64': 0.25.8
1022
+ '@esbuild/linux-riscv64': 0.25.8
1023
+ '@esbuild/linux-s390x': 0.25.8
1024
+ '@esbuild/linux-x64': 0.25.8
1025
+ '@esbuild/netbsd-arm64': 0.25.8
1026
+ '@esbuild/netbsd-x64': 0.25.8
1027
+ '@esbuild/openbsd-arm64': 0.25.8
1028
+ '@esbuild/openbsd-x64': 0.25.8
1029
+ '@esbuild/openharmony-arm64': 0.25.8
1030
+ '@esbuild/sunos-x64': 0.25.8
1031
+ '@esbuild/win32-arm64': 0.25.8
1032
+ '@esbuild/win32-ia32': 0.25.8
1033
+ '@esbuild/win32-x64': 0.25.8
1034
+
1035
1036
+ optionalDependencies:
1037
+ picomatch: 4.0.3
1038
+
1039
1040
+ optional: true
1041
+
1042
1043
+ dependencies:
1044
+ resolve-pkg-maps: 1.0.0
1045
+ optional: true
1046
+
1047
1048
+
1049
1050
+
1051
1052
+
1053
1054
+
1055
1056
+
1057
1058
+
1059
1060
+
1061
1062
+
1063
1064
+ dependencies:
1065
+ nanoid: 3.3.11
1066
+ picocolors: 1.1.1
1067
+ source-map-js: 1.2.1
1068
+
1069
1070
+ optional: true
1071
+
1072
1073
+ dependencies:
1074
+ '@types/estree': 1.0.8
1075
+ optionalDependencies:
1076
+ '@rollup/rollup-android-arm-eabi': 4.46.0
1077
+ '@rollup/rollup-android-arm64': 4.46.0
1078
+ '@rollup/rollup-darwin-arm64': 4.46.0
1079
+ '@rollup/rollup-darwin-x64': 4.46.0
1080
+ '@rollup/rollup-freebsd-arm64': 4.46.0
1081
+ '@rollup/rollup-freebsd-x64': 4.46.0
1082
+ '@rollup/rollup-linux-arm-gnueabihf': 4.46.0
1083
+ '@rollup/rollup-linux-arm-musleabihf': 4.46.0
1084
+ '@rollup/rollup-linux-arm64-gnu': 4.46.0
1085
+ '@rollup/rollup-linux-arm64-musl': 4.46.0
1086
+ '@rollup/rollup-linux-loongarch64-gnu': 4.46.0
1087
+ '@rollup/rollup-linux-ppc64-gnu': 4.46.0
1088
+ '@rollup/rollup-linux-riscv64-gnu': 4.46.0
1089
+ '@rollup/rollup-linux-riscv64-musl': 4.46.0
1090
+ '@rollup/rollup-linux-s390x-gnu': 4.46.0
1091
+ '@rollup/rollup-linux-x64-gnu': 4.46.0
1092
+ '@rollup/rollup-linux-x64-musl': 4.46.0
1093
+ '@rollup/rollup-win32-arm64-msvc': 4.46.0
1094
+ '@rollup/rollup-win32-ia32-msvc': 4.46.0
1095
+ '@rollup/rollup-win32-x64-msvc': 4.46.0
1096
+ fsevents: 2.3.3
1097
+
1098
1099
+ dependencies:
1100
+ '@serialport/binding-mock': 10.2.2
1101
+ '@serialport/bindings-cpp': 12.0.1
1102
+ '@serialport/parser-byte-length': 12.0.0
1103
+ '@serialport/parser-cctalk': 12.0.0
1104
+ '@serialport/parser-delimiter': 12.0.0
1105
+ '@serialport/parser-inter-byte-timeout': 12.0.0
1106
+ '@serialport/parser-packet-length': 12.0.0
1107
+ '@serialport/parser-readline': 12.0.0
1108
+ '@serialport/parser-ready': 12.0.0
1109
+ '@serialport/parser-regex': 12.0.0
1110
+ '@serialport/parser-slip-encoder': 12.0.0
1111
+ '@serialport/parser-spacepacket': 12.0.0
1112
+ '@serialport/stream': 12.0.0
1113
+ debug: 4.3.4
1114
+ transitivePeerDependencies:
1115
+ - supports-color
1116
+
1117
1118
+
1119
1120
+ dependencies:
1121
+ fdir: 6.4.6([email protected])
1122
+ picomatch: 4.0.3
1123
+
1124
1125
+ dependencies:
1126
+ esbuild: 0.25.8
1127
+ get-tsconfig: 4.10.1
1128
+ optionalDependencies:
1129
+ fsevents: 2.3.3
1130
+ optional: true
1131
+
1132
1133
+
1134
1135
+
1136
1137
+ dependencies:
1138
+ cac: 6.7.14
1139
+ debug: 4.4.1
1140
+ es-module-lexer: 1.7.0
1141
+ pathe: 1.1.2
1142
+ vite: 5.4.19(@types/[email protected])
1143
+ transitivePeerDependencies:
1144
+ - '@types/node'
1145
+ - less
1146
+ - lightningcss
1147
+ - sass
1148
+ - sass-embedded
1149
+ - stylus
1150
+ - sugarss
1151
+ - supports-color
1152
+ - terser
1153
+
1154
1155
+ dependencies:
1156
+ esbuild: 0.21.5
1157
+ postcss: 8.5.6
1158
+ rollup: 4.46.0
1159
+ optionalDependencies:
1160
+ '@types/node': 18.19.120
1161
+ fsevents: 2.3.3
1162
+
1163
1164
+ dependencies:
1165
+ esbuild: 0.25.8
1166
+ fdir: 6.4.6([email protected])
1167
+ picomatch: 4.0.3
1168
+ postcss: 8.5.6
1169
+ rollup: 4.46.0
1170
+ tinyglobby: 0.2.14
1171
+ optionalDependencies:
1172
+ '@types/node': 18.19.120
1173
+ fsevents: 2.3.3
1174
+ tsx: 4.20.3
examples/node-quick-start/src/demo-calibrate.ts ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Calibration Demo
3
+ *
4
+ * Demonstrates robot motor calibration with live feedback
5
+ */
6
+
7
+ import { findPort, connectPort, releaseMotors, calibrate } from "@lerobot/node";
8
+ import type { RobotConnection, DiscoveredPort } from "@lerobot/node";
9
+
10
+ async function demoCalibrate() {
11
+ console.log("🎯 Calibration Demo");
12
+ console.log("===================\n");
13
+
14
+ try {
15
+ // Step 1: Find available robot ports
16
+ console.log("📡 Looking for connected robots...");
17
+ const findProcess = await findPort();
18
+ const discoveredPorts = await findProcess.result;
19
+
20
+ if (discoveredPorts.length === 0) {
21
+ throw new Error("No robots found. Please connect your robot first.");
22
+ }
23
+
24
+ console.log(`✅ Found robot on ${discoveredPorts[0].path}`);
25
+
26
+ // Step 2: Connect to robot
27
+ console.log("🔌 Connecting to robot...");
28
+ const robot = await connectPort(
29
+ discoveredPorts[0].path,
30
+ "so100_follower",
31
+ "calibration_demo"
32
+ );
33
+ console.log(`✅ Connected: ${robot.robotType} (ID: ${robot.robotId})\n`);
34
+
35
+ // Step 3: Release motors
36
+ console.log("🔓 Releasing motors for calibration setup...");
37
+ await releaseMotors(robot);
38
+ console.log("✅ Motors released - robot can now be moved by hand");
39
+
40
+ console.log("\n📍 Move robot to your preferred starting position...");
41
+ console.log("Press any key to continue...");
42
+
43
+ // Simple key press handler without readline conflicts
44
+ process.stdin.setRawMode(true);
45
+ process.stdin.resume();
46
+
47
+ await new Promise<void>((resolve) => {
48
+ const onData = () => {
49
+ process.stdin.setRawMode(false);
50
+ process.stdin.pause();
51
+ process.stdin.removeListener("data", onData);
52
+ resolve();
53
+ };
54
+ process.stdin.once("data", onData);
55
+ });
56
+
57
+ // Step 4: Calibration process
58
+ console.log("\n🎯 Starting calibration process...");
59
+ console.log("This will:");
60
+ console.log("1. Set homing offsets (center positions)");
61
+ console.log("2. Record range of motion for each motor");
62
+ console.log("3. Write position limits to robot hardware");
63
+ console.log("4. Save calibration data for future use\n");
64
+
65
+ const calibrationProcess = await calibrate({
66
+ robot,
67
+ onProgress: (message) => {
68
+ console.log(`📊 ${message}`);
69
+ },
70
+ onLiveUpdate: (data) => {
71
+ // Display real-time motor positions and ranges
72
+ const updates = Object.entries(data).map(([name, info]) => {
73
+ const range = info.max - info.min;
74
+ return `${name}: ${info.current} [${info.min}→${info.max}] (range: ${range})`;
75
+ });
76
+
77
+ console.clear();
78
+ console.log("🔄 Live Calibration Data:");
79
+ console.log("========================");
80
+ updates.forEach((update) => console.log(` ${update}`));
81
+ console.log("\n💡 Move each motor through its full range of motion");
82
+ console.log(" Press Enter to complete calibration...");
83
+ },
84
+ });
85
+
86
+ // Wait for calibration to complete (it handles user input internally)
87
+ const calibrationData = await calibrationProcess.result;
88
+
89
+ console.log("\n✅ Calibration completed successfully!");
90
+
91
+ // Display detailed results
92
+ console.log("\n📋 Detailed Calibration Results:");
93
+ console.log("=================================");
94
+ Object.entries(calibrationData).forEach(([motorName, config]) => {
95
+ const range = config.range_max - config.range_min;
96
+ console.log(`${motorName}:`);
97
+ console.log(` Motor ID: ${config.id}`);
98
+ console.log(` Drive Mode: ${config.drive_mode}`);
99
+ console.log(` Homing Offset: ${config.homing_offset}`);
100
+ console.log(
101
+ ` Range: ${config.range_min} → ${config.range_max} (${range} steps)`
102
+ );
103
+ console.log(` Degrees: ~${((range / 4096) * 360).toFixed(1)}°\n`);
104
+ });
105
+
106
+ console.log("💾 Calibration saved to HuggingFace cache directory");
107
+ console.log("🔄 This file is compatible with Python lerobot");
108
+
109
+ console.log("\n🎉 Calibration demo completed!");
110
+ console.log("💡 You can now use this calibration data for teleoperation");
111
+
112
+ // Ensure process can exit cleanly
113
+ process.exit(0);
114
+ } catch (error) {
115
+ console.error("\n❌ Calibration failed:", error.message);
116
+ console.log("\n🔧 Troubleshooting:");
117
+ console.log("- Ensure robot is connected and responsive");
118
+ console.log("- Check that motors can move freely during calibration");
119
+ console.log("- Avoid forcing motors past their mechanical limits");
120
+ console.log("- Try restarting the robot if motors become unresponsive");
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ demoCalibrate();
examples/node-quick-start/src/demo-find-port.ts ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Port Discovery Demo
3
+ *
4
+ * Demonstrates how to find and connect to robot hardware programmatically
5
+ */
6
+
7
+ import { findPort, connectPort } from "@lerobot/node";
8
+ import type { RobotConnection, DiscoveredPort } from "@lerobot/node";
9
+
10
+ async function demoFindPort() {
11
+ console.log("🔍 Port Discovery Demo");
12
+ console.log("======================\n");
13
+
14
+ try {
15
+ // Demo 1: Basic port discovery
16
+ console.log("📋 Demo 1: Basic robot discovery");
17
+ console.log("Looking for connected robots...\n");
18
+
19
+ const findProcess = await findPort({
20
+ onMessage: (message) => console.log(` 📡 ${message}`),
21
+ });
22
+ const discoveredPorts = await findProcess.result;
23
+
24
+ if (discoveredPorts.length === 0) {
25
+ console.log("❌ No robots found.");
26
+ console.log("\n🔧 Make sure your robot is:");
27
+ console.log(" - Connected via USB");
28
+ console.log(" - Powered on");
29
+ console.log(" - Using a working USB cable");
30
+ return;
31
+ }
32
+
33
+ console.log(`\n✅ Found ${discoveredPorts.length} robot port(s):`);
34
+ discoveredPorts.forEach((port, index) => {
35
+ console.log(` ${index + 1}. ${port.robotType} on ${port.path}`);
36
+ console.log(` Port: ${port.path}`);
37
+ console.log(` Type: ${port.robotType}`);
38
+ });
39
+
40
+ // Demo 2: Connect to discovered robot
41
+ console.log("\n🔌 Demo 2: Connecting to discovered robot");
42
+
43
+ const robot = await connectPort(
44
+ discoveredPorts[0].path,
45
+ "so100_follower",
46
+ "demo_robot"
47
+ );
48
+
49
+ console.log(
50
+ `✅ Connected to robot: ${robot.robotType} (ID: ${robot.robotId})`
51
+ );
52
+ console.log(` Port: ${robot.port.path}`);
53
+ console.log(` Connected: ${robot.isConnected ? "✅" : "❌"}`);
54
+ console.log(` Baudrate: ${robot.port.baudRate}`);
55
+
56
+ // Demo 3: Connection details
57
+ console.log("\n🔌 Demo 3: Connection details");
58
+ console.log("Robot connection properties:");
59
+ console.log(` Name: ${robot.name}`);
60
+ console.log(` Type: ${robot.robotType}`);
61
+ console.log(` ID: ${robot.robotId}`);
62
+ console.log(` Port: ${robot.port.path}`);
63
+ console.log(` Serial: ${robot.serialNumber}`);
64
+ console.log(` Connected: ${robot.isConnected ? "✅" : "❌"}`);
65
+
66
+ // Demo 4: Silent discovery (no progress messages)
67
+ console.log("\n🤫 Demo 4: Silent discovery");
68
+ console.log("Finding robots without progress messages...");
69
+
70
+ const silentProcess = await findPort(); // No onMessage callback
71
+ const silentRobots = await silentProcess.result;
72
+
73
+ console.log(`Found ${silentRobots.length} robot(s) silently`);
74
+
75
+ console.log("\n🎉 Port discovery demo completed!");
76
+ console.log("\nℹ️ Note: For interactive port discovery, use the CLI:");
77
+ console.log(" npx lerobot find-port");
78
+ } catch (error) {
79
+ console.error("\n❌ Port discovery failed:", error.message);
80
+ console.log("\n🔧 Troubleshooting:");
81
+ console.log("- Check USB connections");
82
+ console.log("- Verify robot is powered on");
83
+ console.log("- Try different USB ports/cables");
84
+ console.log("- On Linux: Check serial port permissions");
85
+ console.log("- For interactive port discovery, use: npx lerobot find-port");
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ demoFindPort();
examples/node-quick-start/src/demo-teleoperate.ts ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Teleoperation Demo
3
+ *
4
+ * Demonstrates different ways to control robot motors
5
+ */
6
+
7
+ import { findPort, connectPort, teleoperate } from "@lerobot/node";
8
+ import type { RobotConnection, DiscoveredPort } from "@lerobot/node";
9
+ import { createInterface } from "readline";
10
+
11
+ function askUser(question: string): Promise<string> {
12
+ const rl = createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout,
15
+ });
16
+
17
+ return new Promise((resolve) => {
18
+ rl.question(question, (answer) => {
19
+ rl.close();
20
+ resolve(answer.trim());
21
+ });
22
+ });
23
+ }
24
+
25
+ async function demoTeleoperate() {
26
+ console.log("🎮 Teleoperation Demo");
27
+ console.log("=====================\n");
28
+
29
+ try {
30
+ // Step 1: Find available robot ports
31
+ console.log("📡 Looking for connected robots...");
32
+ const findProcess = await findPort();
33
+ const discoveredPorts = await findProcess.result;
34
+
35
+ if (discoveredPorts.length === 0) {
36
+ throw new Error("No robots found. Please connect your robot first.");
37
+ }
38
+
39
+ console.log(`✅ Found robot on ${discoveredPorts[0].path}`);
40
+
41
+ // Step 2: Connect to robot
42
+ console.log("🔌 Connecting to robot...");
43
+ const robot = await connectPort(
44
+ discoveredPorts[0].path,
45
+ "so100_follower",
46
+ "teleop_demo"
47
+ );
48
+ console.log(`✅ Connected: ${robot.robotType} (ID: ${robot.robotId})\n`);
49
+
50
+ // Step 3: Choose teleoperation mode
51
+ console.log("🎯 Choose teleoperation mode:");
52
+ console.log("1. Keyboard Control (interactive)");
53
+ console.log("2. Direct Control (programmatic)");
54
+
55
+ const mode = await askUser("Enter choice (1 or 2): ");
56
+
57
+ if (mode === "1") {
58
+ // Keyboard teleoperation demo
59
+ await demoKeyboardControl(robot);
60
+ } else if (mode === "2") {
61
+ // Direct control demo
62
+ await demoDirectControl(robot);
63
+ } else {
64
+ console.log("Invalid choice. Defaulting to keyboard control...");
65
+ await demoKeyboardControl(robot);
66
+ }
67
+ } catch (error) {
68
+ console.error("\n❌ Teleoperation failed:", error.message);
69
+ console.log("\n🔧 Troubleshooting:");
70
+ console.log("- Ensure robot is calibrated first");
71
+ console.log("- Check that robot is connected and responsive");
72
+ console.log("- Verify calibration data exists");
73
+ console.log("- Try smaller step sizes if movements are too large");
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ async function demoKeyboardControl(robot: RobotConnection) {
79
+ console.log("\n⌨️ Keyboard Control Demo");
80
+ console.log("=========================");
81
+ console.log("\n🎮 Robot Controls:");
82
+ console.log(" Arrow Keys: Shoulder pan/lift");
83
+ console.log(" W/S: Elbow flex/extend");
84
+ console.log(" A/D: Wrist down/up");
85
+ console.log(" Q/E: Wrist roll left/right");
86
+ console.log(" O/C: Gripper open/close");
87
+ console.log(" ESC: Emergency stop");
88
+ console.log(" Ctrl+C: Exit demo\n");
89
+
90
+ const teleop = await teleoperate({
91
+ robot,
92
+ teleop: {
93
+ type: "keyboard",
94
+ // Using optimized defaults for smooth control
95
+ },
96
+ onStateUpdate: (state) => {
97
+ if (state.isActive) {
98
+ // Show live motor positions
99
+ const motorInfo = state.motorConfigs
100
+ .map((motor) => {
101
+ const pos = Math.round(motor.currentPosition);
102
+ const percent = (
103
+ ((pos - motor.minPosition) /
104
+ (motor.maxPosition - motor.minPosition)) *
105
+ 100
106
+ ).toFixed(0);
107
+ return `${motor.name}:${pos}(${percent}%)`;
108
+ })
109
+ .join(" ");
110
+ process.stdout.write(`\r🤖 ${motorInfo}`);
111
+ }
112
+ },
113
+ });
114
+
115
+ // Start keyboard control
116
+ teleop.start();
117
+ console.log("✅ Keyboard control active!");
118
+ console.log("💡 Move robot with keyboard, press Ctrl+C to exit");
119
+
120
+ // Handle graceful shutdown
121
+ process.on("SIGINT", async () => {
122
+ console.log("\n🛑 Stopping keyboard control...");
123
+ teleop.stop();
124
+ await teleop.disconnect();
125
+ console.log("✅ Keyboard control demo completed!");
126
+ process.exit(0);
127
+ });
128
+
129
+ // Keep demo running
130
+ await new Promise(() => {}); // Keep alive
131
+ }
132
+
133
+ async function demoDirectControl(robot: RobotConnection) {
134
+ console.log("\n🎯 Direct Control Demo");
135
+ console.log("======================");
136
+ console.log("This demonstrates programmatic robot control\n");
137
+
138
+ const teleop = await teleoperate({
139
+ robot,
140
+ teleop: { type: "direct" },
141
+ onStateUpdate: (state) => {
142
+ const motorInfo = state.motorConfigs
143
+ .map((motor) => `${motor.name}:${Math.round(motor.currentPosition)}`)
144
+ .join(" ");
145
+ console.log(`🤖 ${motorInfo}`);
146
+ },
147
+ });
148
+
149
+ teleop.start();
150
+
151
+ // Get direct control interface
152
+ const directController = teleop.teleoperator as any;
153
+
154
+ console.log("🎬 Running automated movement sequence...\n");
155
+
156
+ try {
157
+ // Demo sequence: move different motors
158
+ const movements = [
159
+ {
160
+ motor: "shoulder_pan",
161
+ position: 2048,
162
+ description: "Center shoulder pan",
163
+ },
164
+ { motor: "shoulder_lift", position: 1500, description: "Lift shoulder" },
165
+ { motor: "elbow_flex", position: 2500, description: "Flex elbow" },
166
+ { motor: "wrist_flex", position: 2000, description: "Adjust wrist" },
167
+ { motor: "wrist_roll", position: 2048, description: "Center wrist roll" },
168
+ { motor: "gripper", position: 1800, description: "Adjust gripper" },
169
+ ];
170
+
171
+ for (const movement of movements) {
172
+ console.log(`🎯 ${movement.description}...`);
173
+ await directController.moveMotor(movement.motor, movement.position);
174
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
175
+ }
176
+
177
+ console.log("\n🎉 Movement sequence completed!");
178
+
179
+ // Demo multi-motor movement
180
+ console.log("\n🎭 Demonstrating simultaneous multi-motor movement...");
181
+ const results = await directController.moveMotors({
182
+ shoulder_pan: 2048,
183
+ shoulder_lift: 2048,
184
+ elbow_flex: 2048,
185
+ wrist_flex: 2048,
186
+ });
187
+
188
+ console.log("📊 Movement results:");
189
+ Object.entries(results).forEach(([motor, success]) => {
190
+ console.log(` ${motor}: ${success ? "✅" : "❌"}`);
191
+ });
192
+
193
+ // Show current positions
194
+ const positions = directController.getCurrentPositions();
195
+ console.log("\n📍 Final positions:");
196
+ Object.entries(positions).forEach(([motor, position]) => {
197
+ console.log(` ${motor}: ${Math.round(position as number)}`);
198
+ });
199
+ } finally {
200
+ console.log("\n🛑 Stopping direct control...");
201
+ teleop.stop();
202
+ await teleop.disconnect();
203
+ console.log("✅ Direct control demo completed!");
204
+ }
205
+ }
206
+
207
+ demoTeleoperate();
examples/node-quick-start/src/main.ts ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Node.js Quick Start Example - Complete Workflow
3
+ *
4
+ * This example demonstrates the full robot control workflow:
5
+ * 1. Find and connect to robot hardware
6
+ * 2. Release motors for manual positioning
7
+ * 3. Calibrate motor ranges and homing positions
8
+ * 4. Control robot with keyboard teleoperation
9
+ */
10
+
11
+ import {
12
+ findPort,
13
+ connectPort,
14
+ releaseMotors,
15
+ calibrate,
16
+ teleoperate,
17
+ } from "@lerobot/node";
18
+ import type { RobotConnection, DiscoveredPort } from "@lerobot/node";
19
+
20
+ // Utility for user confirmation
21
+ import { createInterface } from "readline";
22
+
23
+ function askUser(question: string): Promise<string> {
24
+ const rl = createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout,
27
+ });
28
+
29
+ return new Promise((resolve) => {
30
+ rl.question(question, (answer) => {
31
+ rl.close();
32
+ resolve(answer.trim());
33
+ });
34
+ });
35
+ }
36
+
37
+ async function quickStartDemo() {
38
+ console.log("🤖 LeRobot.js Node.js Quick Start Demo");
39
+ console.log("=====================================\n");
40
+
41
+ try {
42
+ // Step 1: Find available robot ports
43
+ console.log("📡 Step 1: Looking for connected robots...");
44
+ const findProcess = await findPort();
45
+ const discoveredPorts = await findProcess.result;
46
+
47
+ if (discoveredPorts.length === 0) {
48
+ throw new Error("No robots found. Please check your connections.");
49
+ }
50
+
51
+ console.log(`✅ Found robot on ${discoveredPorts[0].path}`);
52
+
53
+ // Step 2: Connect to the first robot found
54
+ console.log("🔌 Step 2: Connecting to robot...");
55
+ const robot = await connectPort(
56
+ discoveredPorts[0].path,
57
+ "so100_follower",
58
+ "demo_robot_arm"
59
+ );
60
+ console.log(`✅ Connected: ${robot.robotType} (ID: ${robot.robotId})\n`);
61
+
62
+ // Step 3: Release motors for calibration setup
63
+ const shouldRelease = await askUser(
64
+ "🔓 Release motors for manual positioning? (y/n): "
65
+ );
66
+ if (shouldRelease.toLowerCase() === "y") {
67
+ console.log("🔓 Step 2: Releasing motors...");
68
+ await releaseMotors(robot);
69
+ console.log("✅ Motors released - you can now move the robot by hand\n");
70
+
71
+ await askUser(
72
+ "Move robot to desired starting position, then press Enter to continue..."
73
+ );
74
+ }
75
+
76
+ // Step 4: Calibrate the robot
77
+ const shouldCalibrate = await askUser("🎯 Run calibration? (y/n): ");
78
+ if (shouldCalibrate.toLowerCase() === "y") {
79
+ console.log("\n🎯 Step 3: Starting calibration...");
80
+ console.log(
81
+ "This will record the motor ranges and set homing positions.\n"
82
+ );
83
+
84
+ const calibrationProcess = await calibrate({
85
+ robot: robot as RobotConnection,
86
+ onProgress: (message) => {
87
+ console.log(` 📊 ${message}`);
88
+ },
89
+ onLiveUpdate: (data) => {
90
+ // Show live motor positions during range recording
91
+ const positions = Object.entries(data)
92
+ .map(
93
+ ([name, info]) =>
94
+ `${name}:${info.current}(${info.min}-${info.max})`
95
+ )
96
+ .join(" ");
97
+ process.stdout.write(`\r 🔄 Live: ${positions}`);
98
+ },
99
+ });
100
+
101
+ const calibrationData = await calibrationProcess.result;
102
+ console.log("\n✅ Calibration completed!");
103
+
104
+ // Show calibration summary
105
+ console.log("\n📋 Calibration Results:");
106
+ Object.entries(calibrationData).forEach(([motorName, config]) => {
107
+ console.log(
108
+ ` ${motorName}: range ${config.range_min}-${config.range_max}, offset ${config.homing_offset}`
109
+ );
110
+ });
111
+ }
112
+
113
+ // Step 5: Teleoperation
114
+ const shouldTeleoperate = await askUser(
115
+ "\n🎮 Start keyboard teleoperation? (y/n): "
116
+ );
117
+ if (shouldTeleoperate.toLowerCase() === "y") {
118
+ console.log("\n🎮 Step 4: Starting teleoperation...");
119
+ console.log("Use keyboard to control the robot:\n");
120
+
121
+ const teleop = await teleoperate({
122
+ robot: robot as RobotConnection,
123
+ teleop: { type: "keyboard" },
124
+ onStateUpdate: (state) => {
125
+ if (state.isActive) {
126
+ const motorInfo = state.motorConfigs
127
+ .map(
128
+ (motor) => `${motor.name}:${Math.round(motor.currentPosition)}`
129
+ )
130
+ .join(" ");
131
+ process.stdout.write(`\r🤖 Motors: ${motorInfo}`);
132
+ }
133
+ },
134
+ });
135
+
136
+ // Start keyboard control
137
+ teleop.start();
138
+
139
+ console.log("✅ Teleoperation active!");
140
+ console.log("🎯 Use arrow keys, WASD, Q/E, O/C to control");
141
+ console.log("⚠️ Press ESC for emergency stop, Ctrl+C to exit\n");
142
+
143
+ // Handle graceful shutdown
144
+ process.on("SIGINT", async () => {
145
+ console.log("\n🛑 Shutting down teleoperation...");
146
+ teleop.stop();
147
+ await teleop.disconnect();
148
+ console.log("✅ Demo completed successfully!");
149
+ process.exit(0);
150
+ });
151
+
152
+ // Keep the demo running
153
+ console.log("Demo is running... Press Ctrl+C to stop");
154
+ await new Promise(() => {}); // Keep alive
155
+ }
156
+
157
+ console.log("\n🎉 Quick Start Demo completed!");
158
+ console.log(
159
+ "You can now integrate @lerobot/node into your own applications."
160
+ );
161
+ } catch (error) {
162
+ console.error("\n❌ Demo failed:", error.message);
163
+ console.log("\n🔧 Troubleshooting tips:");
164
+ console.log("- Check robot is connected and powered on");
165
+ console.log("- Verify correct serial port permissions");
166
+ console.log("- Try running 'npx lerobot find-port' to test connection");
167
+ process.exit(1);
168
+ }
169
+ }
170
+
171
+ // Handle uncaught errors gracefully
172
+ process.on("uncaughtException", (error) => {
173
+ console.error("\n💥 Unexpected error:", error.message);
174
+ process.exit(1);
175
+ });
176
+
177
+ process.on("unhandledRejection", (error) => {
178
+ console.error("\n💥 Unhandled promise rejection:", error);
179
+ process.exit(1);
180
+ });
181
+
182
+ // Run the demo
183
+ quickStartDemo();
examples/node-quick-start/test-homing-offsets.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "shoulder_pan": {
3
+ "id": 1,
4
+ "drive_mode": 0,
5
+ "homing_offset": 0,
6
+ "range_min": 0,
7
+ "range_max": 4095
8
+ },
9
+ "shoulder_lift": {
10
+ "id": 2,
11
+ "drive_mode": 0,
12
+ "homing_offset": 0,
13
+ "range_min": 0,
14
+ "range_max": 4095
15
+ },
16
+ "elbow_flex": {
17
+ "id": 3,
18
+ "drive_mode": 0,
19
+ "homing_offset": 0,
20
+ "range_min": 0,
21
+ "range_max": 4095
22
+ },
23
+ "wrist_flex": {
24
+ "id": 4,
25
+ "drive_mode": 0,
26
+ "homing_offset": 0,
27
+ "range_min": 0,
28
+ "range_max": 4095
29
+ },
30
+ "wrist_roll": {
31
+ "id": 5,
32
+ "drive_mode": 0,
33
+ "homing_offset": 0,
34
+ "range_min": 0,
35
+ "range_max": 4095
36
+ },
37
+ "gripper": {
38
+ "id": 6,
39
+ "drive_mode": 0,
40
+ "homing_offset": 0,
41
+ "range_min": 0,
42
+ "range_max": 4095
43
+ }
44
+ }
tsconfig.cli.json → examples/node-quick-start/tsconfig.json RENAMED
@@ -1,15 +1,22 @@
1
  {
2
  "compilerOptions": {
3
  "target": "ES2020",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
  "outDir": "./dist",
7
  "rootDir": "./src",
8
- "declaration": true,
9
- "strict": true,
 
10
  "esModuleInterop": true,
 
 
11
  "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true
 
 
 
13
  },
14
- "include": ["src/lerobot/**/*", "src/cli/**/*"]
 
15
  }
 
1
  {
2
  "compilerOptions": {
3
  "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
  "outDir": "./dist",
7
  "rootDir": "./src",
8
+ "declaration": false,
9
+ "sourceMap": true,
10
+ "allowSyntheticDefaultImports": true,
11
  "esModuleInterop": true,
12
+ "allowJs": true,
13
+ "strict": true,
14
  "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true,
18
+ "types": ["node"]
19
  },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["dist", "node_modules"]
22
  }
examples/node-quick-start/vite.config.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import { resolve } from "path";
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ target: "node18",
7
+ lib: {
8
+ entry: {
9
+ main: resolve(__dirname, "src/main.ts"),
10
+ "demo-find-port": resolve(__dirname, "src/demo-find-port.ts"),
11
+ "demo-calibrate": resolve(__dirname, "src/demo-calibrate.ts"),
12
+ "demo-teleoperate": resolve(__dirname, "src/demo-teleoperate.ts"),
13
+ },
14
+ formats: ["es"],
15
+ fileName: (format, entryName) => `${entryName}.js`,
16
+ },
17
+ rollupOptions: {
18
+ external: [
19
+ // Node.js built-ins
20
+ "fs",
21
+ "fs/promises",
22
+ "path",
23
+ "os",
24
+ "readline",
25
+ "process",
26
+ // Dependencies that should remain external
27
+ "serialport",
28
+ "@lerobot/node",
29
+ ],
30
+ },
31
+ outDir: "dist",
32
+ emptyOutDir: true,
33
+ },
34
+ resolve: {
35
+ alias: {
36
+ "@": resolve(__dirname, "src"),
37
+ },
38
+ },
39
+ });
package.json CHANGED
@@ -6,10 +6,7 @@
6
  "workspaces": [
7
  "packages/*"
8
  ],
9
- "files": [
10
- "dist/**/*",
11
- "README.md"
12
- ],
13
  "keywords": [
14
  "robotics",
15
  "ai",
@@ -18,14 +15,13 @@
18
  "lerobot"
19
  ],
20
  "scripts": {
21
- "cli:find-port": "tsx src/cli/index.ts find-port",
22
- "cli:calibrate": "tsx src/cli/index.ts calibrate",
23
- "cli:teleoperate": "tsx src/cli/index.ts teleoperate",
24
  "example:cyberpunk": "cd examples/cyberpunk-standalone && pnpm dev",
25
  "example:iframe-test": "cd examples/iframe-dialog-test && pnpm dev",
26
  "example:sequential-test": "cd examples/test-sequential-operations && pnpm dev",
27
- "build": "pnpm run build:cli",
28
- "build:cli": "tsc --project tsconfig.cli.json",
 
 
29
  "build:cyberpunk": "cd examples/cyberpunk-standalone && pnpm install && pnpm build",
30
  "changeset": "changeset",
31
  "changeset:version": "changeset version",
 
6
  "workspaces": [
7
  "packages/*"
8
  ],
9
+ "private": true,
 
 
 
10
  "keywords": [
11
  "robotics",
12
  "ai",
 
15
  "lerobot"
16
  ],
17
  "scripts": {
 
 
 
18
  "example:cyberpunk": "cd examples/cyberpunk-standalone && pnpm dev",
19
  "example:iframe-test": "cd examples/iframe-dialog-test && pnpm dev",
20
  "example:sequential-test": "cd examples/test-sequential-operations && pnpm dev",
21
+ "dev": "pnpm run dev:node",
22
+ "dev:node": "pnpm -C packages/node dev",
23
+ "dev:cli": "pnpm -C packages/cli dev",
24
+ "build:all": "pnpm -C packages/node build && pnpm -C packages/cli build",
25
  "build:cyberpunk": "cd examples/cyberpunk-standalone && pnpm install && pnpm build",
26
  "changeset": "changeset",
27
  "changeset:version": "changeset version",
packages/cli/.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+ pnpm-debug.log*
7
+
8
+ # Build output
9
+ dist/
10
+ build/
11
+ *.tsbuildinfo
12
+
13
+ # Environment files
14
+ .env
15
+ .env.local
16
+ .env.development.local
17
+ .env.test.local
18
+ .env.production.local
19
+
20
+ # IDE files
21
+ .vscode/
22
+ .idea/
23
+ *.swp
24
+ *.swo
25
+
26
+ # OS files
27
+ .DS_Store
28
+ Thumbs.db
29
+
30
+ # Coverage reports
31
+ coverage/
32
+ *.lcov
33
+
34
+ # Temporary files
35
+ *.tmp
36
+ *.temp
packages/cli/README.md ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # lerobot
2
+
3
+ Python lerobot compatible CLI for Node.js. Provides the same command-line interface as Python lerobot with identical behavior and syntax.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Use directly with npx (recommended)
9
+ npx lerobot find-port
10
+
11
+ # Or install globally
12
+ npm install -g lerobot
13
+ lerobot find-port
14
+ ```
15
+
16
+ ## Commands
17
+
18
+ ### Find Port
19
+
20
+ Discover robot port with interactive cable detection. Matches Python lerobot's `find_port.py` exactly.
21
+
22
+ ```bash
23
+ # Interactive cable detection (always enabled, like Python lerobot)
24
+ lerobot find-port
25
+ ```
26
+
27
+ This command follows Python lerobot's behavior exactly:
28
+
29
+ 1. Lists initial ports
30
+ 2. Prompts to unplug USB cable
31
+ 3. Detects which port disappeared
32
+ 4. Prompts to reconnect cable
33
+ 5. Verifies port is restored
34
+
35
+ ### Calibrate
36
+
37
+ Calibrate robot motors and save calibration data to Hugging Face cache.
38
+
39
+ ```bash
40
+ lerobot calibrate \
41
+ --robot.type=so100_follower \
42
+ --robot.port=/dev/ttyUSB0 \
43
+ --robot.id=my_follower_arm
44
+ ```
45
+
46
+ **Options:**
47
+
48
+ - `--robot.type` - Robot type (e.g., `so100_follower`)
49
+ - `--robot.port` - Serial port (e.g., `/dev/ttyUSB0`, `COM4`)
50
+ - `--robot.id` - Robot identifier (default: `default`)
51
+ - `--output` - Custom output path for calibration file
52
+
53
+ **Compatible with:** `python -m lerobot calibrate`
54
+
55
+ ### Teleoperate
56
+
57
+ Control robot through keyboard teleoperation.
58
+
59
+ ```bash
60
+ lerobot teleoperate \
61
+ --robot.type=so100_follower \
62
+ --robot.port=/dev/ttyUSB0 \
63
+ --robot.id=my_follower_arm
64
+ ```
65
+
66
+ **Options:**
67
+
68
+ - `--robot.type` - Robot type (e.g., `so100_follower`)
69
+ - `--robot.port` - Serial port (e.g., `/dev/ttyUSB0`, `COM4`)
70
+ - `--robot.id` - Robot identifier (default: `default`)
71
+ - `--teleop.type` - Teleoperator type (default: `keyboard`)
72
+ - `--teleop.stepSize` - Step size for keyboard control (default: `25`)
73
+ - `--duration` - Duration in seconds, 0 = unlimited (default: `0`)
74
+
75
+ **Controls:**
76
+
77
+ - `w/s` - Motor 1 up/down
78
+ - `a/d` - Motor 2 left/right
79
+ - `q/e` - Motor 3 up/down
80
+ - `r/f` - Motor 4 forward/back
81
+ - `t/g` - Motor 5 up/down
82
+ - `y/h` - Motor 6 open/close
83
+ - `Ctrl+C` - Stop and exit
84
+
85
+ **Compatible with:** `python -m lerobot teleoperate`
86
+
87
+ ### Release Motors
88
+
89
+ Release robot motors for manual movement.
90
+
91
+ ```bash
92
+ lerobot release-motors \
93
+ --robot.type=so100_follower \
94
+ --robot.port=/dev/ttyUSB0
95
+ ```
96
+
97
+ **Options:**
98
+
99
+ - `--robot.type` - Robot type (e.g., `so100_follower`)
100
+ - `--robot.port` - Serial port (e.g., `/dev/ttyUSB0`, `COM4`)
101
+ - `--robot.id` - Robot identifier (default: `default`)
102
+ - `--motors` - Specific motor IDs to release (comma-separated)
103
+
104
+ **Compatible with:** `python -m lerobot release-motors`
105
+
106
+ ## Python lerobot Compatibility
107
+
108
+ This CLI provides 100% compatible commands with Python lerobot:
109
+
110
+ | Python lerobot | Node.js lerobot | Status |
111
+ | ---------------------------------- | ---------------------------- | ------------- |
112
+ | `python -m lerobot find_port` | `npx lerobot find-port` | ✅ Compatible |
113
+ | `python -m lerobot calibrate` | `npx lerobot calibrate` | ✅ Compatible |
114
+ | `python -m lerobot teleoperate` | `npx lerobot teleoperate` | ✅ Compatible |
115
+ | `python -m lerobot release-motors` | `npx lerobot release-motors` | ✅ Compatible |
116
+
117
+ ### Calibration Data Compatibility
118
+
119
+ Calibration files are saved to the same location as Python lerobot:
120
+
121
+ ```
122
+ ~/.cache/huggingface/lerobot/calibration/robots/{robot_type}/{robot_id}.json
123
+ ```
124
+
125
+ This ensures calibration data is shared between Python and Node.js implementations.
126
+
127
+ ## Examples
128
+
129
+ ### Complete Workflow
130
+
131
+ ```bash
132
+ # 1. Find your robot (interactive mode)
133
+ npx lerobot find-port --interactive
134
+ # Output: Detected port: /dev/ttyUSB0
135
+
136
+ # 2. Calibrate the robot
137
+ npx lerobot calibrate \
138
+ --robot.type=so100_follower \
139
+ --robot.port=/dev/ttyUSB0 \
140
+ --robot.id=my_arm
141
+
142
+ # 3. Control the robot
143
+ npx lerobot teleoperate \
144
+ --robot.type=so100_follower \
145
+ --robot.port=/dev/ttyUSB0 \
146
+ --robot.id=my_arm
147
+
148
+ # 4. Release motors when done
149
+ npx lerobot release-motors \
150
+ --robot.type=so100_follower \
151
+ --robot.port=/dev/ttyUSB0
152
+ ```
153
+
154
+ ### Automation Scripts
155
+
156
+ ```bash
157
+ #!/bin/bash
158
+ # Automated calibration script
159
+
160
+ ROBOT_TYPE="so100_follower"
161
+ ROBOT_PORT="/dev/ttyUSB0"
162
+ ROBOT_ID="production_arm_1"
163
+
164
+ echo "Starting automated calibration..."
165
+ npx lerobot calibrate \
166
+ --robot.type=$ROBOT_TYPE \
167
+ --robot.port=$ROBOT_PORT \
168
+ --robot.id=$ROBOT_ID
169
+
170
+ echo "Calibration complete. Starting teleoperation..."
171
+ npx lerobot teleoperate \
172
+ --robot.type=$ROBOT_TYPE \
173
+ --robot.port=$ROBOT_PORT \
174
+ --robot.id=$ROBOT_ID \
175
+ --duration=60 # Run for 60 seconds
176
+ ```
177
+
178
+ ## Requirements
179
+
180
+ - Node.js 18+
181
+ - Compatible with Windows, macOS, and Linux
182
+ - Same hardware requirements as Python lerobot
183
+
184
+ ## Related Packages
185
+
186
+ - **[@lerobot/node](../node/)** - Node.js library for programmatic control
187
+ - **[@lerobot/web](../web/)** - Browser library for web applications
188
+
189
+ ## License
190
+
191
+ Apache-2.0
packages/cli/package.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "lerobot",
3
+ "version": "0.1.0",
4
+ "description": "CLI for lerobot.js",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "lerobot": "./dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist/**/*",
13
+ "README.md"
14
+ ],
15
+ "keywords": [
16
+ "robotics",
17
+ "cli",
18
+ "lerobot",
19
+ "python-compatible",
20
+ "hardware-control"
21
+ ],
22
+ "scripts": {
23
+ "build": "vite build",
24
+ "dev": "vite-node src/cli.ts",
25
+ "prepublishOnly": "npm run build",
26
+ "test": "echo 'No tests defined'"
27
+ },
28
+ "dependencies": {
29
+ "@lerobot/node": "file:../node",
30
+ "chalk": "^5.3.0",
31
+ "commander": "^11.0.0",
32
+ "serialport": "^12.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "vite": "^6.3.5",
36
+ "vite-node": "^2.0.0",
37
+ "typescript": "^5.3.0",
38
+ "@types/node": "^18.0.0"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/timpietrusky/lerobot.js/tree/main/packages/cli"
46
+ },
47
+ "license": "Apache-2.0",
48
+ "author": "Tim Pietrusky",
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }
packages/cli/pnpm-lock.yaml ADDED
@@ -0,0 +1,1167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ '@lerobot/node':
12
+ specifier: file:../node
13
+ version: file:../node([email protected])
14
+ chalk:
15
+ specifier: ^5.3.0
16
+ version: 5.4.1
17
+ commander:
18
+ specifier: ^11.0.0
19
+ version: 11.1.0
20
+ serialport:
21
+ specifier: ^12.0.0
22
+ version: 12.0.0
23
+ devDependencies:
24
+ '@types/node':
25
+ specifier: ^18.0.0
26
+ version: 18.19.121
27
+ typescript:
28
+ specifier: ^5.3.0
29
+ version: 5.8.3
30
+ vite:
31
+ specifier: ^6.3.5
32
+ version: 6.3.5(@types/[email protected])
33
+ vite-node:
34
+ specifier: ^2.0.0
35
+ version: 2.1.9(@types/[email protected])
36
+
37
+ packages:
38
+
39
+ '@esbuild/[email protected]':
40
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
41
+ engines: {node: '>=12'}
42
+ cpu: [ppc64]
43
+ os: [aix]
44
+
45
+ '@esbuild/[email protected]':
46
+ resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
47
+ engines: {node: '>=18'}
48
+ cpu: [ppc64]
49
+ os: [aix]
50
+
51
+ '@esbuild/[email protected]':
52
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
53
+ engines: {node: '>=12'}
54
+ cpu: [arm64]
55
+ os: [android]
56
+
57
+ '@esbuild/[email protected]':
58
+ resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==}
59
+ engines: {node: '>=18'}
60
+ cpu: [arm64]
61
+ os: [android]
62
+
63
+ '@esbuild/[email protected]':
64
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
65
+ engines: {node: '>=12'}
66
+ cpu: [arm]
67
+ os: [android]
68
+
69
+ '@esbuild/[email protected]':
70
+ resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==}
71
+ engines: {node: '>=18'}
72
+ cpu: [arm]
73
+ os: [android]
74
+
75
+ '@esbuild/[email protected]':
76
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
77
+ engines: {node: '>=12'}
78
+ cpu: [x64]
79
+ os: [android]
80
+
81
+ '@esbuild/[email protected]':
82
+ resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==}
83
+ engines: {node: '>=18'}
84
+ cpu: [x64]
85
+ os: [android]
86
+
87
+ '@esbuild/[email protected]':
88
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
89
+ engines: {node: '>=12'}
90
+ cpu: [arm64]
91
+ os: [darwin]
92
+
93
+ '@esbuild/[email protected]':
94
+ resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==}
95
+ engines: {node: '>=18'}
96
+ cpu: [arm64]
97
+ os: [darwin]
98
+
99
+ '@esbuild/[email protected]':
100
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
101
+ engines: {node: '>=12'}
102
+ cpu: [x64]
103
+ os: [darwin]
104
+
105
+ '@esbuild/[email protected]':
106
+ resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==}
107
+ engines: {node: '>=18'}
108
+ cpu: [x64]
109
+ os: [darwin]
110
+
111
+ '@esbuild/[email protected]':
112
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
113
+ engines: {node: '>=12'}
114
+ cpu: [arm64]
115
+ os: [freebsd]
116
+
117
+ '@esbuild/[email protected]':
118
+ resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==}
119
+ engines: {node: '>=18'}
120
+ cpu: [arm64]
121
+ os: [freebsd]
122
+
123
+ '@esbuild/[email protected]':
124
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
125
+ engines: {node: '>=12'}
126
+ cpu: [x64]
127
+ os: [freebsd]
128
+
129
+ '@esbuild/[email protected]':
130
+ resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==}
131
+ engines: {node: '>=18'}
132
+ cpu: [x64]
133
+ os: [freebsd]
134
+
135
+ '@esbuild/[email protected]':
136
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
137
+ engines: {node: '>=12'}
138
+ cpu: [arm64]
139
+ os: [linux]
140
+
141
+ '@esbuild/[email protected]':
142
+ resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==}
143
+ engines: {node: '>=18'}
144
+ cpu: [arm64]
145
+ os: [linux]
146
+
147
+ '@esbuild/[email protected]':
148
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
149
+ engines: {node: '>=12'}
150
+ cpu: [arm]
151
+ os: [linux]
152
+
153
+ '@esbuild/[email protected]':
154
+ resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==}
155
+ engines: {node: '>=18'}
156
+ cpu: [arm]
157
+ os: [linux]
158
+
159
+ '@esbuild/[email protected]':
160
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
161
+ engines: {node: '>=12'}
162
+ cpu: [ia32]
163
+ os: [linux]
164
+
165
+ '@esbuild/[email protected]':
166
+ resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==}
167
+ engines: {node: '>=18'}
168
+ cpu: [ia32]
169
+ os: [linux]
170
+
171
+ '@esbuild/[email protected]':
172
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
173
+ engines: {node: '>=12'}
174
+ cpu: [loong64]
175
+ os: [linux]
176
+
177
+ '@esbuild/[email protected]':
178
+ resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==}
179
+ engines: {node: '>=18'}
180
+ cpu: [loong64]
181
+ os: [linux]
182
+
183
+ '@esbuild/[email protected]':
184
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
185
+ engines: {node: '>=12'}
186
+ cpu: [mips64el]
187
+ os: [linux]
188
+
189
+ '@esbuild/[email protected]':
190
+ resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==}
191
+ engines: {node: '>=18'}
192
+ cpu: [mips64el]
193
+ os: [linux]
194
+
195
+ '@esbuild/[email protected]':
196
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
197
+ engines: {node: '>=12'}
198
+ cpu: [ppc64]
199
+ os: [linux]
200
+
201
+ '@esbuild/[email protected]':
202
+ resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==}
203
+ engines: {node: '>=18'}
204
+ cpu: [ppc64]
205
+ os: [linux]
206
+
207
+ '@esbuild/[email protected]':
208
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
209
+ engines: {node: '>=12'}
210
+ cpu: [riscv64]
211
+ os: [linux]
212
+
213
+ '@esbuild/[email protected]':
214
+ resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==}
215
+ engines: {node: '>=18'}
216
+ cpu: [riscv64]
217
+ os: [linux]
218
+
219
+ '@esbuild/[email protected]':
220
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
221
+ engines: {node: '>=12'}
222
+ cpu: [s390x]
223
+ os: [linux]
224
+
225
+ '@esbuild/[email protected]':
226
+ resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==}
227
+ engines: {node: '>=18'}
228
+ cpu: [s390x]
229
+ os: [linux]
230
+
231
+ '@esbuild/[email protected]':
232
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
233
+ engines: {node: '>=12'}
234
+ cpu: [x64]
235
+ os: [linux]
236
+
237
+ '@esbuild/[email protected]':
238
+ resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==}
239
+ engines: {node: '>=18'}
240
+ cpu: [x64]
241
+ os: [linux]
242
+
243
+ '@esbuild/[email protected]':
244
+ resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==}
245
+ engines: {node: '>=18'}
246
+ cpu: [arm64]
247
+ os: [netbsd]
248
+
249
+ '@esbuild/[email protected]':
250
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
251
+ engines: {node: '>=12'}
252
+ cpu: [x64]
253
+ os: [netbsd]
254
+
255
+ '@esbuild/[email protected]':
256
+ resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==}
257
+ engines: {node: '>=18'}
258
+ cpu: [x64]
259
+ os: [netbsd]
260
+
261
+ '@esbuild/[email protected]':
262
+ resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==}
263
+ engines: {node: '>=18'}
264
+ cpu: [arm64]
265
+ os: [openbsd]
266
+
267
+ '@esbuild/[email protected]':
268
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
269
+ engines: {node: '>=12'}
270
+ cpu: [x64]
271
+ os: [openbsd]
272
+
273
+ '@esbuild/[email protected]':
274
+ resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==}
275
+ engines: {node: '>=18'}
276
+ cpu: [x64]
277
+ os: [openbsd]
278
+
279
+ '@esbuild/[email protected]':
280
+ resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==}
281
+ engines: {node: '>=18'}
282
+ cpu: [arm64]
283
+ os: [openharmony]
284
+
285
+ '@esbuild/[email protected]':
286
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
287
+ engines: {node: '>=12'}
288
+ cpu: [x64]
289
+ os: [sunos]
290
+
291
+ '@esbuild/[email protected]':
292
+ resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==}
293
+ engines: {node: '>=18'}
294
+ cpu: [x64]
295
+ os: [sunos]
296
+
297
+ '@esbuild/[email protected]':
298
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
299
+ engines: {node: '>=12'}
300
+ cpu: [arm64]
301
+ os: [win32]
302
+
303
+ '@esbuild/[email protected]':
304
+ resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==}
305
+ engines: {node: '>=18'}
306
+ cpu: [arm64]
307
+ os: [win32]
308
+
309
+ '@esbuild/[email protected]':
310
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
311
+ engines: {node: '>=12'}
312
+ cpu: [ia32]
313
+ os: [win32]
314
+
315
+ '@esbuild/[email protected]':
316
+ resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==}
317
+ engines: {node: '>=18'}
318
+ cpu: [ia32]
319
+ os: [win32]
320
+
321
+ '@esbuild/[email protected]':
322
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
323
+ engines: {node: '>=12'}
324
+ cpu: [x64]
325
+ os: [win32]
326
+
327
+ '@esbuild/[email protected]':
328
+ resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==}
329
+ engines: {node: '>=18'}
330
+ cpu: [x64]
331
+ os: [win32]
332
+
333
+ '@lerobot/node@file:../node':
334
+ resolution: {directory: ../node, type: directory}
335
+ engines: {node: '>=18.0.0'}
336
+ peerDependencies:
337
+ typescript: '>=4.5.0'
338
+
339
+ '@rollup/[email protected]':
340
+ resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==}
341
+ cpu: [arm]
342
+ os: [android]
343
+
344
+ '@rollup/[email protected]':
345
+ resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==}
346
+ cpu: [arm64]
347
+ os: [android]
348
+
349
+ '@rollup/[email protected]':
350
+ resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==}
351
+ cpu: [arm64]
352
+ os: [darwin]
353
+
354
+ '@rollup/[email protected]':
355
+ resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==}
356
+ cpu: [x64]
357
+ os: [darwin]
358
+
359
+ '@rollup/[email protected]':
360
+ resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==}
361
+ cpu: [arm64]
362
+ os: [freebsd]
363
+
364
+ '@rollup/[email protected]':
365
+ resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==}
366
+ cpu: [x64]
367
+ os: [freebsd]
368
+
369
+ '@rollup/[email protected]':
370
+ resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==}
371
+ cpu: [arm]
372
+ os: [linux]
373
+
374
+ '@rollup/[email protected]':
375
+ resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==}
376
+ cpu: [arm]
377
+ os: [linux]
378
+
379
+ '@rollup/[email protected]':
380
+ resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==}
381
+ cpu: [arm64]
382
+ os: [linux]
383
+
384
+ '@rollup/[email protected]':
385
+ resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==}
386
+ cpu: [arm64]
387
+ os: [linux]
388
+
389
+ '@rollup/[email protected]':
390
+ resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==}
391
+ cpu: [loong64]
392
+ os: [linux]
393
+
394
+ '@rollup/[email protected]':
395
+ resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==}
396
+ cpu: [ppc64]
397
+ os: [linux]
398
+
399
+ '@rollup/[email protected]':
400
+ resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==}
401
+ cpu: [riscv64]
402
+ os: [linux]
403
+
404
+ '@rollup/[email protected]':
405
+ resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==}
406
+ cpu: [riscv64]
407
+ os: [linux]
408
+
409
+ '@rollup/[email protected]':
410
+ resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==}
411
+ cpu: [s390x]
412
+ os: [linux]
413
+
414
+ '@rollup/[email protected]':
415
+ resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==}
416
+ cpu: [x64]
417
+ os: [linux]
418
+
419
+ '@rollup/[email protected]':
420
+ resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==}
421
+ cpu: [x64]
422
+ os: [linux]
423
+
424
+ '@rollup/[email protected]':
425
+ resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==}
426
+ cpu: [arm64]
427
+ os: [win32]
428
+
429
+ '@rollup/[email protected]':
430
+ resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==}
431
+ cpu: [ia32]
432
+ os: [win32]
433
+
434
+ '@rollup/[email protected]':
435
+ resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==}
436
+ cpu: [x64]
437
+ os: [win32]
438
+
439
+ '@serialport/[email protected]':
440
+ resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
441
+ engines: {node: '>=12.0.0'}
442
+
443
+ '@serialport/[email protected]':
444
+ resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
445
+ engines: {node: '>=16.0.0'}
446
+
447
+ '@serialport/[email protected]':
448
+ resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
449
+ engines: {node: ^12.22 || ^14.13 || >=16}
450
+
451
+ '@serialport/[email protected]':
452
+ resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
453
+ engines: {node: '>=12.0.0'}
454
+
455
+ '@serialport/[email protected]':
456
+ resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
457
+ engines: {node: '>=12.0.0'}
458
+
459
+ '@serialport/[email protected]':
460
+ resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
461
+ engines: {node: '>=12.0.0'}
462
+
463
+ '@serialport/[email protected]':
464
+ resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
465
+ engines: {node: '>=12.0.0'}
466
+
467
+ '@serialport/[email protected]':
468
+ resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
469
+ engines: {node: '>=12.0.0'}
470
+
471
+ '@serialport/[email protected]':
472
+ resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
473
+ engines: {node: '>=8.6.0'}
474
+
475
+ '@serialport/[email protected]':
476
+ resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
477
+ engines: {node: '>=12.0.0'}
478
+
479
+ '@serialport/[email protected]':
480
+ resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
481
+ engines: {node: '>=12.0.0'}
482
+
483
+ '@serialport/[email protected]':
484
+ resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
485
+ engines: {node: '>=12.0.0'}
486
+
487
+ '@serialport/[email protected]':
488
+ resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
489
+ engines: {node: '>=12.0.0'}
490
+
491
+ '@serialport/[email protected]':
492
+ resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
493
+ engines: {node: '>=12.0.0'}
494
+
495
+ '@serialport/[email protected]':
496
+ resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
497
+ engines: {node: '>=12.0.0'}
498
+
499
+ '@serialport/[email protected]':
500
+ resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
501
+ engines: {node: '>=12.0.0'}
502
+
503
+ '@types/[email protected]':
504
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
505
+
506
+ '@types/[email protected]':
507
+ resolution: {integrity: sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==}
508
+
509
510
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
511
+ engines: {node: '>=8'}
512
+
513
514
+ resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
515
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
516
+
517
518
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
519
+ engines: {node: '>=16'}
520
+
521
522
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
523
+ engines: {node: '>=6.0'}
524
+ peerDependencies:
525
+ supports-color: '*'
526
+ peerDependenciesMeta:
527
+ supports-color:
528
+ optional: true
529
+
530
531
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
532
+ engines: {node: '>=6.0'}
533
+ peerDependencies:
534
+ supports-color: '*'
535
+ peerDependenciesMeta:
536
+ supports-color:
537
+ optional: true
538
+
539
540
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
541
+
542
543
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
544
+ engines: {node: '>=12'}
545
+ hasBin: true
546
+
547
548
+ resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
549
+ engines: {node: '>=18'}
550
+ hasBin: true
551
+
552
553
+ resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
554
+ peerDependencies:
555
+ picomatch: ^3 || ^4
556
+ peerDependenciesMeta:
557
+ picomatch:
558
+ optional: true
559
+
560
561
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
562
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
563
+ os: [darwin]
564
+
565
566
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
567
+
568
569
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
570
+
571
572
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
573
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
574
+ hasBin: true
575
+
576
577
+ resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
578
+
579
580
+ resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
581
+ hasBin: true
582
+
583
584
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
585
+
586
587
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
588
+
589
590
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
591
+ engines: {node: '>=12'}
592
+
593
594
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
595
+ engines: {node: ^10 || ^12 || >=14}
596
+
597
598
+ resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==}
599
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
600
+ hasBin: true
601
+
602
603
+ resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
604
+ engines: {node: '>=16.0.0'}
605
+
606
607
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
608
+ engines: {node: '>=0.10.0'}
609
+
610
611
+ resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
612
+ engines: {node: '>=12.0.0'}
613
+
614
615
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
616
+ engines: {node: '>=14.17'}
617
+ hasBin: true
618
+
619
620
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
621
+
622
623
+ resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
624
+ engines: {node: ^18.0.0 || >=20.0.0}
625
+ hasBin: true
626
+
627
628
+ resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
629
+ engines: {node: ^18.0.0 || >=20.0.0}
630
+ hasBin: true
631
+ peerDependencies:
632
+ '@types/node': ^18.0.0 || >=20.0.0
633
+ less: '*'
634
+ lightningcss: ^1.21.0
635
+ sass: '*'
636
+ sass-embedded: '*'
637
+ stylus: '*'
638
+ sugarss: '*'
639
+ terser: ^5.4.0
640
+ peerDependenciesMeta:
641
+ '@types/node':
642
+ optional: true
643
+ less:
644
+ optional: true
645
+ lightningcss:
646
+ optional: true
647
+ sass:
648
+ optional: true
649
+ sass-embedded:
650
+ optional: true
651
+ stylus:
652
+ optional: true
653
+ sugarss:
654
+ optional: true
655
+ terser:
656
+ optional: true
657
+
658
659
+ resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
660
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
661
+ hasBin: true
662
+ peerDependencies:
663
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
664
+ jiti: '>=1.21.0'
665
+ less: '*'
666
+ lightningcss: ^1.21.0
667
+ sass: '*'
668
+ sass-embedded: '*'
669
+ stylus: '*'
670
+ sugarss: '*'
671
+ terser: ^5.16.0
672
+ tsx: ^4.8.1
673
+ yaml: ^2.4.2
674
+ peerDependenciesMeta:
675
+ '@types/node':
676
+ optional: true
677
+ jiti:
678
+ optional: true
679
+ less:
680
+ optional: true
681
+ lightningcss:
682
+ optional: true
683
+ sass:
684
+ optional: true
685
+ sass-embedded:
686
+ optional: true
687
+ stylus:
688
+ optional: true
689
+ sugarss:
690
+ optional: true
691
+ terser:
692
+ optional: true
693
+ tsx:
694
+ optional: true
695
+ yaml:
696
+ optional: true
697
+
698
+ snapshots:
699
+
700
+ '@esbuild/[email protected]':
701
+ optional: true
702
+
703
+ '@esbuild/[email protected]':
704
+ optional: true
705
+
706
+ '@esbuild/[email protected]':
707
+ optional: true
708
+
709
+ '@esbuild/[email protected]':
710
+ optional: true
711
+
712
+ '@esbuild/[email protected]':
713
+ optional: true
714
+
715
+ '@esbuild/[email protected]':
716
+ optional: true
717
+
718
+ '@esbuild/[email protected]':
719
+ optional: true
720
+
721
+ '@esbuild/[email protected]':
722
+ optional: true
723
+
724
+ '@esbuild/[email protected]':
725
+ optional: true
726
+
727
+ '@esbuild/[email protected]':
728
+ optional: true
729
+
730
+ '@esbuild/[email protected]':
731
+ optional: true
732
+
733
+ '@esbuild/[email protected]':
734
+ optional: true
735
+
736
+ '@esbuild/[email protected]':
737
+ optional: true
738
+
739
+ '@esbuild/[email protected]':
740
+ optional: true
741
+
742
+ '@esbuild/[email protected]':
743
+ optional: true
744
+
745
+ '@esbuild/[email protected]':
746
+ optional: true
747
+
748
+ '@esbuild/[email protected]':
749
+ optional: true
750
+
751
+ '@esbuild/[email protected]':
752
+ optional: true
753
+
754
+ '@esbuild/[email protected]':
755
+ optional: true
756
+
757
+ '@esbuild/[email protected]':
758
+ optional: true
759
+
760
+ '@esbuild/[email protected]':
761
+ optional: true
762
+
763
+ '@esbuild/[email protected]':
764
+ optional: true
765
+
766
+ '@esbuild/[email protected]':
767
+ optional: true
768
+
769
+ '@esbuild/[email protected]':
770
+ optional: true
771
+
772
+ '@esbuild/[email protected]':
773
+ optional: true
774
+
775
+ '@esbuild/[email protected]':
776
+ optional: true
777
+
778
+ '@esbuild/[email protected]':
779
+ optional: true
780
+
781
+ '@esbuild/[email protected]':
782
+ optional: true
783
+
784
+ '@esbuild/[email protected]':
785
+ optional: true
786
+
787
+ '@esbuild/[email protected]':
788
+ optional: true
789
+
790
+ '@esbuild/[email protected]':
791
+ optional: true
792
+
793
+ '@esbuild/[email protected]':
794
+ optional: true
795
+
796
+ '@esbuild/[email protected]':
797
+ optional: true
798
+
799
+ '@esbuild/[email protected]':
800
+ optional: true
801
+
802
+ '@esbuild/[email protected]':
803
+ optional: true
804
+
805
+ '@esbuild/[email protected]':
806
+ optional: true
807
+
808
+ '@esbuild/[email protected]':
809
+ optional: true
810
+
811
+ '@esbuild/[email protected]':
812
+ optional: true
813
+
814
+ '@esbuild/[email protected]':
815
+ optional: true
816
+
817
+ '@esbuild/[email protected]':
818
+ optional: true
819
+
820
+ '@esbuild/[email protected]':
821
+ optional: true
822
+
823
+ '@esbuild/[email protected]':
824
+ optional: true
825
+
826
+ '@esbuild/[email protected]':
827
+ optional: true
828
+
829
+ '@esbuild/[email protected]':
830
+ optional: true
831
+
832
+ '@esbuild/[email protected]':
833
+ optional: true
834
+
835
+ '@esbuild/[email protected]':
836
+ optional: true
837
+
838
+ '@esbuild/[email protected]':
839
+ optional: true
840
+
841
+ '@esbuild/[email protected]':
842
+ optional: true
843
+
844
+ '@esbuild/[email protected]':
845
+ optional: true
846
+
847
+ '@lerobot/node@file:../node([email protected])':
848
+ dependencies:
849
+ serialport: 12.0.0
850
+ typescript: 5.8.3
851
+ transitivePeerDependencies:
852
+ - supports-color
853
+
854
+ '@rollup/[email protected]':
855
+ optional: true
856
+
857
+ '@rollup/[email protected]':
858
+ optional: true
859
+
860
+ '@rollup/[email protected]':
861
+ optional: true
862
+
863
+ '@rollup/[email protected]':
864
+ optional: true
865
+
866
+ '@rollup/[email protected]':
867
+ optional: true
868
+
869
+ '@rollup/[email protected]':
870
+ optional: true
871
+
872
+ '@rollup/[email protected]':
873
+ optional: true
874
+
875
+ '@rollup/[email protected]':
876
+ optional: true
877
+
878
+ '@rollup/[email protected]':
879
+ optional: true
880
+
881
+ '@rollup/[email protected]':
882
+ optional: true
883
+
884
+ '@rollup/[email protected]':
885
+ optional: true
886
+
887
+ '@rollup/[email protected]':
888
+ optional: true
889
+
890
+ '@rollup/[email protected]':
891
+ optional: true
892
+
893
+ '@rollup/[email protected]':
894
+ optional: true
895
+
896
+ '@rollup/[email protected]':
897
+ optional: true
898
+
899
+ '@rollup/[email protected]':
900
+ optional: true
901
+
902
+ '@rollup/[email protected]':
903
+ optional: true
904
+
905
+ '@rollup/[email protected]':
906
+ optional: true
907
+
908
+ '@rollup/[email protected]':
909
+ optional: true
910
+
911
+ '@rollup/[email protected]':
912
+ optional: true
913
+
914
+ '@serialport/[email protected]':
915
+ dependencies:
916
+ '@serialport/bindings-interface': 1.2.2
917
+ debug: 4.4.1
918
+ transitivePeerDependencies:
919
+ - supports-color
920
+
921
+ '@serialport/[email protected]':
922
+ dependencies:
923
+ '@serialport/bindings-interface': 1.2.2
924
+ '@serialport/parser-readline': 11.0.0
925
+ debug: 4.3.4
926
+ node-addon-api: 7.0.0
927
+ node-gyp-build: 4.6.0
928
+ transitivePeerDependencies:
929
+ - supports-color
930
+
931
+ '@serialport/[email protected]': {}
932
+
933
+ '@serialport/[email protected]': {}
934
+
935
+ '@serialport/[email protected]': {}
936
+
937
+ '@serialport/[email protected]': {}
938
+
939
+ '@serialport/[email protected]': {}
940
+
941
+ '@serialport/[email protected]': {}
942
+
943
+ '@serialport/[email protected]': {}
944
+
945
+ '@serialport/[email protected]':
946
+ dependencies:
947
+ '@serialport/parser-delimiter': 11.0.0
948
+
949
+ '@serialport/[email protected]':
950
+ dependencies:
951
+ '@serialport/parser-delimiter': 12.0.0
952
+
953
+ '@serialport/[email protected]': {}
954
+
955
+ '@serialport/[email protected]': {}
956
+
957
+ '@serialport/[email protected]': {}
958
+
959
+ '@serialport/[email protected]': {}
960
+
961
+ '@serialport/[email protected]':
962
+ dependencies:
963
+ '@serialport/bindings-interface': 1.2.2
964
+ debug: 4.3.4
965
+ transitivePeerDependencies:
966
+ - supports-color
967
+
968
+ '@types/[email protected]': {}
969
+
970
+ '@types/[email protected]':
971
+ dependencies:
972
+ undici-types: 5.26.5
973
+
974
975
+
976
977
+
978
979
+
980
981
+ dependencies:
982
+ ms: 2.1.2
983
+
984
985
+ dependencies:
986
+ ms: 2.1.3
987
+
988
989
+
990
991
+ optionalDependencies:
992
+ '@esbuild/aix-ppc64': 0.21.5
993
+ '@esbuild/android-arm': 0.21.5
994
+ '@esbuild/android-arm64': 0.21.5
995
+ '@esbuild/android-x64': 0.21.5
996
+ '@esbuild/darwin-arm64': 0.21.5
997
+ '@esbuild/darwin-x64': 0.21.5
998
+ '@esbuild/freebsd-arm64': 0.21.5
999
+ '@esbuild/freebsd-x64': 0.21.5
1000
+ '@esbuild/linux-arm': 0.21.5
1001
+ '@esbuild/linux-arm64': 0.21.5
1002
+ '@esbuild/linux-ia32': 0.21.5
1003
+ '@esbuild/linux-loong64': 0.21.5
1004
+ '@esbuild/linux-mips64el': 0.21.5
1005
+ '@esbuild/linux-ppc64': 0.21.5
1006
+ '@esbuild/linux-riscv64': 0.21.5
1007
+ '@esbuild/linux-s390x': 0.21.5
1008
+ '@esbuild/linux-x64': 0.21.5
1009
+ '@esbuild/netbsd-x64': 0.21.5
1010
+ '@esbuild/openbsd-x64': 0.21.5
1011
+ '@esbuild/sunos-x64': 0.21.5
1012
+ '@esbuild/win32-arm64': 0.21.5
1013
+ '@esbuild/win32-ia32': 0.21.5
1014
+ '@esbuild/win32-x64': 0.21.5
1015
+
1016
1017
+ optionalDependencies:
1018
+ '@esbuild/aix-ppc64': 0.25.8
1019
+ '@esbuild/android-arm': 0.25.8
1020
+ '@esbuild/android-arm64': 0.25.8
1021
+ '@esbuild/android-x64': 0.25.8
1022
+ '@esbuild/darwin-arm64': 0.25.8
1023
+ '@esbuild/darwin-x64': 0.25.8
1024
+ '@esbuild/freebsd-arm64': 0.25.8
1025
+ '@esbuild/freebsd-x64': 0.25.8
1026
+ '@esbuild/linux-arm': 0.25.8
1027
+ '@esbuild/linux-arm64': 0.25.8
1028
+ '@esbuild/linux-ia32': 0.25.8
1029
+ '@esbuild/linux-loong64': 0.25.8
1030
+ '@esbuild/linux-mips64el': 0.25.8
1031
+ '@esbuild/linux-ppc64': 0.25.8
1032
+ '@esbuild/linux-riscv64': 0.25.8
1033
+ '@esbuild/linux-s390x': 0.25.8
1034
+ '@esbuild/linux-x64': 0.25.8
1035
+ '@esbuild/netbsd-arm64': 0.25.8
1036
+ '@esbuild/netbsd-x64': 0.25.8
1037
+ '@esbuild/openbsd-arm64': 0.25.8
1038
+ '@esbuild/openbsd-x64': 0.25.8
1039
+ '@esbuild/openharmony-arm64': 0.25.8
1040
+ '@esbuild/sunos-x64': 0.25.8
1041
+ '@esbuild/win32-arm64': 0.25.8
1042
+ '@esbuild/win32-ia32': 0.25.8
1043
+ '@esbuild/win32-x64': 0.25.8
1044
+
1045
1046
+ optionalDependencies:
1047
+ picomatch: 4.0.3
1048
+
1049
1050
+ optional: true
1051
+
1052
1053
+
1054
1055
+
1056
1057
+
1058
1059
+
1060
1061
+
1062
1063
+
1064
1065
+
1066
1067
+
1068
1069
+ dependencies:
1070
+ nanoid: 3.3.11
1071
+ picocolors: 1.1.1
1072
+ source-map-js: 1.2.1
1073
+
1074
1075
+ dependencies:
1076
+ '@types/estree': 1.0.8
1077
+ optionalDependencies:
1078
+ '@rollup/rollup-android-arm-eabi': 4.46.2
1079
+ '@rollup/rollup-android-arm64': 4.46.2
1080
+ '@rollup/rollup-darwin-arm64': 4.46.2
1081
+ '@rollup/rollup-darwin-x64': 4.46.2
1082
+ '@rollup/rollup-freebsd-arm64': 4.46.2
1083
+ '@rollup/rollup-freebsd-x64': 4.46.2
1084
+ '@rollup/rollup-linux-arm-gnueabihf': 4.46.2
1085
+ '@rollup/rollup-linux-arm-musleabihf': 4.46.2
1086
+ '@rollup/rollup-linux-arm64-gnu': 4.46.2
1087
+ '@rollup/rollup-linux-arm64-musl': 4.46.2
1088
+ '@rollup/rollup-linux-loongarch64-gnu': 4.46.2
1089
+ '@rollup/rollup-linux-ppc64-gnu': 4.46.2
1090
+ '@rollup/rollup-linux-riscv64-gnu': 4.46.2
1091
+ '@rollup/rollup-linux-riscv64-musl': 4.46.2
1092
+ '@rollup/rollup-linux-s390x-gnu': 4.46.2
1093
+ '@rollup/rollup-linux-x64-gnu': 4.46.2
1094
+ '@rollup/rollup-linux-x64-musl': 4.46.2
1095
+ '@rollup/rollup-win32-arm64-msvc': 4.46.2
1096
+ '@rollup/rollup-win32-ia32-msvc': 4.46.2
1097
+ '@rollup/rollup-win32-x64-msvc': 4.46.2
1098
+ fsevents: 2.3.3
1099
+
1100
1101
+ dependencies:
1102
+ '@serialport/binding-mock': 10.2.2
1103
+ '@serialport/bindings-cpp': 12.0.1
1104
+ '@serialport/parser-byte-length': 12.0.0
1105
+ '@serialport/parser-cctalk': 12.0.0
1106
+ '@serialport/parser-delimiter': 12.0.0
1107
+ '@serialport/parser-inter-byte-timeout': 12.0.0
1108
+ '@serialport/parser-packet-length': 12.0.0
1109
+ '@serialport/parser-readline': 12.0.0
1110
+ '@serialport/parser-ready': 12.0.0
1111
+ '@serialport/parser-regex': 12.0.0
1112
+ '@serialport/parser-slip-encoder': 12.0.0
1113
+ '@serialport/parser-spacepacket': 12.0.0
1114
+ '@serialport/stream': 12.0.0
1115
+ debug: 4.3.4
1116
+ transitivePeerDependencies:
1117
+ - supports-color
1118
+
1119
1120
+
1121
1122
+ dependencies:
1123
+ fdir: 6.4.6([email protected])
1124
+ picomatch: 4.0.3
1125
+
1126
1127
+
1128
1129
+
1130
1131
+ dependencies:
1132
+ cac: 6.7.14
1133
+ debug: 4.4.1
1134
+ es-module-lexer: 1.7.0
1135
+ pathe: 1.1.2
1136
+ vite: 5.4.19(@types/[email protected])
1137
+ transitivePeerDependencies:
1138
+ - '@types/node'
1139
+ - less
1140
+ - lightningcss
1141
+ - sass
1142
+ - sass-embedded
1143
+ - stylus
1144
+ - sugarss
1145
+ - supports-color
1146
+ - terser
1147
+
1148
1149
+ dependencies:
1150
+ esbuild: 0.21.5
1151
+ postcss: 8.5.6
1152
+ rollup: 4.46.2
1153
+ optionalDependencies:
1154
+ '@types/node': 18.19.121
1155
+ fsevents: 2.3.3
1156
+
1157
1158
+ dependencies:
1159
+ esbuild: 0.25.8
1160
+ fdir: 6.4.6([email protected])
1161
+ picomatch: 4.0.3
1162
+ postcss: 8.5.6
1163
+ rollup: 4.46.2
1164
+ tinyglobby: 0.2.14
1165
+ optionalDependencies:
1166
+ '@types/node': 18.19.121
1167
+ fsevents: 2.3.3
packages/cli/src/cli.ts ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * lerobot CLI - Python lerobot compatible command-line interface
5
+ * Uses @lerobot/node library for core functionality with CLI-specific interactive features
6
+ */
7
+
8
+ import { program } from "commander";
9
+ import chalk from "chalk";
10
+ import {
11
+ findPort,
12
+ calibrate,
13
+ teleoperate,
14
+ releaseMotors,
15
+ connectPort,
16
+ } from "@lerobot/node";
17
+ import type { RobotConnection } from "@lerobot/node";
18
+ import { SerialPort } from "serialport";
19
+ import { createInterface } from "readline";
20
+ import { platform } from "os";
21
+ import { readdir } from "fs/promises";
22
+ import { join } from "path";
23
+
24
+ /**
25
+ * CLI-specific function to list available serial ports
26
+ * Only used by the CLI, not part of the library API
27
+ */
28
+ async function findAvailablePorts(): Promise<string[]> {
29
+ if (platform() === "win32") {
30
+ // List COM ports using serialport library (equivalent to pyserial)
31
+ const ports = await SerialPort.list();
32
+ return ports.map((port) => port.path);
33
+ } else {
34
+ // List /dev/tty* ports for Unix-based systems (Linux/macOS)
35
+ try {
36
+ const devFiles = await readdir("/dev");
37
+ const ttyPorts = devFiles
38
+ .filter((file) => file.startsWith("tty"))
39
+ .map((file) => join("/dev", file));
40
+ return ttyPorts;
41
+ } catch (error) {
42
+ // Fallback to serialport library if /dev reading fails
43
+ const ports = await SerialPort.list();
44
+ return ports.map((port) => port.path);
45
+ }
46
+ }
47
+ }
48
+
49
+ /**
50
+ * CLI-specific interactive port detection for Python lerobot compatibility
51
+ * Matches Python lerobot's unplug/replug cable detection exactly
52
+ */
53
+ async function detectPortInteractive(
54
+ onMessage?: (message: string) => void
55
+ ): Promise<string> {
56
+ const rl = createInterface({
57
+ input: process.stdin,
58
+ output: process.stdout,
59
+ });
60
+
61
+ function waitForInput(prompt: string): Promise<string> {
62
+ return new Promise((resolve) => {
63
+ rl.question(prompt, (answer: string) => {
64
+ resolve(answer);
65
+ });
66
+ });
67
+ }
68
+
69
+ try {
70
+ const message = "Finding all available ports for the MotorsBus.";
71
+ if (onMessage) onMessage(message);
72
+ else console.log(message);
73
+
74
+ // Get initial port list
75
+ const portsBefore = await findAvailablePorts();
76
+
77
+ // Show initial ports (Python lerobot style)
78
+ const portsMessage = `Ports before disconnecting: [${portsBefore
79
+ .map((p) => `'${p}'`)
80
+ .join(", ")}]`;
81
+ if (onMessage) onMessage(portsMessage);
82
+ else console.log(portsMessage);
83
+
84
+ const disconnectPrompt =
85
+ "Remove the USB cable from your MotorsBus and press Enter when done.";
86
+ await waitForInput(disconnectPrompt);
87
+
88
+ // Get port list after disconnect
89
+ const portsAfter = await findAvailablePorts();
90
+
91
+ // Find the difference
92
+ const portsDiff = portsBefore.filter((port) => !portsAfter.includes(port));
93
+
94
+ if (portsDiff.length === 1) {
95
+ const detectedPort = portsDiff[0];
96
+
97
+ // Show empty line then the result (Python lerobot style)
98
+ if (onMessage) {
99
+ onMessage("");
100
+ onMessage(`The port of this MotorsBus is '${detectedPort}'`);
101
+ onMessage("Reconnect the USB cable.");
102
+ } else {
103
+ console.log("");
104
+ console.log(`The port of this MotorsBus is '${detectedPort}'`);
105
+ console.log("Reconnect the USB cable.");
106
+ }
107
+
108
+ return detectedPort;
109
+ } else if (portsDiff.length === 0) {
110
+ throw new Error(
111
+ "No port difference detected. Please check cable connection."
112
+ );
113
+ } else {
114
+ throw new Error(
115
+ `Multiple ports detected: ${portsDiff.join(
116
+ ", "
117
+ )}. Please disconnect other devices.`
118
+ );
119
+ }
120
+ } finally {
121
+ rl.close();
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Create robot connection directly from specified port (Python lerobot style)
127
+ */
128
+ async function connectToSpecificPort(
129
+ portPath: string,
130
+ robotType: string,
131
+ robotId: string
132
+ ): Promise<RobotConnection> {
133
+ console.log(chalk.gray(`📡 Connecting to ${portPath}...`));
134
+
135
+ const connection = await connectPort(portPath);
136
+
137
+ if (!connection.isConnected) {
138
+ throw new Error(
139
+ `Failed to connect to port ${portPath}: ${connection.error}`
140
+ );
141
+ }
142
+
143
+ // Configure the robot with CLI parameters
144
+ connection.robotType = robotType;
145
+ connection.robotId = robotId;
146
+ connection.name = `${robotType} on ${portPath}`;
147
+
148
+ console.log(chalk.green(`✅ Connected to ${robotType} on ${portPath}`));
149
+ return connection;
150
+ }
151
+
152
+ /**
153
+ * Find port command - matches Python lerobot CLI exactly
154
+ * Always interactive by default (like Python lerobot)
155
+ */
156
+ program
157
+ .command("find-port")
158
+ .description(
159
+ "Find robot port with interactive cable detection (Python lerobot compatible)"
160
+ )
161
+ .action(async () => {
162
+ try {
163
+ console.log(chalk.blue("🔍 Finding robot port..."));
164
+
165
+ // Always use interactive cable detection (Python lerobot behavior)
166
+ await detectPortInteractive((message) =>
167
+ console.log(chalk.gray(message))
168
+ );
169
+ // No additional success message - detectPortInteractive already shows the result
170
+ } catch (error) {
171
+ console.error(
172
+ chalk.red(`❌ Error: ${error instanceof Error ? error.message : error}`)
173
+ );
174
+ process.exit(1);
175
+ }
176
+ });
177
+
178
+ /**
179
+ * Calibrate command - matches Python lerobot exactly
180
+ */
181
+ program
182
+ .command("calibrate")
183
+ .description("Calibrate robot motors")
184
+ .requiredOption("--robot.type <type>", "Robot type (e.g., so100_follower)")
185
+ .requiredOption(
186
+ "--robot.port <port>",
187
+ "Serial port (e.g., /dev/ttyUSB0, COM4)"
188
+ )
189
+ .option("--robot.id <id>", "Robot ID", "default")
190
+ .option("--output <path>", "Output calibration file path")
191
+ .action(async (options) => {
192
+ try {
193
+ const robotType = options["robot.type"];
194
+ const robotPort = options["robot.port"];
195
+ const robotId = options["robot.id"] || "default";
196
+
197
+ console.log(chalk.blue(`🔧 Starting calibration for ${robotType}...`));
198
+
199
+ // Step 1: Connect directly to specified port (Python lerobot style)
200
+ const robot = await connectToSpecificPort(robotPort, robotType, robotId);
201
+
202
+ // Step 2: Release motors
203
+ console.log(chalk.gray("🔓 Releasing motors for calibration setup..."));
204
+ await releaseMotors(robot);
205
+ console.log(
206
+ chalk.green("✅ Motors released - robot can now be moved by hand")
207
+ );
208
+
209
+ // Step 3: Wait for user to position robot
210
+ console.log(
211
+ chalk.yellow(
212
+ "\n📍 Move robot to your preferred starting position, then press Enter..."
213
+ )
214
+ );
215
+ const rl = createInterface({
216
+ input: process.stdin,
217
+ output: process.stdout,
218
+ });
219
+
220
+ await new Promise<void>((resolve) => {
221
+ rl.question("", () => {
222
+ rl.close();
223
+ resolve();
224
+ });
225
+ });
226
+
227
+ console.log(chalk.blue("\n🎯 Starting calibration process..."));
228
+ const calibrationProcess = await calibrate({
229
+ robot,
230
+ outputPath: options.output,
231
+ onProgress: (message) => console.log(chalk.gray(message)),
232
+ onLiveUpdate: (data) => {
233
+ // Clear previous output and display live data as table
234
+ process.stdout.write("\x1B[2J\x1B[0f"); // Clear screen and move cursor to top
235
+
236
+ console.log(chalk.cyan("📊 Live Motor Data:"));
237
+ console.log(
238
+ "┌─────────────────┬─────────┬─────────┬─────────┬─────────┐"
239
+ );
240
+ console.log(
241
+ "│ Motor │ Current │ Min │ Max │ Range │"
242
+ );
243
+ console.log(
244
+ "├─────────────────┼─────────┼─────────┼─────────┼─────────┤"
245
+ );
246
+
247
+ Object.entries(data).forEach(([name, info]) => {
248
+ const motorName = name.padEnd(15);
249
+ const current = info.current.toString().padStart(7);
250
+ const min = info.min.toString().padStart(7);
251
+ const max = info.max.toString().padStart(7);
252
+ const range = info.range.toString().padStart(7);
253
+ console.log(
254
+ `│ ${motorName} │ ${current} │ ${min} │ ${max} │ ${range} │`
255
+ );
256
+ });
257
+
258
+ console.log(
259
+ "└─────────────────┴─────────┴─────────┴─────────┴─────────┘"
260
+ );
261
+ console.log(
262
+ chalk.yellow(
263
+ "Move motors through full range, then press Enter when done..."
264
+ )
265
+ );
266
+ },
267
+ });
268
+
269
+ const results = await calibrationProcess.result;
270
+ console.log(chalk.green("\n✅ Calibration completed successfully!"));
271
+
272
+ // CRITICAL: Close robot connection to allow process to exit
273
+ if (robot.port && robot.port.close) {
274
+ await robot.port.close();
275
+ }
276
+ } catch (error) {
277
+ console.error(
278
+ chalk.red(
279
+ `❌ Calibration failed: ${
280
+ error instanceof Error ? error.message : error
281
+ }`
282
+ )
283
+ );
284
+
285
+ // Close robot connection even on error
286
+ try {
287
+ if (robot && robot.port && robot.port.close) {
288
+ await robot.port.close();
289
+ }
290
+ } catch (closeError) {
291
+ // Ignore close errors
292
+ }
293
+
294
+ process.exit(1);
295
+ }
296
+ });
297
+
298
+ /**
299
+ * Teleoperate command - matches Python lerobot exactly
300
+ */
301
+ program
302
+ .command("teleoperate")
303
+ .alias("teleop")
304
+ .description("Control robot through teleoperation")
305
+ .requiredOption("--robot.type <type>", "Robot type (e.g., so100_follower)")
306
+ .requiredOption(
307
+ "--robot.port <port>",
308
+ "Serial port (e.g., /dev/ttyUSB0, COM4)"
309
+ )
310
+ .option("--robot.id <id>", "Robot ID", "default")
311
+ .option("--teleop.type <type>", "Teleoperator type", "keyboard")
312
+ .option("--duration <seconds>", "Duration in seconds (0 = unlimited)", "0")
313
+ .action(async (options) => {
314
+ try {
315
+ const robotType = options["robot.type"];
316
+ const robotPort = options["robot.port"];
317
+ const robotId = options["robot.id"] || "default";
318
+ const teleopType = options["teleop.type"] || "keyboard";
319
+
320
+ console.log(chalk.blue(`🎮 Starting teleoperation for ${robotType}...`));
321
+
322
+ // Connect directly to specified port (Python lerobot style)
323
+ const robot = await connectToSpecificPort(robotPort, robotType, robotId);
324
+
325
+ const teleoperationProcess = await teleoperate({
326
+ robot,
327
+ teleop: {
328
+ type: teleopType,
329
+ },
330
+ onStateUpdate: (state) => {
331
+ if (state.isActive) {
332
+ const motorInfo = state.motorConfigs
333
+ .map(
334
+ (motor) => `${motor.name}:${Math.round(motor.currentPosition)}`
335
+ )
336
+ .join(" ");
337
+ process.stdout.write(`\r${chalk.cyan("🤖 Motors:")} ${motorInfo}`);
338
+ }
339
+ },
340
+ });
341
+
342
+ // Start teleoperation
343
+ teleoperationProcess.start();
344
+
345
+ // Handle duration limit
346
+ const duration = parseInt(options.duration || "0");
347
+ if (duration > 0) {
348
+ setTimeout(() => {
349
+ console.log(
350
+ chalk.yellow(
351
+ `\n⏰ Duration limit reached (${duration}s). Stopping...`
352
+ )
353
+ );
354
+ teleoperationProcess.stop();
355
+ process.exit(0);
356
+ }, duration * 1000);
357
+ }
358
+
359
+ // Handle process termination
360
+ process.on("SIGINT", async () => {
361
+ console.log(chalk.yellow("\n🛑 Stopping teleoperation..."));
362
+ teleoperationProcess.stop();
363
+ await teleoperationProcess.disconnect();
364
+ process.exit(0);
365
+ });
366
+
367
+ console.log(chalk.green("✅ Teleoperation started successfully!"));
368
+ console.log(chalk.gray("Press Ctrl+C to stop"));
369
+ } catch (error) {
370
+ console.error(
371
+ chalk.red(
372
+ `❌ Teleoperation failed: ${
373
+ error instanceof Error ? error.message : error
374
+ }`
375
+ )
376
+ );
377
+ process.exit(1);
378
+ }
379
+ });
380
+
381
+ /**
382
+ * Release motors command
383
+ */
384
+ program
385
+ .command("release-motors")
386
+ .description("Release robot motors for manual movement")
387
+ .requiredOption("--robot.type <type>", "Robot type (e.g., so100_follower)")
388
+ .requiredOption(
389
+ "--robot.port <port>",
390
+ "Serial port (e.g., /dev/ttyUSB0, COM4)"
391
+ )
392
+ .option("--robot.id <id>", "Robot ID", "default")
393
+ .option("--motors <ids>", "Specific motor IDs to release (comma-separated)")
394
+ .action(async (options) => {
395
+ try {
396
+ const robotType = options["robot.type"];
397
+ const robotPort = options["robot.port"];
398
+ const robotId = options["robot.id"] || "default";
399
+
400
+ console.log(chalk.blue(`🔓 Releasing motors for ${robotType}...`));
401
+
402
+ // Connect directly to specified port (Python lerobot style)
403
+ const robot = await connectToSpecificPort(robotPort, robotType, robotId);
404
+
405
+ const motorIds = options.motors
406
+ ? options.motors.split(",").map((id: string) => parseInt(id.trim()))
407
+ : undefined;
408
+
409
+ await releaseMotors(robot, motorIds);
410
+
411
+ console.log(chalk.green("✅ Motors released successfully!"));
412
+ console.log(chalk.gray("Motors can now be moved freely by hand."));
413
+ } catch (error) {
414
+ console.error(
415
+ chalk.red(
416
+ `❌ Failed to release motors: ${
417
+ error instanceof Error ? error.message : error
418
+ }`
419
+ )
420
+ );
421
+ process.exit(1);
422
+ }
423
+ });
424
+
425
+ /**
426
+ * Version and help setup
427
+ */
428
+ program
429
+ .name("lerobot")
430
+ .description("Node.js robotics control CLI - Python lerobot compatible")
431
+ .version("0.1.0");
432
+
433
+ /**
434
+ * Parse CLI arguments and run
435
+ */
436
+ program.parse();
437
+
438
+ // Show help if no command provided
439
+ if (!process.argv.slice(2).length) {
440
+ program.outputHelp();
441
+ }
packages/cli/src/index.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @lerobot/cli - Python lerobot compatible CLI commands
3
+ *
4
+ * This package provides CLI commands that match Python lerobot exactly:
5
+ * - lerobot find-port (with interactive cable detection)
6
+ * - lerobot calibrate
7
+ * - lerobot teleoperate
8
+ * - lerobot release-motors
9
+ *
10
+ * Uses @lerobot/node library for core functionality
11
+ */
12
+
13
+ export * from "./cli.js";
packages/cli/tsconfig.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "outDir": "./dist",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "removeComments": true,
11
+ "noEmit": false,
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "types": ["node"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["src/**/*.test.ts", "dist", "node_modules"]
20
+ }
packages/cli/vite.config.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({
4
+ build: {
5
+ target: "node18",
6
+ lib: {
7
+ entry: {
8
+ cli: "src/cli.ts",
9
+ index: "src/index.ts",
10
+ },
11
+ formats: ["es"],
12
+ },
13
+ rollupOptions: {
14
+ external: [
15
+ // Node.js built-ins
16
+ "fs",
17
+ "fs/promises",
18
+ "path",
19
+ "os",
20
+ "readline",
21
+ "util",
22
+ "events",
23
+ "stream",
24
+ // External dependencies
25
+ "@lerobot/node",
26
+ "chalk",
27
+ "commander",
28
+ "serialport",
29
+ ],
30
+ },
31
+ minify: false,
32
+ sourcemap: true,
33
+ },
34
+ });
packages/node/.eslintrc.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "root": true,
3
+ "parser": "@typescript-eslint/parser",
4
+ "plugins": ["@typescript-eslint"],
5
+ "extends": ["eslint:recommended", "@typescript-eslint/recommended"],
6
+ "parserOptions": {
7
+ "ecmaVersion": 2022,
8
+ "sourceType": "module"
9
+ },
10
+ "env": {
11
+ "node": true,
12
+ "es2022": true
13
+ },
14
+ "rules": {
15
+ "@typescript-eslint/no-unused-vars": [
16
+ "error",
17
+ { "argsIgnorePattern": "^_" }
18
+ ],
19
+ "@typescript-eslint/no-explicit-any": "warn",
20
+ "@typescript-eslint/no-non-null-assertion": "warn"
21
+ },
22
+ "ignorePatterns": ["dist/", "node_modules/"]
23
+ }
packages/node/.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build outputs
5
+ dist/
6
+ *.tsbuildinfo
7
+
8
+ # Environment files
9
+ .env
10
+ .env.local
11
+ .env.production
12
+
13
+ # IDE files
14
+ .vscode/
15
+ .idea/
16
+ *.swp
17
+ *.swo
18
+
19
+ # OS files
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # Test coverage
24
+ coverage/
25
+
26
+ # Logs
27
+ *.log
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+
32
+ # Runtime data
33
+ pids
34
+ *.pid
35
+ *.seed
36
+ *.pid.lock
packages/node/README.md ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @lerobot/node
2
+
3
+ Control robots with Node.js (serialport), inspired by [LeRobot](https://github.com/huggingface/lerobot)
4
+
5
+ 🚀 **[Try the live (web) demo →](https://huggingface.co/spaces/NERDDISCO/LeRobot.js)**
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # pnpm
11
+ pnpm add @lerobot/node
12
+
13
+ # npm
14
+ npm install @lerobot/node
15
+
16
+ # yarn
17
+ yarn add @lerobot/node
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import {
24
+ findPort,
25
+ connectPort,
26
+ releaseMotors,
27
+ calibrate,
28
+ teleoperate,
29
+ } from "@lerobot/node";
30
+
31
+ // 1. find available robot ports
32
+ console.log("🔍 finding available robot ports...");
33
+ const findProcess = await findPort();
34
+ const robots = await findProcess.result;
35
+
36
+ if (robots.length === 0) {
37
+ console.log("❌ no robots found. check connections.");
38
+ process.exit(1);
39
+ }
40
+
41
+ // 2. connect to the first robot found
42
+ console.log(`✅ found ${robots.length} robot(s). connecting to first one...`);
43
+ const robot = await connectPort(robots[0].path, robots[0].robotType);
44
+
45
+ // 3. release motors for manual positioning
46
+ console.log("🔓 releasing motors for manual positioning...");
47
+ await releaseMotors(robot);
48
+
49
+ // 4. calibrate motors by moving through full range
50
+ console.log("⚙️ starting calibration...");
51
+ const calibrationProcess = await calibrate({
52
+ robot,
53
+ onProgress: (message) => console.log(message),
54
+ onLiveUpdate: (data) => console.log("live positions:", data),
55
+ });
56
+
57
+ // move robot through its range, then stop calibration
58
+ console.log("👋 move robot through full range, press enter when done...");
59
+ process.stdin.once("data", () => {
60
+ calibrationProcess.stop();
61
+ });
62
+
63
+ const calibrationData = await calibrationProcess.result;
64
+ console.log("✅ calibration complete!");
65
+
66
+ // 5. control robot with keyboard
67
+ console.log("🎮 starting keyboard control...");
68
+ const teleop = await teleoperate({
69
+ robot,
70
+ calibrationData,
71
+ teleop: { type: "keyboard" },
72
+ });
73
+ teleop.start();
74
+
75
+ // stop control after 30 seconds
76
+ setTimeout(() => {
77
+ teleop.stop();
78
+ console.log("🛑 control stopped");
79
+ }, 30000);
80
+ ```
81
+
82
+ ## How It Works
83
+
84
+ ### **Beginner Flow: `findPort()` → `connectPort()` → Use Robot**
85
+
86
+ Most users should start with `findPort()` for discovery, then `connectPort()` for connection:
87
+
88
+ ```typescript
89
+ // ✅ recommended: discover then connect
90
+ const findProcess = await findPort();
91
+ const robots = await findProcess.result;
92
+ const robot = await connectPort(robots[0].path, robots[0].robotType);
93
+ ```
94
+
95
+ ### **Advanced: Direct Connection with `connectPort()`**
96
+
97
+ Only use `connectPort()` when you already know the exact port:
98
+
99
+ ```typescript
100
+ // ⚡ advanced: direct connection to known port
101
+ const robot = await connectPort("/dev/ttyUSB0", "so100_follower");
102
+ ```
103
+
104
+ ## Core API
105
+
106
+ ### `findPort(config?): Promise<FindPortProcess>`
107
+
108
+ Discovers available robotics hardware on serial ports. Unlike the web version, this only discovers ports - connection happens separately with `connectPort()`.
109
+
110
+ ```typescript
111
+ // Discover all available robots
112
+ const findProcess = await findPort();
113
+ const robots = await findProcess.result;
114
+
115
+ console.log(`Found ${robots.length} robot(s):`);
116
+ robots.forEach((robot) => {
117
+ console.log(`- ${robot.robotType} on ${robot.path}`);
118
+ });
119
+
120
+ // Connect to specific robot
121
+ const robot = await connectPort(robots[0].path, robots[0].robotType);
122
+ ```
123
+
124
+ #### Options
125
+
126
+ - `config?: FindPortConfig` - Optional configuration
127
+ - `onMessage?: (message: string) => void` - Progress messages callback
128
+
129
+ #### Returns: `FindPortProcess`
130
+
131
+ - `result: Promise<DiscoveredRobot[]>` - Array of discovered robots with `path`, `robotType`, and other metadata
132
+ - `stop(): void` - Cancel discovery process
133
+
134
+ #### DiscoveredRobot Structure
135
+
136
+ ```typescript
137
+ interface DiscoveredRobot {
138
+ path: string; // Serial port path (e.g., "/dev/ttyUSB0")
139
+ robotType: "so100_follower" | "so100_leader";
140
+ // Additional metadata...
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ### `connectPort(port): Promise<RobotConnection>`
147
+
148
+ Creates a connection to a robot on the specified serial port.
149
+
150
+ ```typescript
151
+ // Connect to SO-100 follower arm
152
+ const robot = await connectPort(
153
+ "/dev/ttyUSB0", // Serial port path
154
+ "so100_follower", // Robot type
155
+ "my_robot_arm" // Custom robot ID
156
+ );
157
+
158
+ // Windows
159
+ const robot = await connectPort("COM4", "so100_follower", "my_robot");
160
+
161
+ // Connection is ready to use
162
+ console.log(`Connected to ${robot.robotType} on ${robot.port.path}`);
163
+ ```
164
+
165
+ #### Parameters
166
+
167
+ - `port: string` - Serial port path (e.g., `/dev/ttyUSB0`, `COM4`)
168
+ - `robotType: "so100_follower" | "so100_leader"` - Type of robot
169
+ - `robotId: string` - Custom identifier for your robot
170
+
171
+ #### Returns: `Promise<RobotConnection>`
172
+
173
+ - Initialized robot connection ready for calibration/teleoperation
174
+ - Includes configured motor IDs, keyboard controls, and hardware settings
175
+
176
+ ---
177
+
178
+ ### `calibrate(config): Promise<CalibrationProcess>`
179
+
180
+ Calibrates motor homing offsets and records range of motion. **Identical to Python lerobot behavior.**
181
+
182
+ ```typescript
183
+ const calibrationProcess = await calibrate({
184
+ robot,
185
+ onProgress: (message) => {
186
+ console.log(message); // "⚙️ Setting motor homing offsets"
187
+ },
188
+ onLiveUpdate: (data) => {
189
+ // Real-time motor positions during range recording
190
+ Object.entries(data).forEach(([motor, info]) => {
191
+ console.log(`${motor}: ${info.current} (range: ${info.range})`);
192
+ });
193
+ },
194
+ });
195
+
196
+ // Move robot through full range of motion...
197
+ // When finished, stop calibration
198
+ calibrationProcess.stop();
199
+
200
+ const calibrationData = await calibrationProcess.result;
201
+
202
+ // Save to file (Python-compatible format)
203
+ import { writeFileSync } from "fs";
204
+ writeFileSync(
205
+ "./my_robot_calibration.json",
206
+ JSON.stringify(calibrationData, null, 2)
207
+ );
208
+ ```
209
+
210
+ #### Options
211
+
212
+ - `config: CalibrateConfig`
213
+ - `robot: RobotConnection` - Connected robot from `connectPort()`
214
+ - `onProgress?: (message: string) => void` - Progress messages
215
+ - `onLiveUpdate?: (data: LiveCalibrationData) => void` - Real-time position updates
216
+
217
+ #### Returns: `CalibrationProcess`
218
+
219
+ - `result: Promise<CalibrationResults>` - **Python-compatible** calibration data
220
+ - `stop(): void` - Stop calibration process
221
+
222
+ #### Calibration Data Format
223
+
224
+ **Python Compatible**: This format is identical to Python lerobot calibration files - you can use the same calibration data across both implementations.
225
+
226
+ ```json
227
+ {
228
+ "shoulder_pan": {
229
+ "id": 1,
230
+ "drive_mode": 0,
231
+ "homing_offset": 14,
232
+ "range_min": 1015,
233
+ "range_max": 3128
234
+ },
235
+ "shoulder_lift": {
236
+ "id": 2,
237
+ "drive_mode": 0,
238
+ "homing_offset": 989,
239
+ "range_min": 965,
240
+ "range_max": 3265
241
+ },
242
+ "elbow_flex": {
243
+ "id": 3,
244
+ "drive_mode": 0,
245
+ "homing_offset": -879,
246
+ "range_min": 820,
247
+ "range_max": 3051
248
+ },
249
+ "wrist_flex": {
250
+ "id": 4,
251
+ "drive_mode": 0,
252
+ "homing_offset": 31,
253
+ "range_min": 758,
254
+ "range_max": 3277
255
+ },
256
+ "wrist_roll": {
257
+ "id": 5,
258
+ "drive_mode": 0,
259
+ "homing_offset": -37,
260
+ "range_min": 2046,
261
+ "range_max": 3171
262
+ },
263
+ "gripper": {
264
+ "id": 6,
265
+ "drive_mode": 0,
266
+ "homing_offset": -1173,
267
+ "range_min": 2038,
268
+ "range_max": 3528
269
+ }
270
+ }
271
+ ```
272
+
273
+ ---
274
+
275
+ ### `teleoperate(config): Promise<TeleoperationProcess>`
276
+
277
+ Real-time robot control with keyboard input. **Smooth, responsive movement** optimized for Node.js.
278
+
279
+ #### Keyboard Teleoperation
280
+
281
+ ```typescript
282
+ const teleop = await teleoperate({
283
+ robot,
284
+ teleop: { type: "keyboard" },
285
+ onStateUpdate: (state) => {
286
+ console.log(`Active: ${state.isActive}`);
287
+ state.motorConfigs.forEach((motor) => {
288
+ console.log(`${motor.name}: ${motor.currentPosition}`);
289
+ });
290
+ },
291
+ });
292
+
293
+ // Start keyboard control
294
+ teleop.start();
295
+
296
+ // Control will be active until stopped
297
+ setTimeout(() => teleop.stop(), 60000);
298
+ ```
299
+
300
+ #### Options
301
+
302
+ - `config: TeleoperateConfig`
303
+ - `robot: RobotConnection` - Connected robot
304
+ - `teleop: TeleoperatorConfig` - Teleoperator configuration:
305
+ - `{ type: "keyboard" }` - Keyboard control with optimized defaults
306
+ - `onStateUpdate?: (state: TeleoperationState) => void` - State change callback
307
+
308
+ #### Returns: `TeleoperationProcess`
309
+
310
+ - `start(): void` - Begin teleoperation (shows keyboard controls)
311
+ - `stop(): void` - Stop teleoperation
312
+ - `getState(): TeleoperationState` - Current state and motor positions
313
+
314
+ #### Keyboard Controls (SO-100)
315
+
316
+ ```
317
+ Arrow Keys: Shoulder pan/lift
318
+ WASD: Elbow flex, wrist flex
319
+ Q/E: Wrist roll
320
+ O/C: Gripper open/close
321
+ ESC: Emergency stop
322
+ Ctrl+C: Exit
323
+ ```
324
+
325
+ #### Performance Characteristics
326
+
327
+ - **120 Hz update rate** for smooth movement
328
+ - **Immediate response** on keypress (no delay)
329
+ - **8-unit step size** matching browser demo
330
+ - **150ms key timeout** for optimal single-tap vs hold behavior
331
+
332
+ ---
333
+
334
+ ### `releaseMotors(robot): Promise<void>`
335
+
336
+ Releases motor torque so robot can be moved freely by hand.
337
+
338
+ ```typescript
339
+ // Release all motors for calibration
340
+ await releaseMotors(robot);
341
+ console.log("Motors released - you can now move the robot freely");
342
+ ```
343
+
344
+ #### Parameters
345
+
346
+ - `robot: RobotConnection` - Connected robot
347
+
348
+ ---
349
+
350
+ ## CLI Usage
351
+
352
+ For command-line usage, install the CLI package:
353
+
354
+ ```bash
355
+ # Install CLI globally
356
+ pnpm add -g lerobot
357
+
358
+ # Find and connect to robot
359
+ npx lerobot find-port
360
+
361
+ # Calibrate robot
362
+ npx lerobot calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot
363
+
364
+ # Control robot with keyboard
365
+ npx lerobot teleoperate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot
366
+
367
+ # Release motors
368
+ npx lerobot release-motors --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot
369
+ ```
370
+
371
+ **CLI commands are identical to Python lerobot** - same syntax, same behavior, seamless migration.
372
+
373
+ ## Node.js Requirements
374
+
375
+ - **Node.js 18+**
376
+ - **Serial port access** (may require permissions on Linux/macOS)
377
+ - **Supported platforms**: Windows, macOS, Linux
378
+
379
+ ### Serial Port Permissions
380
+
381
+ **Linux/macOS:**
382
+
383
+ ```bash
384
+ # Add user to dialout group (Linux)
385
+ sudo usermod -a -G dialout $USER
386
+
387
+ # Set permissions (macOS)
388
+ sudo chmod 666 /dev/tty.usbserial-*
389
+ ```
390
+
391
+ **Windows:** No additional setup required.
392
+
393
+ ## Hardware Support
394
+
395
+ Currently supports SO-100 follower and leader arms with STS3215 motors. More devices coming soon.
396
+
397
+ ## Migration from lerobot.py
398
+
399
+ ```python
400
+ # Python lerobot
401
+ python -m lerobot.calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0
402
+
403
+ # Node.js equivalent
404
+ npx lerobot calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0
405
+ ```
406
+
407
+ - **Same commands** - just replace `python -m lerobot.` with `npx lerobot`
408
+ - **Same calibration files** - Python and Node.js calibrations are interchangeable
packages/node/package.json ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@lerobot/node",
3
+ "version": "0.1.0",
4
+ "description": "Node.js-based robotics control using SerialPort",
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
+ "serialport",
33
+ "hardware-control",
34
+ "nodejs",
35
+ "typescript"
36
+ ],
37
+ "scripts": {
38
+ "build": "vite build",
39
+ "dev": "vite build --watch",
40
+ "lint": "eslint src --ext .ts,.tsx --report-unused-disable-directives --max-warnings 0",
41
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
42
+ "prepublishOnly": "npm run build",
43
+ "test": "vitest run",
44
+ "test:ui": "vitest --ui",
45
+ "test:coverage": "vitest run --coverage"
46
+ },
47
+ "dependencies": {
48
+ "serialport": "^12.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^18.0.0",
52
+ "@typescript-eslint/eslint-plugin": "^8.41.0",
53
+ "@typescript-eslint/parser": "^8.41.0",
54
+ "@vitest/ui": "^2.0.0",
55
+ "eslint": "^9.34.0",
56
+ "typescript": "^5.3.0",
57
+ "vite": "^6.3.5",
58
+ "vite-node": "^2.0.0",
59
+ "vitest": "^2.0.0"
60
+ },
61
+ "peerDependencies": {
62
+ "typescript": ">=4.5.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ },
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "https://github.com/timpietrusky/lerobot.js/tree/main/packages/node"
70
+ },
71
+ "license": "Apache-2.0",
72
+ "author": "Tim Pietrusky",
73
+ "publishConfig": {
74
+ "access": "public"
75
+ }
76
+ }
packages/node/pnpm-lock.yaml ADDED
@@ -0,0 +1,2396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ serialport:
12
+ specifier: ^12.0.0
13
+ version: 12.0.0
14
+ devDependencies:
15
+ '@types/node':
16
+ specifier: ^18.0.0
17
+ version: 18.19.123
18
+ '@typescript-eslint/eslint-plugin':
19
+ specifier: ^8.41.0
20
21
+ '@typescript-eslint/parser':
22
+ specifier: ^8.41.0
23
24
+ '@vitest/ui':
25
+ specifier: ^2.0.0
26
+ version: 2.1.9([email protected])
27
+ eslint:
28
+ specifier: ^9.34.0
29
+ version: 9.34.0
30
+ typescript:
31
+ specifier: ^5.3.0
32
+ version: 5.8.3
33
+ vite:
34
+ specifier: ^6.3.5
35
+ version: 6.3.5(@types/[email protected])
36
+ vite-node:
37
+ specifier: ^2.0.0
38
+ version: 2.1.9(@types/[email protected])
39
+ vitest:
40
+ specifier: ^2.0.0
41
+ version: 2.1.9(@types/[email protected])(@vitest/[email protected])
42
+
43
+ packages:
44
+
45
+ '@esbuild/[email protected]':
46
+ resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
47
+ engines: {node: '>=12'}
48
+ cpu: [ppc64]
49
+ os: [aix]
50
+
51
+ '@esbuild/[email protected]':
52
+ resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
53
+ engines: {node: '>=18'}
54
+ cpu: [ppc64]
55
+ os: [aix]
56
+
57
+ '@esbuild/[email protected]':
58
+ resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
59
+ engines: {node: '>=12'}
60
+ cpu: [arm64]
61
+ os: [android]
62
+
63
+ '@esbuild/[email protected]':
64
+ resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==}
65
+ engines: {node: '>=18'}
66
+ cpu: [arm64]
67
+ os: [android]
68
+
69
+ '@esbuild/[email protected]':
70
+ resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
71
+ engines: {node: '>=12'}
72
+ cpu: [arm]
73
+ os: [android]
74
+
75
+ '@esbuild/[email protected]':
76
+ resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==}
77
+ engines: {node: '>=18'}
78
+ cpu: [arm]
79
+ os: [android]
80
+
81
+ '@esbuild/[email protected]':
82
+ resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
83
+ engines: {node: '>=12'}
84
+ cpu: [x64]
85
+ os: [android]
86
+
87
+ '@esbuild/[email protected]':
88
+ resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==}
89
+ engines: {node: '>=18'}
90
+ cpu: [x64]
91
+ os: [android]
92
+
93
+ '@esbuild/[email protected]':
94
+ resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
95
+ engines: {node: '>=12'}
96
+ cpu: [arm64]
97
+ os: [darwin]
98
+
99
+ '@esbuild/[email protected]':
100
+ resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==}
101
+ engines: {node: '>=18'}
102
+ cpu: [arm64]
103
+ os: [darwin]
104
+
105
+ '@esbuild/[email protected]':
106
+ resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
107
+ engines: {node: '>=12'}
108
+ cpu: [x64]
109
+ os: [darwin]
110
+
111
+ '@esbuild/[email protected]':
112
+ resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==}
113
+ engines: {node: '>=18'}
114
+ cpu: [x64]
115
+ os: [darwin]
116
+
117
+ '@esbuild/[email protected]':
118
+ resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
119
+ engines: {node: '>=12'}
120
+ cpu: [arm64]
121
+ os: [freebsd]
122
+
123
+ '@esbuild/[email protected]':
124
+ resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==}
125
+ engines: {node: '>=18'}
126
+ cpu: [arm64]
127
+ os: [freebsd]
128
+
129
+ '@esbuild/[email protected]':
130
+ resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
131
+ engines: {node: '>=12'}
132
+ cpu: [x64]
133
+ os: [freebsd]
134
+
135
+ '@esbuild/[email protected]':
136
+ resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==}
137
+ engines: {node: '>=18'}
138
+ cpu: [x64]
139
+ os: [freebsd]
140
+
141
+ '@esbuild/[email protected]':
142
+ resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
143
+ engines: {node: '>=12'}
144
+ cpu: [arm64]
145
+ os: [linux]
146
+
147
+ '@esbuild/[email protected]':
148
+ resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==}
149
+ engines: {node: '>=18'}
150
+ cpu: [arm64]
151
+ os: [linux]
152
+
153
+ '@esbuild/[email protected]':
154
+ resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
155
+ engines: {node: '>=12'}
156
+ cpu: [arm]
157
+ os: [linux]
158
+
159
+ '@esbuild/[email protected]':
160
+ resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==}
161
+ engines: {node: '>=18'}
162
+ cpu: [arm]
163
+ os: [linux]
164
+
165
+ '@esbuild/[email protected]':
166
+ resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
167
+ engines: {node: '>=12'}
168
+ cpu: [ia32]
169
+ os: [linux]
170
+
171
+ '@esbuild/[email protected]':
172
+ resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==}
173
+ engines: {node: '>=18'}
174
+ cpu: [ia32]
175
+ os: [linux]
176
+
177
+ '@esbuild/[email protected]':
178
+ resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
179
+ engines: {node: '>=12'}
180
+ cpu: [loong64]
181
+ os: [linux]
182
+
183
+ '@esbuild/[email protected]':
184
+ resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==}
185
+ engines: {node: '>=18'}
186
+ cpu: [loong64]
187
+ os: [linux]
188
+
189
+ '@esbuild/[email protected]':
190
+ resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
191
+ engines: {node: '>=12'}
192
+ cpu: [mips64el]
193
+ os: [linux]
194
+
195
+ '@esbuild/[email protected]':
196
+ resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==}
197
+ engines: {node: '>=18'}
198
+ cpu: [mips64el]
199
+ os: [linux]
200
+
201
+ '@esbuild/[email protected]':
202
+ resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
203
+ engines: {node: '>=12'}
204
+ cpu: [ppc64]
205
+ os: [linux]
206
+
207
+ '@esbuild/[email protected]':
208
+ resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==}
209
+ engines: {node: '>=18'}
210
+ cpu: [ppc64]
211
+ os: [linux]
212
+
213
+ '@esbuild/[email protected]':
214
+ resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
215
+ engines: {node: '>=12'}
216
+ cpu: [riscv64]
217
+ os: [linux]
218
+
219
+ '@esbuild/[email protected]':
220
+ resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==}
221
+ engines: {node: '>=18'}
222
+ cpu: [riscv64]
223
+ os: [linux]
224
+
225
+ '@esbuild/[email protected]':
226
+ resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
227
+ engines: {node: '>=12'}
228
+ cpu: [s390x]
229
+ os: [linux]
230
+
231
+ '@esbuild/[email protected]':
232
+ resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==}
233
+ engines: {node: '>=18'}
234
+ cpu: [s390x]
235
+ os: [linux]
236
+
237
+ '@esbuild/[email protected]':
238
+ resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
239
+ engines: {node: '>=12'}
240
+ cpu: [x64]
241
+ os: [linux]
242
+
243
+ '@esbuild/[email protected]':
244
+ resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==}
245
+ engines: {node: '>=18'}
246
+ cpu: [x64]
247
+ os: [linux]
248
+
249
+ '@esbuild/[email protected]':
250
+ resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==}
251
+ engines: {node: '>=18'}
252
+ cpu: [arm64]
253
+ os: [netbsd]
254
+
255
+ '@esbuild/[email protected]':
256
+ resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
257
+ engines: {node: '>=12'}
258
+ cpu: [x64]
259
+ os: [netbsd]
260
+
261
+ '@esbuild/[email protected]':
262
+ resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==}
263
+ engines: {node: '>=18'}
264
+ cpu: [x64]
265
+ os: [netbsd]
266
+
267
+ '@esbuild/[email protected]':
268
+ resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==}
269
+ engines: {node: '>=18'}
270
+ cpu: [arm64]
271
+ os: [openbsd]
272
+
273
+ '@esbuild/[email protected]':
274
+ resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
275
+ engines: {node: '>=12'}
276
+ cpu: [x64]
277
+ os: [openbsd]
278
+
279
+ '@esbuild/[email protected]':
280
+ resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==}
281
+ engines: {node: '>=18'}
282
+ cpu: [x64]
283
+ os: [openbsd]
284
+
285
+ '@esbuild/[email protected]':
286
+ resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==}
287
+ engines: {node: '>=18'}
288
+ cpu: [arm64]
289
+ os: [openharmony]
290
+
291
+ '@esbuild/[email protected]':
292
+ resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
293
+ engines: {node: '>=12'}
294
+ cpu: [x64]
295
+ os: [sunos]
296
+
297
+ '@esbuild/[email protected]':
298
+ resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==}
299
+ engines: {node: '>=18'}
300
+ cpu: [x64]
301
+ os: [sunos]
302
+
303
+ '@esbuild/[email protected]':
304
+ resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
305
+ engines: {node: '>=12'}
306
+ cpu: [arm64]
307
+ os: [win32]
308
+
309
+ '@esbuild/[email protected]':
310
+ resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==}
311
+ engines: {node: '>=18'}
312
+ cpu: [arm64]
313
+ os: [win32]
314
+
315
+ '@esbuild/[email protected]':
316
+ resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
317
+ engines: {node: '>=12'}
318
+ cpu: [ia32]
319
+ os: [win32]
320
+
321
+ '@esbuild/[email protected]':
322
+ resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==}
323
+ engines: {node: '>=18'}
324
+ cpu: [ia32]
325
+ os: [win32]
326
+
327
+ '@esbuild/[email protected]':
328
+ resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
329
+ engines: {node: '>=12'}
330
+ cpu: [x64]
331
+ os: [win32]
332
+
333
+ '@esbuild/[email protected]':
334
+ resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==}
335
+ engines: {node: '>=18'}
336
+ cpu: [x64]
337
+ os: [win32]
338
+
339
+ '@eslint-community/[email protected]':
340
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
341
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
342
+ peerDependencies:
343
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
344
+
345
+ '@eslint-community/[email protected]':
346
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
347
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
348
+
349
+ '@eslint/[email protected]':
350
+ resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
351
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
352
+
353
+ '@eslint/[email protected]':
354
+ resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
355
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
356
+
357
+ '@eslint/[email protected]':
358
+ resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
359
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
360
+
361
+ '@eslint/[email protected]':
362
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
363
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
364
+
365
+ '@eslint/[email protected]':
366
+ resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==}
367
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
368
+
369
+ '@eslint/[email protected]':
370
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
371
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
372
+
373
+ '@eslint/[email protected]':
374
+ resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
375
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
376
+
377
+ '@humanfs/[email protected]':
378
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
379
+ engines: {node: '>=18.18.0'}
380
+
381
+ '@humanfs/[email protected]':
382
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
383
+ engines: {node: '>=18.18.0'}
384
+
385
+ '@humanwhocodes/[email protected]':
386
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
387
+ engines: {node: '>=12.22'}
388
+
389
+ '@humanwhocodes/[email protected]':
390
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
391
+ engines: {node: '>=18.18'}
392
+
393
+ '@humanwhocodes/[email protected]':
394
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
395
+ engines: {node: '>=18.18'}
396
+
397
+ '@jridgewell/[email protected]':
398
+ resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
399
+
400
+ '@nodelib/[email protected]':
401
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
402
+ engines: {node: '>= 8'}
403
+
404
+ '@nodelib/[email protected]':
405
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
406
+ engines: {node: '>= 8'}
407
+
408
+ '@nodelib/[email protected]':
409
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
410
+ engines: {node: '>= 8'}
411
+
412
+ '@polka/[email protected]':
413
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
414
+
415
+ '@rollup/[email protected]':
416
+ resolution: {integrity: sha512-9f3nSTFI2ivfxc7/tHBHcJ8pRnp8ROrELvsVprlQPVvcZ+j5zztYd+PTJGpyIOAdTvNwNrpCXswKSeoQcyGjMQ==}
417
+ cpu: [arm]
418
+ os: [android]
419
+
420
+ '@rollup/[email protected]':
421
+ resolution: {integrity: sha512-tFZSEhqJ8Yrpe50TzOdeoYi72gi/jsnT7y8Qrozf3cNu28WX+s6I3XzEPUAqoaT9SAS8Xz9AzGTFlxxCH/w20w==}
422
+ cpu: [arm64]
423
+ os: [android]
424
+
425
+ '@rollup/[email protected]':
426
+ resolution: {integrity: sha512-+DikIIs+p6yU2hF51UaWG8BnHbq90X0QIOt5zqSKSZxY+G3qqdLih214e9InJal21af2PuuxkDectetGfbVPJw==}
427
+ cpu: [arm64]
428
+ os: [darwin]
429
+
430
+ '@rollup/[email protected]':
431
+ resolution: {integrity: sha512-5a+NofhdEB/WimSlFMskbFQn1vqz1FWryYpA99trmZGO6qEmiS0IsX6w4B3d91U878Q2ZQdiaFF1gxX4P147og==}
432
+ cpu: [x64]
433
+ os: [darwin]
434
+
435
+ '@rollup/[email protected]':
436
+ resolution: {integrity: sha512-igr/RlKPS3OCy4jD3XBmAmo3UAcNZkJSubRsw1JeM8bAbwf15k/3eMZXD91bnjheijJiOJcga3kfCLKjV8IXNg==}
437
+ cpu: [arm64]
438
+ os: [freebsd]
439
+
440
+ '@rollup/[email protected]':
441
+ resolution: {integrity: sha512-MdigWzPSHlQzB1xZ+MdFDWTAH+kcn7UxjEBoOKuaso7z1DRlnAnrknB1mTtNOQ+GdPI8xgExAGwHeqQjntR0Cg==}
442
+ cpu: [x64]
443
+ os: [freebsd]
444
+
445
+ '@rollup/[email protected]':
446
+ resolution: {integrity: sha512-dmZseE0ZwA/4yy1+BwFrDqFTjjNg24GO9xSrb1weVbt6AFkhp5pz1gVS7IMtfIvoWy8yp6q/zN0bKnefRUImvQ==}
447
+ cpu: [arm]
448
+ os: [linux]
449
+
450
+ '@rollup/[email protected]':
451
+ resolution: {integrity: sha512-fzhfn6p9Cfm3W8UrWKIa4l7Wfjs/KGdgaswMBBE3KY3Ta43jg2XsPrAtfezHpsRk0Nx+TFuS3hZk/To2N5kFPQ==}
452
+ cpu: [arm]
453
+ os: [linux]
454
+
455
+ '@rollup/[email protected]':
456
+ resolution: {integrity: sha512-vVDD+iPDPmJQ5nAQ5Tifq3ywdv60FartglFI8VOCK+hcU9aoG0qlQTsDJP97O5yiTaTqlneZWoARMcVC5nyUoQ==}
457
+ cpu: [arm64]
458
+ os: [linux]
459
+
460
+ '@rollup/[email protected]':
461
+ resolution: {integrity: sha512-0d0jx08fzDHCzXqrtCMEEyxKU0SvJrWmUjUDE2/KDQ2UDJql0tfiwYvEx1oHELClKO8CNdE+AGJj+RqXscZpdQ==}
462
+ cpu: [arm64]
463
+ os: [linux]
464
+
465
+ '@rollup/[email protected]':
466
+ resolution: {integrity: sha512-XBYu9oW9eKJadWn8M7hkTZsD4yG+RrsTrVEgyKwb4L72cpJjRbRboTG9Lg9fec8MxJp/cfTHAocg4mnismQR8A==}
467
+ cpu: [loong64]
468
+ os: [linux]
469
+
470
+ '@rollup/[email protected]':
471
+ resolution: {integrity: sha512-wJaRvcT17PoOK6Ggcfo3nouFlybHvARBS4jzT0PC/lg17fIJHcDS2fZz3sD+iA4nRlho2zE6OGbU0HvwATdokQ==}
472
+ cpu: [ppc64]
473
+ os: [linux]
474
+
475
+ '@rollup/[email protected]':
476
+ resolution: {integrity: sha512-GZ5bkMFteAGkcmh8x0Ok4LSa+L62Ez0tMsHPX6JtR0wl4Xc3bQcrFHDiR5DGLEDFtGrXih4Nd/UDaFqs968/wA==}
477
+ cpu: [riscv64]
478
+ os: [linux]
479
+
480
+ '@rollup/[email protected]':
481
+ resolution: {integrity: sha512-7CjPw6FflFsVOUfWOrVrREiV3IYXG4RzZ1ZQUaT3BtSK8YXN6x286o+sruPZJESIaPebYuFowmg54ZdrkVBYog==}
482
+ cpu: [riscv64]
483
+ os: [linux]
484
+
485
+ '@rollup/[email protected]':
486
+ resolution: {integrity: sha512-nmvnl0ZiuysltcB/cKjUh40Rx4FbSyueERDsl2FLvLYr6pCgSsvGr3SocUT84svSpmloS7f1DRWqtRha74Gi1w==}
487
+ cpu: [s390x]
488
+ os: [linux]
489
+
490
+ '@rollup/[email protected]':
491
+ resolution: {integrity: sha512-Cv+moII5C8RM6gZbR3cb21o6rquVDZrN2o81maROg1LFzBz2dZUwIQSxFA8GtGZ/F2KtsqQ2z3eFPBb6akvQNg==}
492
+ cpu: [x64]
493
+ os: [linux]
494
+
495
+ '@rollup/[email protected]':
496
+ resolution: {integrity: sha512-PHcMG8DZTM9RCIjp8QIfN0VYtX0TtBPnWOTRurFhoCDoi9zptUZL2k7pCs+5rgut7JAiUsYy+huyhVKPcmxoog==}
497
+ cpu: [x64]
498
+ os: [linux]
499
+
500
+ '@rollup/[email protected]':
501
+ resolution: {integrity: sha512-1SI/Rd47e8aQJeFWMDg16ET+fjvCcD/CzeaRmIEPmb05hx+3cCcwIF4ebUag4yTt/D1peE+Mgp0+Po3M358cAA==}
502
+ cpu: [arm64]
503
+ os: [win32]
504
+
505
+ '@rollup/[email protected]':
506
+ resolution: {integrity: sha512-JwOCYxmumFDfDhx4kNyz6kTVK3gWzBIvVdMNzQMRDubcoGRDniOOmo6DDNP42qwZx3Bp9/6vWJ+kNzNqXoHmeA==}
507
+ cpu: [ia32]
508
+ os: [win32]
509
+
510
+ '@rollup/[email protected]':
511
+ resolution: {integrity: sha512-IPMIfrfkG1GaEXi+JSsQEx8x9b4b+hRZXO7KYc2pKio3zO2/VDXDs6B9Ts/nnO+25Fk1tdAVtUn60HKKPPzDig==}
512
+ cpu: [x64]
513
+ os: [win32]
514
+
515
+ '@serialport/[email protected]':
516
+ resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==}
517
+ engines: {node: '>=12.0.0'}
518
+
519
+ '@serialport/[email protected]':
520
+ resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==}
521
+ engines: {node: '>=16.0.0'}
522
+
523
+ '@serialport/[email protected]':
524
+ resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==}
525
+ engines: {node: ^12.22 || ^14.13 || >=16}
526
+
527
+ '@serialport/[email protected]':
528
+ resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==}
529
+ engines: {node: '>=12.0.0'}
530
+
531
+ '@serialport/[email protected]':
532
+ resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==}
533
+ engines: {node: '>=12.0.0'}
534
+
535
+ '@serialport/[email protected]':
536
+ resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==}
537
+ engines: {node: '>=12.0.0'}
538
+
539
+ '@serialport/[email protected]':
540
+ resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==}
541
+ engines: {node: '>=12.0.0'}
542
+
543
+ '@serialport/[email protected]':
544
+ resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==}
545
+ engines: {node: '>=12.0.0'}
546
+
547
+ '@serialport/[email protected]':
548
+ resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==}
549
+ engines: {node: '>=8.6.0'}
550
+
551
+ '@serialport/[email protected]':
552
+ resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==}
553
+ engines: {node: '>=12.0.0'}
554
+
555
+ '@serialport/[email protected]':
556
+ resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==}
557
+ engines: {node: '>=12.0.0'}
558
+
559
+ '@serialport/[email protected]':
560
+ resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==}
561
+ engines: {node: '>=12.0.0'}
562
+
563
+ '@serialport/[email protected]':
564
+ resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==}
565
+ engines: {node: '>=12.0.0'}
566
+
567
+ '@serialport/[email protected]':
568
+ resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==}
569
+ engines: {node: '>=12.0.0'}
570
+
571
+ '@serialport/[email protected]':
572
+ resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==}
573
+ engines: {node: '>=12.0.0'}
574
+
575
+ '@serialport/[email protected]':
576
+ resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==}
577
+ engines: {node: '>=12.0.0'}
578
+
579
+ '@types/[email protected]':
580
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
581
+
582
+ '@types/[email protected]':
583
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
584
+
585
+ '@types/[email protected]':
586
+ resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==}
587
+
588
+ '@typescript-eslint/[email protected]':
589
+ resolution: {integrity: sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==}
590
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
591
+ peerDependencies:
592
+ '@typescript-eslint/parser': ^8.41.0
593
+ eslint: ^8.57.0 || ^9.0.0
594
+ typescript: '>=4.8.4 <6.0.0'
595
+
596
+ '@typescript-eslint/[email protected]':
597
+ resolution: {integrity: sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==}
598
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
599
+ peerDependencies:
600
+ eslint: ^8.57.0 || ^9.0.0
601
+ typescript: '>=4.8.4 <6.0.0'
602
+
603
+ '@typescript-eslint/[email protected]':
604
+ resolution: {integrity: sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==}
605
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
606
+ peerDependencies:
607
+ typescript: '>=4.8.4 <6.0.0'
608
+
609
+ '@typescript-eslint/[email protected]':
610
+ resolution: {integrity: sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==}
611
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
612
+
613
+ '@typescript-eslint/[email protected]':
614
+ resolution: {integrity: sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==}
615
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
616
+ peerDependencies:
617
+ typescript: '>=4.8.4 <6.0.0'
618
+
619
+ '@typescript-eslint/[email protected]':
620
+ resolution: {integrity: sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==}
621
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
622
+ peerDependencies:
623
+ eslint: ^8.57.0 || ^9.0.0
624
+ typescript: '>=4.8.4 <6.0.0'
625
+
626
+ '@typescript-eslint/[email protected]':
627
+ resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==}
628
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
629
+
630
+ '@typescript-eslint/[email protected]':
631
+ resolution: {integrity: sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==}
632
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
633
+ peerDependencies:
634
+ typescript: '>=4.8.4 <6.0.0'
635
+
636
+ '@typescript-eslint/[email protected]':
637
+ resolution: {integrity: sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==}
638
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
639
+ peerDependencies:
640
+ eslint: ^8.57.0 || ^9.0.0
641
+ typescript: '>=4.8.4 <6.0.0'
642
+
643
+ '@typescript-eslint/[email protected]':
644
+ resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==}
645
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
646
+
647
+ '@vitest/[email protected]':
648
+ resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
649
+
650
+ '@vitest/[email protected]':
651
+ resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
652
+ peerDependencies:
653
+ msw: ^2.4.9
654
+ vite: ^5.0.0
655
+ peerDependenciesMeta:
656
+ msw:
657
+ optional: true
658
+ vite:
659
+ optional: true
660
+
661
+ '@vitest/[email protected]':
662
+ resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
663
+
664
+ '@vitest/[email protected]':
665
+ resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
666
+
667
+ '@vitest/[email protected]':
668
+ resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
669
+
670
+ '@vitest/[email protected]':
671
+ resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
672
+
673
+ '@vitest/[email protected]':
674
+ resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==}
675
+ peerDependencies:
676
+ vitest: 2.1.9
677
+
678
+ '@vitest/[email protected]':
679
+ resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
680
+
681
682
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
683
+ peerDependencies:
684
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
685
+
686
687
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
688
+ engines: {node: '>=0.4.0'}
689
+ hasBin: true
690
+
691
692
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
693
+
694
695
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
696
+ engines: {node: '>=8'}
697
+
698
699
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
700
+
701
702
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
703
+ engines: {node: '>=12'}
704
+
705
706
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
707
+
708
709
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
710
+
711
712
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
713
+
714
715
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
716
+ engines: {node: '>=8'}
717
+
718
719
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
720
+ engines: {node: '>=8'}
721
+
722
723
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
724
+ engines: {node: '>=6'}
725
+
726
727
+ resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==}
728
+ engines: {node: '>=18'}
729
+
730
731
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
732
+ engines: {node: '>=10'}
733
+
734
735
+ resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
736
+ engines: {node: '>= 16'}
737
+
738
739
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
740
+ engines: {node: '>=7.0.0'}
741
+
742
743
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
744
+
745
746
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
747
+
748
749
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
750
+ engines: {node: '>= 8'}
751
+
752
753
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
754
+ engines: {node: '>=6.0'}
755
+ peerDependencies:
756
+ supports-color: '*'
757
+ peerDependenciesMeta:
758
+ supports-color:
759
+ optional: true
760
+
761
762
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
763
+ engines: {node: '>=6.0'}
764
+ peerDependencies:
765
+ supports-color: '*'
766
+ peerDependenciesMeta:
767
+ supports-color:
768
+ optional: true
769
+
770
771
+ resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
772
+ engines: {node: '>=6'}
773
+
774
775
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
776
+
777
778
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
779
+
780
781
+ resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
782
+ engines: {node: '>=12'}
783
+ hasBin: true
784
+
785
786
+ resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
787
+ engines: {node: '>=18'}
788
+ hasBin: true
789
+
790
791
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
792
+ engines: {node: '>=10'}
793
+
794
795
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
796
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
797
+
798
799
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
800
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
801
+
802
803
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
804
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
805
+
806
807
+ resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==}
808
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
809
+ hasBin: true
810
+ peerDependencies:
811
+ jiti: '*'
812
+ peerDependenciesMeta:
813
+ jiti:
814
+ optional: true
815
+
816
817
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
818
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
819
+
820
821
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
822
+ engines: {node: '>=0.10'}
823
+
824
825
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
826
+ engines: {node: '>=4.0'}
827
+
828
829
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
830
+ engines: {node: '>=4.0'}
831
+
832
833
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
834
+
835
836
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
837
+ engines: {node: '>=0.10.0'}
838
+
839
840
+ resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
841
+ engines: {node: '>=12.0.0'}
842
+
843
844
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
845
+
846
847
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
848
+ engines: {node: '>=8.6.0'}
849
+
850
851
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
852
+
853
854
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
855
+
856
857
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
858
+
859
860
+ resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
861
+ peerDependencies:
862
+ picomatch: ^3 || ^4
863
+ peerDependenciesMeta:
864
+ picomatch:
865
+ optional: true
866
+
867
868
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
869
+
870
871
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
872
+ engines: {node: '>=16.0.0'}
873
+
874
875
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
876
+ engines: {node: '>=8'}
877
+
878
879
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
880
+ engines: {node: '>=10'}
881
+
882
883
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
884
+ engines: {node: '>=16'}
885
+
886
887
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
888
+
889
890
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
891
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
892
+ os: [darwin]
893
+
894
895
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
896
+ engines: {node: '>= 6'}
897
+
898
899
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
900
+ engines: {node: '>=10.13.0'}
901
+
902
903
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
904
+ engines: {node: '>=18'}
905
+
906
907
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
908
+
909
910
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
911
+ engines: {node: '>=8'}
912
+
913
914
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
915
+ engines: {node: '>= 4'}
916
+
917
918
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
919
+ engines: {node: '>= 4'}
920
+
921
922
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
923
+ engines: {node: '>=6'}
924
+
925
926
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
927
+ engines: {node: '>=0.8.19'}
928
+
929
930
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
931
+ engines: {node: '>=0.10.0'}
932
+
933
934
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
935
+ engines: {node: '>=0.10.0'}
936
+
937
938
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
939
+ engines: {node: '>=0.12.0'}
940
+
941
942
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
943
+
944
945
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
946
+ hasBin: true
947
+
948
949
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
950
+
951
952
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
953
+
954
955
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
956
+
957
958
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
959
+
960
961
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
962
+ engines: {node: '>= 0.8.0'}
963
+
964
965
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
966
+ engines: {node: '>=10'}
967
+
968
969
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
970
+
971
972
+ resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==}
973
+
974
975
+ resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
976
+
977
978
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
979
+ engines: {node: '>= 8'}
980
+
981
982
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
983
+ engines: {node: '>=8.6'}
984
+
985
986
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
987
+
988
989
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
990
+ engines: {node: '>=16 || 14 >=14.17'}
991
+
992
993
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
994
+ engines: {node: '>=10'}
995
+
996
997
+ resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
998
+
999
1000
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
1001
+
1002
1003
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
1004
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
1005
+ hasBin: true
1006
+
1007
1008
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
1009
+
1010
1011
+ resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
1012
+
1013
1014
+ resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
1015
+ hasBin: true
1016
+
1017
1018
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
1019
+ engines: {node: '>= 0.8.0'}
1020
+
1021
1022
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
1023
+ engines: {node: '>=10'}
1024
+
1025
1026
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
1027
+ engines: {node: '>=10'}
1028
+
1029
1030
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
1031
+ engines: {node: '>=6'}
1032
+
1033
1034
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
1035
+ engines: {node: '>=8'}
1036
+
1037
1038
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
1039
+ engines: {node: '>=8'}
1040
+
1041
1042
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
1043
+
1044
1045
+ resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
1046
+ engines: {node: '>= 14.16'}
1047
+
1048
1049
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
1050
+
1051
1052
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
1053
+ engines: {node: '>=8.6'}
1054
+
1055
1056
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
1057
+ engines: {node: '>=12'}
1058
+
1059
1060
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
1061
+ engines: {node: ^10 || ^12 || >=14}
1062
+
1063
1064
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
1065
+ engines: {node: '>= 0.8.0'}
1066
+
1067
1068
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
1069
+ engines: {node: '>=6'}
1070
+
1071
1072
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
1073
+
1074
1075
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
1076
+ engines: {node: '>=4'}
1077
+
1078
1079
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
1080
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
1081
+
1082
1083
+ resolution: {integrity: sha512-ONmkT3Ud3IfW15nl7l4qAZko5/2iZ5ALVBDh02ZSZ5IGVLJSYkRcRa3iB58VyEIyoofs9m2xdVrm+lTi97+3pw==}
1084
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
1085
+ hasBin: true
1086
+
1087
1088
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
1089
+
1090
1091
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
1092
+ engines: {node: '>=10'}
1093
+ hasBin: true
1094
+
1095
1096
+ resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==}
1097
+ engines: {node: '>=16.0.0'}
1098
+
1099
1100
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
1101
+ engines: {node: '>=8'}
1102
+
1103
1104
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
1105
+ engines: {node: '>=8'}
1106
+
1107
1108
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
1109
+
1110
1111
+ resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
1112
+ engines: {node: '>=18'}
1113
+
1114
1115
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
1116
+ engines: {node: '>=0.10.0'}
1117
+
1118
1119
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
1120
+
1121
1122
+ resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
1123
+
1124
1125
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
1126
+ engines: {node: '>=8'}
1127
+
1128
1129
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
1130
+ engines: {node: '>=8'}
1131
+
1132
1133
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
1134
+
1135
1136
+ resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
1137
+
1138
1139
+ resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
1140
+ engines: {node: '>=12.0.0'}
1141
+
1142
1143
+ resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
1144
+ engines: {node: ^18.0.0 || >=20.0.0}
1145
+
1146
1147
+ resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
1148
+ engines: {node: '>=14.0.0'}
1149
+
1150
1151
+ resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
1152
+ engines: {node: '>=14.0.0'}
1153
+
1154
1155
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
1156
+ engines: {node: '>=8.0'}
1157
+
1158
1159
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
1160
+ engines: {node: '>=6'}
1161
+
1162
1163
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
1164
+ engines: {node: '>=18.12'}
1165
+ peerDependencies:
1166
+ typescript: '>=4.8.4'
1167
+
1168
1169
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
1170
+ engines: {node: '>= 0.8.0'}
1171
+
1172
1173
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
1174
+ engines: {node: '>=14.17'}
1175
+ hasBin: true
1176
+
1177
1178
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
1179
+
1180
1181
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
1182
+
1183
1184
+ resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
1185
+ engines: {node: ^18.0.0 || >=20.0.0}
1186
+ hasBin: true
1187
+
1188
1189
+ resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
1190
+ engines: {node: ^18.0.0 || >=20.0.0}
1191
+ hasBin: true
1192
+ peerDependencies:
1193
+ '@types/node': ^18.0.0 || >=20.0.0
1194
+ less: '*'
1195
+ lightningcss: ^1.21.0
1196
+ sass: '*'
1197
+ sass-embedded: '*'
1198
+ stylus: '*'
1199
+ sugarss: '*'
1200
+ terser: ^5.4.0
1201
+ peerDependenciesMeta:
1202
+ '@types/node':
1203
+ optional: true
1204
+ less:
1205
+ optional: true
1206
+ lightningcss:
1207
+ optional: true
1208
+ sass:
1209
+ optional: true
1210
+ sass-embedded:
1211
+ optional: true
1212
+ stylus:
1213
+ optional: true
1214
+ sugarss:
1215
+ optional: true
1216
+ terser:
1217
+ optional: true
1218
+
1219
1220
+ resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
1221
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
1222
+ hasBin: true
1223
+ peerDependencies:
1224
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
1225
+ jiti: '>=1.21.0'
1226
+ less: '*'
1227
+ lightningcss: ^1.21.0
1228
+ sass: '*'
1229
+ sass-embedded: '*'
1230
+ stylus: '*'
1231
+ sugarss: '*'
1232
+ terser: ^5.16.0
1233
+ tsx: ^4.8.1
1234
+ yaml: ^2.4.2
1235
+ peerDependenciesMeta:
1236
+ '@types/node':
1237
+ optional: true
1238
+ jiti:
1239
+ optional: true
1240
+ less:
1241
+ optional: true
1242
+ lightningcss:
1243
+ optional: true
1244
+ sass:
1245
+ optional: true
1246
+ sass-embedded:
1247
+ optional: true
1248
+ stylus:
1249
+ optional: true
1250
+ sugarss:
1251
+ optional: true
1252
+ terser:
1253
+ optional: true
1254
+ tsx:
1255
+ optional: true
1256
+ yaml:
1257
+ optional: true
1258
+
1259
1260
+ resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
1261
+ engines: {node: ^18.0.0 || >=20.0.0}
1262
+ hasBin: true
1263
+ peerDependencies:
1264
+ '@edge-runtime/vm': '*'
1265
+ '@types/node': ^18.0.0 || >=20.0.0
1266
+ '@vitest/browser': 2.1.9
1267
+ '@vitest/ui': 2.1.9
1268
+ happy-dom: '*'
1269
+ jsdom: '*'
1270
+ peerDependenciesMeta:
1271
+ '@edge-runtime/vm':
1272
+ optional: true
1273
+ '@types/node':
1274
+ optional: true
1275
+ '@vitest/browser':
1276
+ optional: true
1277
+ '@vitest/ui':
1278
+ optional: true
1279
+ happy-dom:
1280
+ optional: true
1281
+ jsdom:
1282
+ optional: true
1283
+
1284
1285
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
1286
+ engines: {node: '>= 8'}
1287
+ hasBin: true
1288
+
1289
1290
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
1291
+ engines: {node: '>=8'}
1292
+ hasBin: true
1293
+
1294
1295
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
1296
+ engines: {node: '>=0.10.0'}
1297
+
1298
1299
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
1300
+ engines: {node: '>=10'}
1301
+
1302
+ snapshots:
1303
+
1304
+ '@esbuild/[email protected]':
1305
+ optional: true
1306
+
1307
+ '@esbuild/[email protected]':
1308
+ optional: true
1309
+
1310
+ '@esbuild/[email protected]':
1311
+ optional: true
1312
+
1313
+ '@esbuild/[email protected]':
1314
+ optional: true
1315
+
1316
+ '@esbuild/[email protected]':
1317
+ optional: true
1318
+
1319
+ '@esbuild/[email protected]':
1320
+ optional: true
1321
+
1322
+ '@esbuild/[email protected]':
1323
+ optional: true
1324
+
1325
+ '@esbuild/[email protected]':
1326
+ optional: true
1327
+
1328
+ '@esbuild/[email protected]':
1329
+ optional: true
1330
+
1331
+ '@esbuild/[email protected]':
1332
+ optional: true
1333
+
1334
+ '@esbuild/[email protected]':
1335
+ optional: true
1336
+
1337
+ '@esbuild/[email protected]':
1338
+ optional: true
1339
+
1340
+ '@esbuild/[email protected]':
1341
+ optional: true
1342
+
1343
+ '@esbuild/[email protected]':
1344
+ optional: true
1345
+
1346
+ '@esbuild/[email protected]':
1347
+ optional: true
1348
+
1349
+ '@esbuild/[email protected]':
1350
+ optional: true
1351
+
1352
+ '@esbuild/[email protected]':
1353
+ optional: true
1354
+
1355
+ '@esbuild/[email protected]':
1356
+ optional: true
1357
+
1358
+ '@esbuild/[email protected]':
1359
+ optional: true
1360
+
1361
+ '@esbuild/[email protected]':
1362
+ optional: true
1363
+
1364
+ '@esbuild/[email protected]':
1365
+ optional: true
1366
+
1367
+ '@esbuild/[email protected]':
1368
+ optional: true
1369
+
1370
+ '@esbuild/[email protected]':
1371
+ optional: true
1372
+
1373
+ '@esbuild/[email protected]':
1374
+ optional: true
1375
+
1376
+ '@esbuild/[email protected]':
1377
+ optional: true
1378
+
1379
+ '@esbuild/[email protected]':
1380
+ optional: true
1381
+
1382
+ '@esbuild/[email protected]':
1383
+ optional: true
1384
+
1385
+ '@esbuild/[email protected]':
1386
+ optional: true
1387
+
1388
+ '@esbuild/[email protected]':
1389
+ optional: true
1390
+
1391
+ '@esbuild/[email protected]':
1392
+ optional: true
1393
+
1394
+ '@esbuild/[email protected]':
1395
+ optional: true
1396
+
1397
+ '@esbuild/[email protected]':
1398
+ optional: true
1399
+
1400
+ '@esbuild/[email protected]':
1401
+ optional: true
1402
+
1403
+ '@esbuild/[email protected]':
1404
+ optional: true
1405
+
1406
+ '@esbuild/[email protected]':
1407
+ optional: true
1408
+
1409
+ '@esbuild/[email protected]':
1410
+ optional: true
1411
+
1412
+ '@esbuild/[email protected]':
1413
+ optional: true
1414
+
1415
+ '@esbuild/[email protected]':
1416
+ optional: true
1417
+
1418
+ '@esbuild/[email protected]':
1419
+ optional: true
1420
+
1421
+ '@esbuild/[email protected]':
1422
+ optional: true
1423
+
1424
+ '@esbuild/[email protected]':
1425
+ optional: true
1426
+
1427
+ '@esbuild/[email protected]':
1428
+ optional: true
1429
+
1430
+ '@esbuild/[email protected]':
1431
+ optional: true
1432
+
1433
+ '@esbuild/[email protected]':
1434
+ optional: true
1435
+
1436
+ '@esbuild/[email protected]':
1437
+ optional: true
1438
+
1439
+ '@esbuild/[email protected]':
1440
+ optional: true
1441
+
1442
+ '@esbuild/[email protected]':
1443
+ optional: true
1444
+
1445
+ '@esbuild/[email protected]':
1446
+ optional: true
1447
+
1448
+ '@esbuild/[email protected]':
1449
+ optional: true
1450
+
1451
+ '@eslint-community/[email protected]([email protected])':
1452
+ dependencies:
1453
+ eslint: 9.34.0
1454
+ eslint-visitor-keys: 3.4.3
1455
+
1456
+ '@eslint-community/[email protected]': {}
1457
+
1458
+ '@eslint/[email protected]':
1459
+ dependencies:
1460
+ '@eslint/object-schema': 2.1.6
1461
+ debug: 4.4.1
1462
+ minimatch: 3.1.2
1463
+ transitivePeerDependencies:
1464
+ - supports-color
1465
+
1466
+ '@eslint/[email protected]': {}
1467
+
1468
+ '@eslint/[email protected]':
1469
+ dependencies:
1470
+ '@types/json-schema': 7.0.15
1471
+
1472
+ '@eslint/[email protected]':
1473
+ dependencies:
1474
+ ajv: 6.12.6
1475
+ debug: 4.4.1
1476
+ espree: 10.4.0
1477
+ globals: 14.0.0
1478
+ ignore: 5.3.2
1479
+ import-fresh: 3.3.1
1480
+ js-yaml: 4.1.0
1481
+ minimatch: 3.1.2
1482
+ strip-json-comments: 3.1.1
1483
+ transitivePeerDependencies:
1484
+ - supports-color
1485
+
1486
+ '@eslint/[email protected]': {}
1487
+
1488
+ '@eslint/[email protected]': {}
1489
+
1490
+ '@eslint/[email protected]':
1491
+ dependencies:
1492
+ '@eslint/core': 0.15.2
1493
+ levn: 0.4.1
1494
+
1495
+ '@humanfs/[email protected]': {}
1496
+
1497
+ '@humanfs/[email protected]':
1498
+ dependencies:
1499
+ '@humanfs/core': 0.19.1
1500
+ '@humanwhocodes/retry': 0.3.1
1501
+
1502
+ '@humanwhocodes/[email protected]': {}
1503
+
1504
+ '@humanwhocodes/[email protected]': {}
1505
+
1506
+ '@humanwhocodes/[email protected]': {}
1507
+
1508
+ '@jridgewell/[email protected]': {}
1509
+
1510
+ '@nodelib/[email protected]':
1511
+ dependencies:
1512
+ '@nodelib/fs.stat': 2.0.5
1513
+ run-parallel: 1.2.0
1514
+
1515
+ '@nodelib/[email protected]': {}
1516
+
1517
+ '@nodelib/[email protected]':
1518
+ dependencies:
1519
+ '@nodelib/fs.scandir': 2.1.5
1520
+ fastq: 1.19.1
1521
+
1522
+ '@polka/[email protected]': {}
1523
+
1524
+ '@rollup/[email protected]':
1525
+ optional: true
1526
+
1527
+ '@rollup/[email protected]':
1528
+ optional: true
1529
+
1530
+ '@rollup/[email protected]':
1531
+ optional: true
1532
+
1533
+ '@rollup/[email protected]':
1534
+ optional: true
1535
+
1536
+ '@rollup/[email protected]':
1537
+ optional: true
1538
+
1539
+ '@rollup/[email protected]':
1540
+ optional: true
1541
+
1542
+ '@rollup/[email protected]':
1543
+ optional: true
1544
+
1545
+ '@rollup/[email protected]':
1546
+ optional: true
1547
+
1548
+ '@rollup/[email protected]':
1549
+ optional: true
1550
+
1551
+ '@rollup/[email protected]':
1552
+ optional: true
1553
+
1554
+ '@rollup/[email protected]':
1555
+ optional: true
1556
+
1557
+ '@rollup/[email protected]':
1558
+ optional: true
1559
+
1560
+ '@rollup/[email protected]':
1561
+ optional: true
1562
+
1563
+ '@rollup/[email protected]':
1564
+ optional: true
1565
+
1566
+ '@rollup/[email protected]':
1567
+ optional: true
1568
+
1569
+ '@rollup/[email protected]':
1570
+ optional: true
1571
+
1572
+ '@rollup/[email protected]':
1573
+ optional: true
1574
+
1575
+ '@rollup/[email protected]':
1576
+ optional: true
1577
+
1578
+ '@rollup/[email protected]':
1579
+ optional: true
1580
+
1581
+ '@rollup/[email protected]':
1582
+ optional: true
1583
+
1584
+ '@serialport/[email protected]':
1585
+ dependencies:
1586
+ '@serialport/bindings-interface': 1.2.2
1587
+ debug: 4.3.4
1588
+ transitivePeerDependencies:
1589
+ - supports-color
1590
+
1591
+ '@serialport/[email protected]':
1592
+ dependencies:
1593
+ '@serialport/bindings-interface': 1.2.2
1594
+ '@serialport/parser-readline': 11.0.0
1595
+ debug: 4.3.4
1596
+ node-addon-api: 7.0.0
1597
+ node-gyp-build: 4.6.0
1598
+ transitivePeerDependencies:
1599
+ - supports-color
1600
+
1601
+ '@serialport/[email protected]': {}
1602
+
1603
+ '@serialport/[email protected]': {}
1604
+
1605
+ '@serialport/[email protected]': {}
1606
+
1607
+ '@serialport/[email protected]': {}
1608
+
1609
+ '@serialport/[email protected]': {}
1610
+
1611
+ '@serialport/[email protected]': {}
1612
+
1613
+ '@serialport/[email protected]': {}
1614
+
1615
+ '@serialport/[email protected]':
1616
+ dependencies:
1617
+ '@serialport/parser-delimiter': 11.0.0
1618
+
1619
+ '@serialport/[email protected]':
1620
+ dependencies:
1621
+ '@serialport/parser-delimiter': 12.0.0
1622
+
1623
+ '@serialport/[email protected]': {}
1624
+
1625
+ '@serialport/[email protected]': {}
1626
+
1627
+ '@serialport/[email protected]': {}
1628
+
1629
+ '@serialport/[email protected]': {}
1630
+
1631
+ '@serialport/[email protected]':
1632
+ dependencies:
1633
+ '@serialport/bindings-interface': 1.2.2
1634
+ debug: 4.3.4
1635
+ transitivePeerDependencies:
1636
+ - supports-color
1637
+
1638
+ '@types/[email protected]': {}
1639
+
1640
+ '@types/[email protected]': {}
1641
+
1642
+ '@types/[email protected]':
1643
+ dependencies:
1644
+ undici-types: 5.26.5
1645
+
1646
1647
+ dependencies:
1648
+ '@eslint-community/regexpp': 4.12.1
1649
+ '@typescript-eslint/parser': 8.41.0([email protected])([email protected])
1650
+ '@typescript-eslint/scope-manager': 8.41.0
1651
+ '@typescript-eslint/type-utils': 8.41.0([email protected])([email protected])
1652
+ '@typescript-eslint/utils': 8.41.0([email protected])([email protected])
1653
+ '@typescript-eslint/visitor-keys': 8.41.0
1654
+ eslint: 9.34.0
1655
+ graphemer: 1.4.0
1656
+ ignore: 7.0.5
1657
+ natural-compare: 1.4.0
1658
+ ts-api-utils: 2.1.0([email protected])
1659
+ typescript: 5.8.3
1660
+ transitivePeerDependencies:
1661
+ - supports-color
1662
+
1663
1664
+ dependencies:
1665
+ '@typescript-eslint/scope-manager': 8.41.0
1666
+ '@typescript-eslint/types': 8.41.0
1667
+ '@typescript-eslint/typescript-estree': 8.41.0([email protected])
1668
+ '@typescript-eslint/visitor-keys': 8.41.0
1669
+ debug: 4.4.1
1670
+ eslint: 9.34.0
1671
+ typescript: 5.8.3
1672
+ transitivePeerDependencies:
1673
+ - supports-color
1674
+
1675
+ '@typescript-eslint/[email protected]([email protected])':
1676
+ dependencies:
1677
+ '@typescript-eslint/tsconfig-utils': 8.41.0([email protected])
1678
+ '@typescript-eslint/types': 8.41.0
1679
+ debug: 4.4.1
1680
+ typescript: 5.8.3
1681
+ transitivePeerDependencies:
1682
+ - supports-color
1683
+
1684
+ '@typescript-eslint/[email protected]':
1685
+ dependencies:
1686
+ '@typescript-eslint/types': 8.41.0
1687
+ '@typescript-eslint/visitor-keys': 8.41.0
1688
+
1689
+ '@typescript-eslint/[email protected]([email protected])':
1690
+ dependencies:
1691
+ typescript: 5.8.3
1692
+
1693
1694
+ dependencies:
1695
+ '@typescript-eslint/types': 8.41.0
1696
+ '@typescript-eslint/typescript-estree': 8.41.0([email protected])
1697
+ '@typescript-eslint/utils': 8.41.0([email protected])([email protected])
1698
+ debug: 4.4.1
1699
+ eslint: 9.34.0
1700
+ ts-api-utils: 2.1.0([email protected])
1701
+ typescript: 5.8.3
1702
+ transitivePeerDependencies:
1703
+ - supports-color
1704
+
1705
+ '@typescript-eslint/[email protected]': {}
1706
+
1707
+ '@typescript-eslint/[email protected]([email protected])':
1708
+ dependencies:
1709
+ '@typescript-eslint/project-service': 8.41.0([email protected])
1710
+ '@typescript-eslint/tsconfig-utils': 8.41.0([email protected])
1711
+ '@typescript-eslint/types': 8.41.0
1712
+ '@typescript-eslint/visitor-keys': 8.41.0
1713
+ debug: 4.4.1
1714
+ fast-glob: 3.3.3
1715
+ is-glob: 4.0.3
1716
+ minimatch: 9.0.5
1717
+ semver: 7.7.2
1718
+ ts-api-utils: 2.1.0([email protected])
1719
+ typescript: 5.8.3
1720
+ transitivePeerDependencies:
1721
+ - supports-color
1722
+
1723
1724
+ dependencies:
1725
+ '@eslint-community/eslint-utils': 4.7.0([email protected])
1726
+ '@typescript-eslint/scope-manager': 8.41.0
1727
+ '@typescript-eslint/types': 8.41.0
1728
+ '@typescript-eslint/typescript-estree': 8.41.0([email protected])
1729
+ eslint: 9.34.0
1730
+ typescript: 5.8.3
1731
+ transitivePeerDependencies:
1732
+ - supports-color
1733
+
1734
+ '@typescript-eslint/[email protected]':
1735
+ dependencies:
1736
+ '@typescript-eslint/types': 8.41.0
1737
+ eslint-visitor-keys: 4.2.1
1738
+
1739
+ '@vitest/[email protected]':
1740
+ dependencies:
1741
+ '@vitest/spy': 2.1.9
1742
+ '@vitest/utils': 2.1.9
1743
+ chai: 5.2.1
1744
+ tinyrainbow: 1.2.0
1745
+
1746
1747
+ dependencies:
1748
+ '@vitest/spy': 2.1.9
1749
+ estree-walker: 3.0.3
1750
+ magic-string: 0.30.17
1751
+ optionalDependencies:
1752
+ vite: 5.4.19(@types/[email protected])
1753
+
1754
+ '@vitest/[email protected]':
1755
+ dependencies:
1756
+ tinyrainbow: 1.2.0
1757
+
1758
+ '@vitest/[email protected]':
1759
+ dependencies:
1760
+ '@vitest/utils': 2.1.9
1761
+ pathe: 1.1.2
1762
+
1763
+ '@vitest/[email protected]':
1764
+ dependencies:
1765
+ '@vitest/pretty-format': 2.1.9
1766
+ magic-string: 0.30.17
1767
+ pathe: 1.1.2
1768
+
1769
+ '@vitest/[email protected]':
1770
+ dependencies:
1771
+ tinyspy: 3.0.2
1772
+
1773
1774
+ dependencies:
1775
+ '@vitest/utils': 2.1.9
1776
+ fflate: 0.8.2
1777
+ flatted: 3.3.3
1778
+ pathe: 1.1.2
1779
+ sirv: 3.0.1
1780
+ tinyglobby: 0.2.14
1781
+ tinyrainbow: 1.2.0
1782
+ vitest: 2.1.9(@types/[email protected])(@vitest/[email protected])
1783
+
1784
+ '@vitest/[email protected]':
1785
+ dependencies:
1786
+ '@vitest/pretty-format': 2.1.9
1787
+ loupe: 3.2.0
1788
+ tinyrainbow: 1.2.0
1789
+
1790
1791
+ dependencies:
1792
+ acorn: 8.15.0
1793
+
1794
1795
+
1796
1797
+ dependencies:
1798
+ fast-deep-equal: 3.1.3
1799
+ fast-json-stable-stringify: 2.1.0
1800
+ json-schema-traverse: 0.4.1
1801
+ uri-js: 4.4.1
1802
+
1803
1804
+ dependencies:
1805
+ color-convert: 2.0.1
1806
+
1807
1808
+
1809
1810
+
1811
1812
+
1813
1814
+ dependencies:
1815
+ balanced-match: 1.0.2
1816
+ concat-map: 0.0.1
1817
+
1818
1819
+ dependencies:
1820
+ balanced-match: 1.0.2
1821
+
1822
1823
+ dependencies:
1824
+ fill-range: 7.1.1
1825
+
1826
1827
+
1828
1829
+
1830
1831
+ dependencies:
1832
+ assertion-error: 2.0.1
1833
+ check-error: 2.1.1
1834
+ deep-eql: 5.0.2
1835
+ loupe: 3.2.0
1836
+ pathval: 2.0.1
1837
+
1838
1839
+ dependencies:
1840
+ ansi-styles: 4.3.0
1841
+ supports-color: 7.2.0
1842
+
1843
1844
+
1845
1846
+ dependencies:
1847
+ color-name: 1.1.4
1848
+
1849
1850
+
1851
1852
+
1853
1854
+ dependencies:
1855
+ path-key: 3.1.1
1856
+ shebang-command: 2.0.0
1857
+ which: 2.0.2
1858
+
1859
1860
+ dependencies:
1861
+ ms: 2.1.2
1862
+
1863
1864
+ dependencies:
1865
+ ms: 2.1.3
1866
+
1867
1868
+
1869
1870
+
1871
1872
+
1873
1874
+ optionalDependencies:
1875
+ '@esbuild/aix-ppc64': 0.21.5
1876
+ '@esbuild/android-arm': 0.21.5
1877
+ '@esbuild/android-arm64': 0.21.5
1878
+ '@esbuild/android-x64': 0.21.5
1879
+ '@esbuild/darwin-arm64': 0.21.5
1880
+ '@esbuild/darwin-x64': 0.21.5
1881
+ '@esbuild/freebsd-arm64': 0.21.5
1882
+ '@esbuild/freebsd-x64': 0.21.5
1883
+ '@esbuild/linux-arm': 0.21.5
1884
+ '@esbuild/linux-arm64': 0.21.5
1885
+ '@esbuild/linux-ia32': 0.21.5
1886
+ '@esbuild/linux-loong64': 0.21.5
1887
+ '@esbuild/linux-mips64el': 0.21.5
1888
+ '@esbuild/linux-ppc64': 0.21.5
1889
+ '@esbuild/linux-riscv64': 0.21.5
1890
+ '@esbuild/linux-s390x': 0.21.5
1891
+ '@esbuild/linux-x64': 0.21.5
1892
+ '@esbuild/netbsd-x64': 0.21.5
1893
+ '@esbuild/openbsd-x64': 0.21.5
1894
+ '@esbuild/sunos-x64': 0.21.5
1895
+ '@esbuild/win32-arm64': 0.21.5
1896
+ '@esbuild/win32-ia32': 0.21.5
1897
+ '@esbuild/win32-x64': 0.21.5
1898
+
1899
1900
+ optionalDependencies:
1901
+ '@esbuild/aix-ppc64': 0.25.8
1902
+ '@esbuild/android-arm': 0.25.8
1903
+ '@esbuild/android-arm64': 0.25.8
1904
+ '@esbuild/android-x64': 0.25.8
1905
+ '@esbuild/darwin-arm64': 0.25.8
1906
+ '@esbuild/darwin-x64': 0.25.8
1907
+ '@esbuild/freebsd-arm64': 0.25.8
1908
+ '@esbuild/freebsd-x64': 0.25.8
1909
+ '@esbuild/linux-arm': 0.25.8
1910
+ '@esbuild/linux-arm64': 0.25.8
1911
+ '@esbuild/linux-ia32': 0.25.8
1912
+ '@esbuild/linux-loong64': 0.25.8
1913
+ '@esbuild/linux-mips64el': 0.25.8
1914
+ '@esbuild/linux-ppc64': 0.25.8
1915
+ '@esbuild/linux-riscv64': 0.25.8
1916
+ '@esbuild/linux-s390x': 0.25.8
1917
+ '@esbuild/linux-x64': 0.25.8
1918
+ '@esbuild/netbsd-arm64': 0.25.8
1919
+ '@esbuild/netbsd-x64': 0.25.8
1920
+ '@esbuild/openbsd-arm64': 0.25.8
1921
+ '@esbuild/openbsd-x64': 0.25.8
1922
+ '@esbuild/openharmony-arm64': 0.25.8
1923
+ '@esbuild/sunos-x64': 0.25.8
1924
+ '@esbuild/win32-arm64': 0.25.8
1925
+ '@esbuild/win32-ia32': 0.25.8
1926
+ '@esbuild/win32-x64': 0.25.8
1927
+
1928
1929
+
1930
1931
+ dependencies:
1932
+ esrecurse: 4.3.0
1933
+ estraverse: 5.3.0
1934
+
1935
1936
+
1937
1938
+
1939
1940
+ dependencies:
1941
+ '@eslint-community/eslint-utils': 4.7.0([email protected])
1942
+ '@eslint-community/regexpp': 4.12.1
1943
+ '@eslint/config-array': 0.21.0
1944
+ '@eslint/config-helpers': 0.3.1
1945
+ '@eslint/core': 0.15.2
1946
+ '@eslint/eslintrc': 3.3.1
1947
+ '@eslint/js': 9.34.0
1948
+ '@eslint/plugin-kit': 0.3.5
1949
+ '@humanfs/node': 0.16.6
1950
+ '@humanwhocodes/module-importer': 1.0.1
1951
+ '@humanwhocodes/retry': 0.4.3
1952
+ '@types/estree': 1.0.8
1953
+ '@types/json-schema': 7.0.15
1954
+ ajv: 6.12.6
1955
+ chalk: 4.1.2
1956
+ cross-spawn: 7.0.6
1957
+ debug: 4.4.1
1958
+ escape-string-regexp: 4.0.0
1959
+ eslint-scope: 8.4.0
1960
+ eslint-visitor-keys: 4.2.1
1961
+ espree: 10.4.0
1962
+ esquery: 1.6.0
1963
+ esutils: 2.0.3
1964
+ fast-deep-equal: 3.1.3
1965
+ file-entry-cache: 8.0.0
1966
+ find-up: 5.0.0
1967
+ glob-parent: 6.0.2
1968
+ ignore: 5.3.2
1969
+ imurmurhash: 0.1.4
1970
+ is-glob: 4.0.3
1971
+ json-stable-stringify-without-jsonify: 1.0.1
1972
+ lodash.merge: 4.6.2
1973
+ minimatch: 3.1.2
1974
+ natural-compare: 1.4.0
1975
+ optionator: 0.9.4
1976
+ transitivePeerDependencies:
1977
+ - supports-color
1978
+
1979
1980
+ dependencies:
1981
+ acorn: 8.15.0
1982
+ acorn-jsx: 5.3.2([email protected])
1983
+ eslint-visitor-keys: 4.2.1
1984
+
1985
1986
+ dependencies:
1987
+ estraverse: 5.3.0
1988
+
1989
1990
+ dependencies:
1991
+ estraverse: 5.3.0
1992
+
1993
1994
+
1995
1996
+ dependencies:
1997
+ '@types/estree': 1.0.8
1998
+
1999
2000
+
2001
2002
+
2003
2004
+
2005
2006
+ dependencies:
2007
+ '@nodelib/fs.stat': 2.0.5
2008
+ '@nodelib/fs.walk': 1.2.8
2009
+ glob-parent: 5.1.2
2010
+ merge2: 1.4.1
2011
+ micromatch: 4.0.8
2012
+
2013
2014
+
2015
2016
+
2017
2018
+ dependencies:
2019
+ reusify: 1.1.0
2020
+
2021
2022
+ optionalDependencies:
2023
+ picomatch: 4.0.3
2024
+
2025
2026
+
2027
2028
+ dependencies:
2029
+ flat-cache: 4.0.1
2030
+
2031
2032
+ dependencies:
2033
+ to-regex-range: 5.0.1
2034
+
2035
2036
+ dependencies:
2037
+ locate-path: 6.0.0
2038
+ path-exists: 4.0.0
2039
+
2040
2041
+ dependencies:
2042
+ flatted: 3.3.3
2043
+ keyv: 4.5.4
2044
+
2045
2046
+
2047
2048
+ optional: true
2049
+
2050
2051
+ dependencies:
2052
+ is-glob: 4.0.3
2053
+
2054
2055
+ dependencies:
2056
+ is-glob: 4.0.3
2057
+
2058
2059
+
2060
2061
+
2062
2063
+
2064
2065
+
2066
2067
+
2068
2069
+ dependencies:
2070
+ parent-module: 1.0.1
2071
+ resolve-from: 4.0.0
2072
+
2073
2074
+
2075
2076
+
2077
2078
+ dependencies:
2079
+ is-extglob: 2.1.1
2080
+
2081
2082
+
2083
2084
+
2085
2086
+ dependencies:
2087
+ argparse: 2.0.1
2088
+
2089
2090
+
2091
2092
+
2093
2094
+
2095
2096
+ dependencies:
2097
+ json-buffer: 3.0.1
2098
+
2099
2100
+ dependencies:
2101
+ prelude-ls: 1.2.1
2102
+ type-check: 0.4.0
2103
+
2104
2105
+ dependencies:
2106
+ p-locate: 5.0.0
2107
+
2108
2109
+
2110
2111
+
2112
2113
+ dependencies:
2114
+ '@jridgewell/sourcemap-codec': 1.5.4
2115
+
2116
2117
+
2118
2119
+ dependencies:
2120
+ braces: 3.0.3
2121
+ picomatch: 2.3.1
2122
+
2123
2124
+ dependencies:
2125
+ brace-expansion: 1.1.12
2126
+
2127
2128
+ dependencies:
2129
+ brace-expansion: 2.0.2
2130
+
2131
2132
+
2133
2134
+
2135
2136
+
2137
2138
+
2139
2140
+
2141
2142
+
2143
2144
+
2145
2146
+ dependencies:
2147
+ deep-is: 0.1.4
2148
+ fast-levenshtein: 2.0.6
2149
+ levn: 0.4.1
2150
+ prelude-ls: 1.2.1
2151
+ type-check: 0.4.0
2152
+ word-wrap: 1.2.5
2153
+
2154
2155
+ dependencies:
2156
+ yocto-queue: 0.1.0
2157
+
2158
2159
+ dependencies:
2160
+ p-limit: 3.1.0
2161
+
2162
2163
+ dependencies:
2164
+ callsites: 3.1.0
2165
+
2166
2167
+
2168
2169
+
2170
2171
+
2172
2173
+
2174
2175
+
2176
2177
+
2178
2179
+
2180
2181
+ dependencies:
2182
+ nanoid: 3.3.11
2183
+ picocolors: 1.1.1
2184
+ source-map-js: 1.2.1
2185
+
2186
2187
+
2188
2189
+
2190
2191
+
2192
2193
+
2194
2195
+
2196
2197
+ dependencies:
2198
+ '@types/estree': 1.0.8
2199
+ optionalDependencies:
2200
+ '@rollup/rollup-android-arm-eabi': 4.46.0
2201
+ '@rollup/rollup-android-arm64': 4.46.0
2202
+ '@rollup/rollup-darwin-arm64': 4.46.0
2203
+ '@rollup/rollup-darwin-x64': 4.46.0
2204
+ '@rollup/rollup-freebsd-arm64': 4.46.0
2205
+ '@rollup/rollup-freebsd-x64': 4.46.0
2206
+ '@rollup/rollup-linux-arm-gnueabihf': 4.46.0
2207
+ '@rollup/rollup-linux-arm-musleabihf': 4.46.0
2208
+ '@rollup/rollup-linux-arm64-gnu': 4.46.0
2209
+ '@rollup/rollup-linux-arm64-musl': 4.46.0
2210
+ '@rollup/rollup-linux-loongarch64-gnu': 4.46.0
2211
+ '@rollup/rollup-linux-ppc64-gnu': 4.46.0
2212
+ '@rollup/rollup-linux-riscv64-gnu': 4.46.0
2213
+ '@rollup/rollup-linux-riscv64-musl': 4.46.0
2214
+ '@rollup/rollup-linux-s390x-gnu': 4.46.0
2215
+ '@rollup/rollup-linux-x64-gnu': 4.46.0
2216
+ '@rollup/rollup-linux-x64-musl': 4.46.0
2217
+ '@rollup/rollup-win32-arm64-msvc': 4.46.0
2218
+ '@rollup/rollup-win32-ia32-msvc': 4.46.0
2219
+ '@rollup/rollup-win32-x64-msvc': 4.46.0
2220
+ fsevents: 2.3.3
2221
+
2222
2223
+ dependencies:
2224
+ queue-microtask: 1.2.3
2225
+
2226
2227
+
2228
2229
+ dependencies:
2230
+ '@serialport/binding-mock': 10.2.2
2231
+ '@serialport/bindings-cpp': 12.0.1
2232
+ '@serialport/parser-byte-length': 12.0.0
2233
+ '@serialport/parser-cctalk': 12.0.0
2234
+ '@serialport/parser-delimiter': 12.0.0
2235
+ '@serialport/parser-inter-byte-timeout': 12.0.0
2236
+ '@serialport/parser-packet-length': 12.0.0
2237
+ '@serialport/parser-readline': 12.0.0
2238
+ '@serialport/parser-ready': 12.0.0
2239
+ '@serialport/parser-regex': 12.0.0
2240
+ '@serialport/parser-slip-encoder': 12.0.0
2241
+ '@serialport/parser-spacepacket': 12.0.0
2242
+ '@serialport/stream': 12.0.0
2243
+ debug: 4.3.4
2244
+ transitivePeerDependencies:
2245
+ - supports-color
2246
+
2247
2248
+ dependencies:
2249
+ shebang-regex: 3.0.0
2250
+
2251
2252
+
2253
2254
+
2255
2256
+ dependencies:
2257
+ '@polka/url': 1.0.0-next.29
2258
+ mrmime: 2.0.1
2259
+ totalist: 3.0.1
2260
+
2261
2262
+
2263
2264
+
2265
2266
+
2267
2268
+
2269
2270
+ dependencies:
2271
+ has-flag: 4.0.0
2272
+
2273
2274
+
2275
2276
+
2277
2278
+ dependencies:
2279
+ fdir: 6.4.6([email protected])
2280
+ picomatch: 4.0.3
2281
+
2282
2283
+
2284
2285
+
2286
2287
+
2288
2289
+ dependencies:
2290
+ is-number: 7.0.0
2291
+
2292
2293
+
2294
2295
+ dependencies:
2296
+ typescript: 5.8.3
2297
+
2298
2299
+ dependencies:
2300
+ prelude-ls: 1.2.1
2301
+
2302
2303
+
2304
2305
+
2306
2307
+ dependencies:
2308
+ punycode: 2.3.1
2309
+
2310
2311
+ dependencies:
2312
+ cac: 6.7.14
2313
+ debug: 4.4.1
2314
+ es-module-lexer: 1.7.0
2315
+ pathe: 1.1.2
2316
+ vite: 5.4.19(@types/[email protected])
2317
+ transitivePeerDependencies:
2318
+ - '@types/node'
2319
+ - less
2320
+ - lightningcss
2321
+ - sass
2322
+ - sass-embedded
2323
+ - stylus
2324
+ - sugarss
2325
+ - supports-color
2326
+ - terser
2327
+
2328
2329
+ dependencies:
2330
+ esbuild: 0.21.5
2331
+ postcss: 8.5.6
2332
+ rollup: 4.46.0
2333
+ optionalDependencies:
2334
+ '@types/node': 18.19.123
2335
+ fsevents: 2.3.3
2336
+
2337
2338
+ dependencies:
2339
+ esbuild: 0.25.8
2340
+ fdir: 6.4.6([email protected])
2341
+ picomatch: 4.0.3
2342
+ postcss: 8.5.6
2343
+ rollup: 4.46.0
2344
+ tinyglobby: 0.2.14
2345
+ optionalDependencies:
2346
+ '@types/node': 18.19.123
2347
+ fsevents: 2.3.3
2348
+
2349
2350
+ dependencies:
2351
+ '@vitest/expect': 2.1.9
2352
+ '@vitest/mocker': 2.1.9([email protected](@types/[email protected]))
2353
+ '@vitest/pretty-format': 2.1.9
2354
+ '@vitest/runner': 2.1.9
2355
+ '@vitest/snapshot': 2.1.9
2356
+ '@vitest/spy': 2.1.9
2357
+ '@vitest/utils': 2.1.9
2358
+ chai: 5.2.1
2359
+ debug: 4.4.1
2360
+ expect-type: 1.2.2
2361
+ magic-string: 0.30.17
2362
+ pathe: 1.1.2
2363
+ std-env: 3.9.0
2364
+ tinybench: 2.9.0
2365
+ tinyexec: 0.3.2
2366
+ tinypool: 1.1.1
2367
+ tinyrainbow: 1.2.0
2368
+ vite: 5.4.19(@types/[email protected])
2369
+ vite-node: 2.1.9(@types/[email protected])
2370
+ why-is-node-running: 2.3.0
2371
+ optionalDependencies:
2372
+ '@types/node': 18.19.123
2373
+ '@vitest/ui': 2.1.9([email protected])
2374
+ transitivePeerDependencies:
2375
+ - less
2376
+ - lightningcss
2377
+ - msw
2378
+ - sass
2379
+ - sass-embedded
2380
+ - stylus
2381
+ - sugarss
2382
+ - supports-color
2383
+ - terser
2384
+
2385
2386
+ dependencies:
2387
+ isexe: 2.0.0
2388
+
2389
2390
+ dependencies:
2391
+ siginfo: 2.0.0
2392
+ stackback: 0.0.2
2393
+
2394
2395
+
2396
packages/node/src/calibrate.ts ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Node.js calibration functionality using serialport API
3
+ * Provides both Python lerobot compatible CLI behavior and programmatic usage
4
+ * Uses proven calibration algorithms with web-compatible API
5
+ */
6
+
7
+ import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js";
8
+ import { createSO100Config } from "./robots/so100_config.js";
9
+ import {
10
+ readAllMotorPositions,
11
+ releaseMotors as releaseMotorsLowLevel,
12
+ type MotorCommunicationPort,
13
+ } from "./utils/motor-communication.js";
14
+ import {
15
+ setHomingOffsets,
16
+ writeHardwarePositionLimits,
17
+ } from "./utils/motor-calibration.js";
18
+ import { createInterface } from "readline";
19
+ import { writeFile } from "fs/promises";
20
+ import { join } from "path";
21
+ import { homedir } from "os";
22
+
23
+ // Debug logging removed - calibration working perfectly
24
+ import type {
25
+ CalibrateConfig,
26
+ CalibrationResults,
27
+ LiveCalibrationData,
28
+ CalibrationProcess,
29
+ } from "./types/calibration.js";
30
+ import type { RobotConnection } from "./types/robot-connection.js";
31
+
32
+ /**
33
+ * Get calibration file path (matches Python lerobot location)
34
+ */
35
+ function getCalibrationFilePath(robotType: string, robotId: string): string {
36
+ const HF_HOME =
37
+ process.env.HF_HOME || join(homedir(), ".cache", "huggingface");
38
+ const calibrationDir = join(
39
+ HF_HOME,
40
+ "lerobot",
41
+ "calibration",
42
+ "robots",
43
+ robotType
44
+ );
45
+ return join(calibrationDir, `${robotId}.json`);
46
+ }
47
+
48
+ /**
49
+ * Create readline interface for user input
50
+ */
51
+ function createReadlineInterface() {
52
+ return createInterface({
53
+ input: process.stdin,
54
+ output: process.stdout,
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Wait for user input with a prompt
60
+ */
61
+ function waitForInput(rl: any, prompt: string): Promise<string> {
62
+ return new Promise((resolve) => {
63
+ rl.question(prompt, (answer: string) => {
64
+ resolve(answer);
65
+ });
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Record ranges of motion with live updates
71
+ */
72
+ async function recordRangesOfMotion(
73
+ port: MotorCommunicationPort,
74
+ motorIds: number[],
75
+ motorNames: string[],
76
+ shouldStop: () => boolean,
77
+ onLiveUpdate?: (data: LiveCalibrationData) => void,
78
+ onProgress?: (message: string) => void
79
+ ): Promise<{
80
+ rangeMins: { [motor: string]: number };
81
+ rangeMaxes: { [motor: string]: number };
82
+ }> {
83
+ const rangeMins: { [motor: string]: number } = {};
84
+ const rangeMaxes: { [motor: string]: number } = {};
85
+
86
+ // Read actual current positions (now centered due to applied homing offsets)
87
+ const startPositions = await readAllMotorPositions(port, motorIds);
88
+
89
+ for (let i = 0; i < motorNames.length; i++) {
90
+ const motorName = motorNames[i];
91
+ rangeMins[motorName] = startPositions[i];
92
+ rangeMaxes[motorName] = startPositions[i];
93
+ }
94
+
95
+ if (onProgress) {
96
+ onProgress(
97
+ "Move each motor through its full range of motion. The ranges will be recorded automatically."
98
+ );
99
+ onProgress(
100
+ "Press Enter when you have finished moving all motors through their ranges."
101
+ );
102
+ } else {
103
+ console.log(
104
+ "Move each motor through its full range of motion. The ranges will be recorded automatically."
105
+ );
106
+ console.log(
107
+ "Press Enter when you have finished moving all motors through their ranges."
108
+ );
109
+ }
110
+
111
+ // Set up readline for user input
112
+ const rl = createReadlineInterface();
113
+ let isRecording = true;
114
+
115
+ // Start recording in background
116
+ const recordingInterval = setInterval(async () => {
117
+ if (!isRecording) return;
118
+
119
+ try {
120
+ const currentPositions = await readAllMotorPositions(port, motorIds);
121
+ const liveData: LiveCalibrationData = {};
122
+
123
+ for (let i = 0; i < motorNames.length; i++) {
124
+ const motorName = motorNames[i];
125
+ const position = currentPositions[i];
126
+
127
+ // Update ranges
128
+ rangeMins[motorName] = Math.min(rangeMins[motorName], position);
129
+ rangeMaxes[motorName] = Math.max(rangeMaxes[motorName], position);
130
+
131
+ // Build live data
132
+ liveData[motorName] = {
133
+ current: position,
134
+ min: rangeMins[motorName],
135
+ max: rangeMaxes[motorName],
136
+ range: rangeMaxes[motorName] - rangeMins[motorName],
137
+ };
138
+ }
139
+
140
+ if (onLiveUpdate) {
141
+ onLiveUpdate(liveData);
142
+ }
143
+ } catch (error) {
144
+ // Silent - continue recording
145
+ }
146
+ }, 100); // Update every 100ms
147
+
148
+ // Wait for user to finish
149
+ try {
150
+ await waitForInput(rl, "");
151
+ // IMMEDIATELY stop recording and live updates
152
+ isRecording = false;
153
+ clearInterval(recordingInterval);
154
+ } finally {
155
+ // Ensure cleanup even if there's an error
156
+ isRecording = false;
157
+ clearInterval(recordingInterval);
158
+ rl.close();
159
+ }
160
+
161
+ return { rangeMins, rangeMaxes };
162
+ }
163
+
164
+ /**
165
+ * Main calibrate function with web-compatible API
166
+ */
167
+ export async function calibrate(
168
+ config: CalibrateConfig
169
+ ): Promise<CalibrationProcess> {
170
+ const { robot, onLiveUpdate, onProgress, outputPath } = config;
171
+
172
+ // Validate robot configuration
173
+ if (!robot.robotType) {
174
+ throw new Error(
175
+ "Robot type is required for calibration. Please configure the robot first."
176
+ );
177
+ }
178
+
179
+ if (!robot.isConnected || !robot.port) {
180
+ throw new Error(
181
+ "Robot is not connected. Please use findPort() to connect first."
182
+ );
183
+ }
184
+
185
+ let shouldStop = false;
186
+ let port: NodeSerialPortWrapper | null = null;
187
+
188
+ const calibrationPromise = (async (): Promise<CalibrationResults> => {
189
+ try {
190
+ // Use the EXISTING port connection (don't create new one!)
191
+ port = robot.port;
192
+
193
+ // Get robot-specific configuration
194
+ let robotConfig;
195
+ if (robot.robotType.startsWith("so100")) {
196
+ robotConfig = createSO100Config(robot.robotType);
197
+ } else {
198
+ throw new Error(`Unsupported robot type: ${robot.robotType}`);
199
+ }
200
+
201
+ const { motorIds, motorNames, driveModes } = robotConfig;
202
+
203
+ // Debug logging removed - calibration working perfectly
204
+
205
+ // Starting calibration silently
206
+
207
+ // Step 1: Set homing offsets (motors should already be released and positioned)
208
+ // Note: Motors should be released BEFORE calling calibrate(), not inside it
209
+ // Setting homing offsets silently
210
+ const homingOffsets = await setHomingOffsets(port, motorIds, motorNames);
211
+
212
+ // Early debug test removed - calibration working perfectly
213
+
214
+ if (shouldStop) throw new Error("Calibration stopped by user");
215
+
216
+ // Step 2: Record ranges of motion silently
217
+ const { rangeMins, rangeMaxes } = await recordRangesOfMotion(
218
+ port,
219
+ motorIds,
220
+ motorNames,
221
+ () => shouldStop,
222
+ onLiveUpdate,
223
+ onProgress
224
+ );
225
+
226
+ if (shouldStop) throw new Error("Calibration stopped by user");
227
+
228
+ // Step 3: Write hardware position limits silently
229
+ await writeHardwarePositionLimits(
230
+ port,
231
+ motorIds,
232
+ motorNames,
233
+ rangeMins,
234
+ rangeMaxes
235
+ );
236
+
237
+ // Step 4: Skip motor locking (Python lerobot doesn't lock motors after calibration)
238
+
239
+ // Build calibration results (Python lerobot compatible format)
240
+
241
+ const calibrationResults: CalibrationResults = {};
242
+ for (let i = 0; i < motorNames.length; i++) {
243
+ const motorName = motorNames[i];
244
+ const homingOffsetValue = homingOffsets[motorName];
245
+
246
+ calibrationResults[motorName] = {
247
+ id: motorIds[i],
248
+ drive_mode: driveModes[i],
249
+ homing_offset: homingOffsetValue,
250
+ range_min: rangeMins[motorName],
251
+ range_max: rangeMaxes[motorName],
252
+ };
253
+ }
254
+
255
+ // Save calibration file
256
+ const calibrationPath =
257
+ outputPath ||
258
+ getCalibrationFilePath(robot.robotType, robot.robotId || "default");
259
+
260
+ // Ensure directory exists
261
+ const { mkdir } = await import("fs/promises");
262
+ const { dirname } = await import("path");
263
+ await mkdir(dirname(calibrationPath), { recursive: true });
264
+
265
+ await writeFile(
266
+ calibrationPath,
267
+ JSON.stringify(calibrationResults, null, 2)
268
+ );
269
+
270
+ if (onProgress) {
271
+ onProgress(`Calibration complete! Saved to: ${calibrationPath}`);
272
+ } else {
273
+ console.log(`Calibration complete! Saved to: ${calibrationPath}`);
274
+ }
275
+
276
+ return calibrationResults;
277
+ } finally {
278
+ // Note: Don't close the port - it belongs to the robot connection
279
+ }
280
+ })();
281
+
282
+ return {
283
+ stop(): void {
284
+ shouldStop = true;
285
+ },
286
+ result: calibrationPromise,
287
+ };
288
+ }
packages/node/src/find_port.ts ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Node.js port discovery using serialport API
3
+ * Provides programmatic port discovery compatible with @lerobot/web API
4
+ */
5
+
6
+ import { SerialPort } from "serialport";
7
+ import { platform } from "os";
8
+ import { readdir } from "fs/promises";
9
+ import { join } from "path";
10
+ import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js";
11
+ import type {
12
+ FindPortConfig,
13
+ FindPortProcess,
14
+ DiscoveredPort,
15
+ RobotConnection,
16
+ } from "./types/port-discovery.js";
17
+
18
+ /**
19
+ * Find available serial ports on the system
20
+ * Mirrors Python's find_available_ports() function
21
+ * Exported for CLI usage
22
+ */
23
+ export async function findAvailablePorts(): Promise<string[]> {
24
+ if (platform() === "win32") {
25
+ // List COM ports using serialport library (equivalent to pyserial)
26
+ const ports = await SerialPort.list();
27
+ return ports.map((port) => port.path);
28
+ } else {
29
+ // List /dev/tty* ports for Unix-based systems (Linux/macOS)
30
+ try {
31
+ const devFiles = await readdir("/dev");
32
+ const ttyPorts = devFiles
33
+ .filter((file) => file.startsWith("tty"))
34
+ .map((file) => join("/dev", file));
35
+ return ttyPorts;
36
+ } catch (error) {
37
+ // Fallback to serialport library if /dev reading fails
38
+ const ports = await SerialPort.list();
39
+ return ports.map((port) => port.path);
40
+ }
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Connect directly to a robot port (Python lerobot compatible)
46
+ * Equivalent to robot.connect() in Python lerobot
47
+ */
48
+ export async function connectPort(
49
+ portPath: string,
50
+ robotType: "so100_follower" | "so100_leader" = "so100_follower",
51
+ robotId: string = "robot"
52
+ ): Promise<RobotConnection> {
53
+ // Test connection
54
+ const port = new NodeSerialPortWrapper(portPath);
55
+ let isConnected = false;
56
+
57
+ try {
58
+ await port.initialize();
59
+ isConnected = true;
60
+ await port.close();
61
+ } catch (error) {
62
+ // Connection failed
63
+ }
64
+
65
+ // Return the ACTUAL working port, properly initialized!
66
+ const workingPort = new NodeSerialPortWrapper(portPath);
67
+
68
+ // Initialize the working port if connection test succeeded
69
+ if (isConnected) {
70
+ try {
71
+ await workingPort.initialize();
72
+ } catch (error) {
73
+ isConnected = false;
74
+ }
75
+ }
76
+
77
+ return {
78
+ port: workingPort, // ← Return the initialized working port!
79
+ name: `Robot on ${portPath}`,
80
+ robotType,
81
+ robotId,
82
+ isConnected,
83
+ serialNumber: portPath, // Use port path as serial number for Node.js
84
+ error: isConnected ? undefined : "Connection failed",
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Interactive mode: Return discovered robot ports (Node.js style)
90
+ * Unlike web version, this only discovers - user must call connectPort() separately
91
+ */
92
+ async function findPortInteractive(
93
+ options: FindPortConfig
94
+ ): Promise<DiscoveredPort[]> {
95
+ const { onMessage } = options;
96
+
97
+ onMessage?.("🔍 Searching for available robot ports...");
98
+
99
+ // Get all available ports
100
+ const availablePorts = await findAvailablePorts();
101
+
102
+ if (availablePorts.length === 0) {
103
+ throw new Error("No serial ports found");
104
+ }
105
+
106
+ onMessage?.(
107
+ `Found ${availablePorts.length} port(s), first available: ${availablePorts[0]}`
108
+ );
109
+
110
+ // Return discovered ports (no connection attempt)
111
+ return availablePorts.map((path) => ({
112
+ path,
113
+ robotType: "so100_follower" as const, // Default type, user can override
114
+ }));
115
+ }
116
+
117
+ /**
118
+ * Auto-connect mode: Connect to robots by serial number/port path
119
+ * Returns all connection attempts (successful and failed)
120
+ */
121
+ async function findPortAutoConnect(
122
+ robotConfigs: NonNullable<FindPortConfig["robotConfigs"]>,
123
+ options: FindPortConfig
124
+ ): Promise<RobotConnection[]> {
125
+ const { onMessage } = options;
126
+ const results: RobotConnection[] = [];
127
+
128
+ onMessage?.(`🔍 Auto-connecting to ${robotConfigs.length} robot(s)...`);
129
+
130
+ for (const config of robotConfigs) {
131
+ try {
132
+ onMessage?.(
133
+ `Connecting to ${config.robotId} (${config.serialNumber})...`
134
+ );
135
+
136
+ // Use serialNumber as port path for Node.js
137
+ const connection = await connectPort(config.serialNumber);
138
+
139
+ if (connection.isConnected) {
140
+ onMessage?.(`✅ Connected to ${config.robotId}`);
141
+ results.push({
142
+ ...connection,
143
+ robotType: config.robotType,
144
+ robotId: config.robotId,
145
+ serialNumber: config.serialNumber,
146
+ });
147
+ } else {
148
+ onMessage?.(`❌ Failed to connect to ${config.robotId}`);
149
+ results.push({
150
+ ...connection,
151
+ robotType: config.robotType,
152
+ robotId: config.robotId,
153
+ serialNumber: config.serialNumber,
154
+ isConnected: false,
155
+ error: connection.error || "Connection failed",
156
+ });
157
+ }
158
+ } catch (error) {
159
+ onMessage?.(
160
+ `❌ Error connecting to ${config.robotId}: ${
161
+ error instanceof Error ? error.message : error
162
+ }`
163
+ );
164
+ results.push({
165
+ port: {
166
+ path: config.serialNumber,
167
+ write: async () => {},
168
+ read: async () => null,
169
+ open: async () => {},
170
+ close: async () => {},
171
+ isOpen: false,
172
+ },
173
+ name: `Failed: ${config.robotId}`,
174
+ isConnected: false,
175
+ robotType: config.robotType,
176
+ robotId: config.robotId,
177
+ serialNumber: config.serialNumber,
178
+ error: error instanceof Error ? error.message : "Unknown error",
179
+ });
180
+ }
181
+ }
182
+
183
+ const successCount = results.filter((r) => r.isConnected).length;
184
+ onMessage?.(
185
+ `🎯 Connected to ${successCount}/${robotConfigs.length} robot(s)`
186
+ );
187
+
188
+ return results;
189
+ }
190
+
191
+ /**
192
+ * Main findPort function - Node.js discovery-only API
193
+ *
194
+ * Discovers available robot ports without connecting.
195
+ * User must call connectPort() separately to establish connections.
196
+ */
197
+ export async function findPort(
198
+ config: FindPortConfig = {}
199
+ ): Promise<FindPortProcess> {
200
+ const { onMessage } = config;
201
+ let stopped = false;
202
+
203
+ onMessage?.("🤖 Interactive port discovery started");
204
+
205
+ // Create result promise
206
+ const resultPromise = (async () => {
207
+ if (stopped) {
208
+ throw new Error("Port discovery was stopped");
209
+ }
210
+
211
+ return await findPortInteractive(config);
212
+ })();
213
+
214
+ // Return process object
215
+ return {
216
+ result: resultPromise,
217
+ stop: () => {
218
+ stopped = true;
219
+ onMessage?.("🛑 Port discovery stopped");
220
+ },
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Interactive port detection for CLI usage only
226
+ * Matches Python lerobot's unplug/replug cable detection exactly
227
+ * This function should only be used by the CLI, not the library
228
+ */
229
+ export async function detectPortInteractive(
230
+ onMessage?: (message: string) => void
231
+ ): Promise<string> {
232
+ const { createInterface } = await import("readline");
233
+
234
+ const rl = createInterface({
235
+ input: process.stdin,
236
+ output: process.stdout,
237
+ });
238
+
239
+ function waitForInput(prompt: string): Promise<string> {
240
+ return new Promise((resolve) => {
241
+ rl.question(prompt, (answer: string) => {
242
+ resolve(answer);
243
+ });
244
+ });
245
+ }
246
+
247
+ try {
248
+ const message = "Finding all available ports for the MotorsBus.";
249
+ if (onMessage) onMessage(message);
250
+ else console.log(message);
251
+
252
+ // Get initial port list
253
+ const portsBefore = await findAvailablePorts();
254
+
255
+ const disconnectPrompt =
256
+ "Remove the USB cable from your MotorsBus and press Enter when done.";
257
+ await waitForInput(disconnectPrompt);
258
+
259
+ // Get port list after disconnect
260
+ const portsAfter = await findAvailablePorts();
261
+
262
+ // Find the difference
263
+ const portsDiff = portsBefore.filter((port) => !portsAfter.includes(port));
264
+
265
+ if (portsDiff.length === 1) {
266
+ const detectedPort = portsDiff[0];
267
+ const successMessage = `Detected port: ${detectedPort}`;
268
+ if (onMessage) onMessage(successMessage);
269
+ else console.log(successMessage);
270
+
271
+ const reconnectPrompt =
272
+ "Reconnect the USB cable to your MotorsBus and press Enter when done.";
273
+ await waitForInput(reconnectPrompt);
274
+
275
+ // Verify the port is back
276
+ const portsReconnected = await findAvailablePorts();
277
+ if (portsReconnected.includes(detectedPort)) {
278
+ const verifyMessage = `Verified port: ${detectedPort}`;
279
+ if (onMessage) onMessage(verifyMessage);
280
+ else console.log(verifyMessage);
281
+ return detectedPort;
282
+ } else {
283
+ throw new Error("Port not found after reconnection");
284
+ }
285
+ } else if (portsDiff.length === 0) {
286
+ throw new Error(
287
+ "No port difference detected. Please check cable connection."
288
+ );
289
+ } else {
290
+ throw new Error(
291
+ `Multiple ports detected: ${portsDiff.join(
292
+ ", "
293
+ )}. Please disconnect other devices.`
294
+ );
295
+ }
296
+ } finally {
297
+ rl.close();
298
+ }
299
+ }
packages/node/src/index.ts ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @lerobot/node - Node.js-based robotics control using SerialPort API
3
+ *
4
+ * Control robotics hardware directly from Node.js applications, CLI tools, and desktop software.
5
+ */
6
+
7
+ // Core functions
8
+ export { calibrate } from "./calibrate.js";
9
+ export { teleoperate } from "./teleoperate.js";
10
+ export { findPort, connectPort } from "./find_port.js";
11
+ export { releaseMotors } from "./release_motors.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
+ FindPortConfig,
24
+ FindPortProcess,
25
+ DiscoveredPort,
26
+ } from "./types/port-discovery.js";
27
+
28
+ export type {
29
+ CalibrateConfig,
30
+ CalibrationResults,
31
+ LiveCalibrationData,
32
+ CalibrationProcess,
33
+ } from "./types/calibration.js";
34
+
35
+ export type {
36
+ MotorConfig,
37
+ TeleoperationState,
38
+ TeleoperationProcess,
39
+ TeleoperateConfig,
40
+ TeleoperatorConfig,
41
+ DirectTeleoperatorConfig,
42
+ } from "./types/teleoperation.js";
43
+
44
+ export type {
45
+ RobotHardwareConfig,
46
+ KeyboardControl,
47
+ } from "./types/robot-config.js";
48
+
49
+ // Utilities (advanced users)
50
+ export { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js";
51
+ export {
52
+ readAllMotorPositions,
53
+ readMotorPosition,
54
+ } from "./utils/motor-communication.js";
55
+ export {
56
+ createSO100Config,
57
+ SO100_KEYBOARD_CONTROLS,
58
+ } from "./robots/so100_config.js";
59
+ export { KEYBOARD_TELEOPERATOR_DEFAULTS } from "./teleoperators/index.js";
60
+ export {
61
+ getHfHome,
62
+ getHfLerobotHome,
63
+ getCalibrationDir,
64
+ } from "./utils/constants.js";
packages/node/src/release_motors.ts ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * User-facing motor release functionality for Node.js
3
+ * Simple API - pass in robotConnection, motors get released
4
+ *
5
+ * Handles robot configuration and port management internally
6
+ */
7
+
8
+ import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js";
9
+ import { createSO100Config } from "./robots/so100_config.js";
10
+ import { releaseMotors as releaseMotorsLowLevel } from "./utils/motor-communication.js";
11
+ import type { RobotConnection } from "./types/robot-connection.js";
12
+
13
+ /**
14
+ * Release robot motors (allows free movement by hand)
15
+ * Perfect for calibration setup or manual positioning
16
+ *
17
+ * @param robotConnection - Connected robot with configured type
18
+ * @param motorIds - Optional specific motor IDs to release (defaults to all motors for robot type)
19
+ * @throws Error if robot type not configured or motorIds invalid
20
+ */
21
+ export async function releaseMotors(
22
+ robotConnection: RobotConnection,
23
+ motorIds?: number[]
24
+ ): Promise<void> {
25
+ // Validate robot type is configured
26
+ if (!robotConnection.robotType) {
27
+ throw new Error(
28
+ "Robot type is required to release motors. Please configure the robot first."
29
+ );
30
+ }
31
+
32
+ // Validate robot connection
33
+ if (!robotConnection.isConnected || !robotConnection.port) {
34
+ throw new Error(
35
+ "Robot is not connected. Please use findPort() to connect first."
36
+ );
37
+ }
38
+
39
+ // Use the EXISTING port connection (don't create new one!)
40
+ const port = robotConnection.port;
41
+
42
+ // Get robot-specific configuration
43
+ let robotConfig;
44
+ if (robotConnection.robotType.startsWith("so100")) {
45
+ robotConfig = createSO100Config(robotConnection.robotType);
46
+ } else {
47
+ throw new Error(`Unsupported robot type: ${robotConnection.robotType}`);
48
+ }
49
+
50
+ // Determine which motors to release
51
+ const motorsToRelease = motorIds || robotConfig.motorIds;
52
+
53
+ // Validate motorIds are valid for this robot type
54
+ if (motorIds) {
55
+ const invalidMotors = motorIds.filter(
56
+ (id) => !robotConfig.motorIds.includes(id)
57
+ );
58
+ if (invalidMotors.length > 0) {
59
+ throw new Error(
60
+ `Invalid motor IDs [${invalidMotors.join(", ")}] for ${
61
+ robotConnection.robotType
62
+ }. Valid IDs: [${robotConfig.motorIds.join(", ")}]`
63
+ );
64
+ }
65
+ }
66
+
67
+ // Release the motors using low-level function
68
+ await releaseMotorsLowLevel(port, motorsToRelease);
69
+ // Note: Don't close the port - it belongs to the robot connection
70
+ }
packages/node/src/robots/so100_config.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * SO-100 specific hardware configuration
3
+ */
4
+
5
+ import type { RobotHardwareConfig } from "../types/robot-config.js";
6
+
7
+ /**
8
+ * STS3215 Protocol Configuration for SO-100 devices
9
+ */
10
+ export const NODE_STS3215_PROTOCOL = {
11
+ resolution: 4096, // 12-bit resolution (0-4095)
12
+ homingOffsetAddress: 31, // Address for Homing_Offset register
13
+ homingOffsetLength: 2, // 2 bytes for Homing_Offset
14
+ presentPositionAddress: 56, // Address for Present_Position register
15
+ presentPositionLength: 2, // 2 bytes for Present_Position
16
+ minPositionLimitAddress: 9, // Address for Min_Position_Limit register
17
+ minPositionLimitLength: 2, // 2 bytes for Min_Position_Limit
18
+ maxPositionLimitAddress: 11, // Address for Max_Position_Limit register
19
+ maxPositionLimitLength: 2, // 2 bytes for Max_Position_Limit
20
+ signMagnitudeBit: 11, // Bit 11 is sign bit for Homing_Offset encoding
21
+ } as const;
22
+
23
+ /**
24
+ * SO-100 Device Configuration
25
+ * Motor names, IDs, and drive modes for both follower and leader
26
+ */
27
+ export const SO100_CONFIG = {
28
+ motorNames: [
29
+ "shoulder_pan",
30
+ "shoulder_lift",
31
+ "elbow_flex",
32
+ "wrist_flex",
33
+ "wrist_roll",
34
+ "gripper",
35
+ ],
36
+ motorIds: [1, 2, 3, 4, 5, 6],
37
+ // All SO-100 motors use drive_mode=0
38
+ driveModes: [0, 0, 0, 0, 0, 0],
39
+ };
40
+
41
+ /**
42
+ * SO-100 Keyboard Controls for Teleoperation
43
+ * Robot-specific mapping optimized for SO-100 joint layout
44
+ */
45
+ export const SO100_KEYBOARD_CONTROLS = {
46
+ // Shoulder controls
47
+ ArrowUp: { motor: "shoulder_lift", direction: 1, description: "Shoulder up" },
48
+ ArrowDown: {
49
+ motor: "shoulder_lift",
50
+ direction: -1,
51
+ description: "Shoulder down",
52
+ },
53
+ ArrowLeft: {
54
+ motor: "shoulder_pan",
55
+ direction: -1,
56
+ description: "Shoulder left",
57
+ },
58
+ ArrowRight: {
59
+ motor: "shoulder_pan",
60
+ direction: 1,
61
+ description: "Shoulder right",
62
+ },
63
+
64
+ // WASD controls
65
+ w: { motor: "elbow_flex", direction: 1, description: "Elbow flex" },
66
+ s: { motor: "elbow_flex", direction: -1, description: "Elbow extend" },
67
+ a: { motor: "wrist_flex", direction: -1, description: "Wrist down" },
68
+ d: { motor: "wrist_flex", direction: 1, description: "Wrist up" },
69
+
70
+ // Wrist roll and gripper
71
+ q: { motor: "wrist_roll", direction: -1, description: "Wrist roll left" },
72
+ e: { motor: "wrist_roll", direction: 1, description: "Wrist roll right" },
73
+ o: { motor: "gripper", direction: 1, description: "Gripper open" },
74
+ c: { motor: "gripper", direction: -1, description: "Gripper close" },
75
+
76
+ // Emergency stop
77
+ Escape: {
78
+ motor: "emergency_stop",
79
+ direction: 0,
80
+ description: "Emergency stop",
81
+ },
82
+ } as const;
83
+
84
+ /**
85
+ * Create SO-100 hardware configuration
86
+ */
87
+ export function createSO100Config(
88
+ deviceType: "so100_follower" | "so100_leader"
89
+ ): RobotHardwareConfig {
90
+ return {
91
+ deviceType,
92
+ motorNames: SO100_CONFIG.motorNames,
93
+ motorIds: SO100_CONFIG.motorIds,
94
+ driveModes: SO100_CONFIG.driveModes,
95
+ keyboardControls: SO100_KEYBOARD_CONTROLS,
96
+ protocol: NODE_STS3215_PROTOCOL,
97
+ };
98
+ }
packages/node/src/teleoperate.ts ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Node.js teleoperation functionality using serialport API
3
+ * Provides both Python lerobot compatible CLI behavior and programmatic usage
4
+ * Uses proven teleoperator classes with web-compatible API
5
+ */
6
+
7
+ import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js";
8
+ import { createSO100Config } from "./robots/so100_config.js";
9
+ import { readAllMotorPositions } from "./utils/motor-communication.js";
10
+ import {
11
+ KeyboardTeleoperator,
12
+ DirectTeleoperator,
13
+ } from "./teleoperators/index.js";
14
+ import { readFile } from "fs/promises";
15
+ import { join } from "path";
16
+ import { homedir } from "os";
17
+ import { existsSync } from "fs";
18
+ import type {
19
+ TeleoperateConfig,
20
+ TeleoperationProcess,
21
+ MotorConfig,
22
+ TeleoperationState,
23
+ } from "./types/teleoperation.js";
24
+ import type { RobotConnection } from "./types/robot-connection.js";
25
+ import type { CalibrationResults } from "./types/calibration.js";
26
+
27
+ /**
28
+ * Get calibration file path (matches Python lerobot location)
29
+ */
30
+ function getCalibrationFilePath(robotType: string, robotId: string): string {
31
+ const HF_HOME =
32
+ process.env.HF_HOME || join(homedir(), ".cache", "huggingface");
33
+ const calibrationDir = join(
34
+ HF_HOME,
35
+ "lerobot",
36
+ "calibration",
37
+ "robots",
38
+ robotType
39
+ );
40
+ return join(calibrationDir, `${robotId}.json`);
41
+ }
42
+
43
+ /**
44
+ * Load calibration data from file system
45
+ */
46
+ async function loadCalibrationData(
47
+ robotType: string,
48
+ robotId: string
49
+ ): Promise<CalibrationResults | null> {
50
+ const calibrationPath = getCalibrationFilePath(robotType, robotId);
51
+
52
+ if (!existsSync(calibrationPath)) {
53
+ return null;
54
+ }
55
+
56
+ try {
57
+ const calibrationJson = await readFile(calibrationPath, "utf-8");
58
+ return JSON.parse(calibrationJson) as CalibrationResults;
59
+ } catch (error) {
60
+ console.warn(
61
+ `Failed to load calibration data from ${calibrationPath}:`,
62
+ error
63
+ );
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Build motor configurations from robot config and calibration data
70
+ */
71
+ function buildMotorConfigs(
72
+ robotConfig: any,
73
+ calibrationData?: CalibrationResults | null
74
+ ): MotorConfig[] {
75
+ const motorConfigs: MotorConfig[] = [];
76
+
77
+ for (let i = 0; i < robotConfig.motorNames.length; i++) {
78
+ const motorName = robotConfig.motorNames[i];
79
+ const motorId = robotConfig.motorIds[i];
80
+
81
+ let minPosition = 0;
82
+ let maxPosition = robotConfig.protocol.resolution - 1; // Default full range
83
+
84
+ // Use calibration data if available
85
+ if (calibrationData && calibrationData[motorName]) {
86
+ minPosition = calibrationData[motorName].range_min;
87
+ maxPosition = calibrationData[motorName].range_max;
88
+ }
89
+
90
+ motorConfigs.push({
91
+ id: motorId,
92
+ name: motorName,
93
+ currentPosition: Math.floor((minPosition + maxPosition) / 2), // Start at center
94
+ minPosition,
95
+ maxPosition,
96
+ });
97
+ }
98
+
99
+ return motorConfigs;
100
+ }
101
+
102
+ /**
103
+ * Main teleoperate function with web-compatible API
104
+ */
105
+ export async function teleoperate(
106
+ config: TeleoperateConfig
107
+ ): Promise<TeleoperationProcess> {
108
+ const { robot, teleop, calibrationData, onStateUpdate } = config;
109
+
110
+ // Validate robot configuration
111
+ if (!robot.robotType) {
112
+ throw new Error(
113
+ "Robot type is required for teleoperation. Please configure the robot first."
114
+ );
115
+ }
116
+
117
+ if (!robot.isConnected || !robot.port) {
118
+ throw new Error(
119
+ "Robot is not connected. Please use findPort() to connect first."
120
+ );
121
+ }
122
+
123
+ // Use the EXISTING port connection (don't create new one!)
124
+ const port = robot.port;
125
+
126
+ // Get robot-specific configuration
127
+ let robotConfig;
128
+ if (robot.robotType.startsWith("so100")) {
129
+ robotConfig = createSO100Config(robot.robotType);
130
+ } else {
131
+ throw new Error(`Unsupported robot type: ${robot.robotType}`);
132
+ }
133
+
134
+ // Load or use provided calibration data
135
+ let effectiveCalibrationData = calibrationData;
136
+ if (!effectiveCalibrationData && robot.robotId) {
137
+ effectiveCalibrationData = await loadCalibrationData(
138
+ robot.robotType,
139
+ robot.robotId
140
+ );
141
+ }
142
+
143
+ if (!effectiveCalibrationData) {
144
+ console.warn(
145
+ "No calibration data found. Using default motor ranges. Consider running calibration first."
146
+ );
147
+ }
148
+
149
+ // Build motor configurations
150
+ const motorConfigs = buildMotorConfigs(robotConfig, effectiveCalibrationData);
151
+
152
+ // Read current motor positions
153
+ try {
154
+ const currentPositions = await readAllMotorPositions(
155
+ port,
156
+ robotConfig.motorIds
157
+ );
158
+ for (let i = 0; i < motorConfigs.length; i++) {
159
+ motorConfigs[i].currentPosition = currentPositions[i];
160
+ }
161
+ } catch (error) {
162
+ console.warn("Failed to read initial motor positions:", error);
163
+ }
164
+
165
+ // Create appropriate teleoperator based on configuration
166
+ let teleoperator;
167
+ switch (teleop.type) {
168
+ case "keyboard":
169
+ teleoperator = new KeyboardTeleoperator(
170
+ teleop,
171
+ port,
172
+ motorConfigs,
173
+ robotConfig.keyboardControls,
174
+ onStateUpdate
175
+ );
176
+ break;
177
+
178
+ case "direct":
179
+ teleoperator = new DirectTeleoperator(teleop, port, motorConfigs);
180
+ break;
181
+
182
+ default:
183
+ throw new Error(`Unsupported teleoperator type: ${(teleop as any).type}`);
184
+ }
185
+
186
+ // Initialize teleoperator
187
+ await teleoperator.initialize();
188
+
189
+ // Build process object
190
+ const process: TeleoperationProcess = {
191
+ start(): void {
192
+ teleoperator.start();
193
+ },
194
+
195
+ stop(): void {
196
+ teleoperator.stop();
197
+ },
198
+
199
+ updateKeyState(key: string, pressed: boolean): void {
200
+ if ("updateKeyState" in teleoperator) {
201
+ (teleoperator as any).updateKeyState(key, pressed);
202
+ }
203
+ },
204
+
205
+ getState(): TeleoperationState {
206
+ const teleoperatorSpecificState = teleoperator.getState();
207
+ return {
208
+ isActive: teleoperator.isActiveTeleoperator,
209
+ motorConfigs: [...teleoperator.motorConfigs],
210
+ lastUpdate: Date.now(),
211
+ ...teleoperatorSpecificState,
212
+ };
213
+ },
214
+
215
+ teleoperator: teleoperator,
216
+
217
+ async disconnect(): Promise<void> {
218
+ await teleoperator.disconnect();
219
+ },
220
+ };
221
+
222
+ return process;
223
+ }
packages/node/src/teleoperators/base-teleoperator.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Base teleoperator interface and abstract class for Node.js platform
3
+ * Defines the contract that all teleoperators must implement
4
+ */
5
+
6
+ import type { MotorConfig } from "../types/teleoperation.js";
7
+ import type { MotorCommunicationPort } from "../utils/motor-communication.js";
8
+
9
+ /**
10
+ * Base interface that all Node.js teleoperators must implement
11
+ */
12
+ export interface NodeTeleoperator {
13
+ initialize(): Promise<void>;
14
+ start(): void;
15
+ stop(): void;
16
+ disconnect(): Promise<void>;
17
+ getState(): TeleoperatorSpecificState;
18
+ onMotorConfigsUpdate(motorConfigs: MotorConfig[]): void;
19
+ motorConfigs: MotorConfig[];
20
+ }
21
+
22
+ /**
23
+ * Teleoperator-specific state (union type for different teleoperator types)
24
+ */
25
+ export type TeleoperatorSpecificState = {
26
+ keyStates?: { [key: string]: { pressed: boolean; timestamp: number } }; // keyboard
27
+ leaderPositions?: { [motor: string]: number }; // leader arm
28
+ };
29
+
30
+ /**
31
+ * Base abstract class with common functionality for all teleoperators
32
+ */
33
+ export abstract class BaseNodeTeleoperator implements NodeTeleoperator {
34
+ protected port: MotorCommunicationPort;
35
+ public motorConfigs: MotorConfig[] = [];
36
+ protected isActive: boolean = false;
37
+
38
+ constructor(port: MotorCommunicationPort, motorConfigs: MotorConfig[]) {
39
+ this.port = port;
40
+ this.motorConfigs = motorConfigs;
41
+ }
42
+
43
+ abstract initialize(): Promise<void>;
44
+ abstract start(): void;
45
+ abstract stop(): void;
46
+ abstract getState(): TeleoperatorSpecificState;
47
+
48
+ async disconnect(): Promise<void> {
49
+ this.stop();
50
+ if (this.port && "close" in this.port) {
51
+ await (this.port as any).close();
52
+ }
53
+ }
54
+
55
+ onMotorConfigsUpdate(motorConfigs: MotorConfig[]): void {
56
+ this.motorConfigs = motorConfigs;
57
+ }
58
+
59
+ get isActiveTeleoperator(): boolean {
60
+ return this.isActive;
61
+ }
62
+ }
packages/node/src/teleoperators/direct-teleoperator.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Direct teleoperator for Node.js platform
3
+ * Provides programmatic control without user interface
4
+ */
5
+
6
+ import {
7
+ BaseNodeTeleoperator,
8
+ type TeleoperatorSpecificState,
9
+ } from "./base-teleoperator.js";
10
+ import type {
11
+ DirectTeleoperatorConfig,
12
+ MotorConfig,
13
+ } from "../types/teleoperation.js";
14
+ import type { MotorCommunicationPort } from "../utils/motor-communication.js";
15
+ import {
16
+ readMotorPosition,
17
+ writeMotorPosition,
18
+ } from "../utils/motor-communication.js";
19
+
20
+ /**
21
+ * Direct teleoperator provides programmatic motor control
22
+ * Use this when you want to control motors directly from code
23
+ */
24
+ export class DirectTeleoperator extends BaseNodeTeleoperator {
25
+ constructor(
26
+ config: DirectTeleoperatorConfig,
27
+ port: MotorCommunicationPort,
28
+ motorConfigs: MotorConfig[]
29
+ ) {
30
+ super(port, motorConfigs);
31
+ }
32
+
33
+ async initialize(): Promise<void> {
34
+ // Read current motor positions
35
+ for (const config of this.motorConfigs) {
36
+ const position = await readMotorPosition(this.port, config.id);
37
+ if (position !== null) {
38
+ config.currentPosition = position;
39
+ }
40
+ }
41
+ }
42
+
43
+ start(): void {
44
+ this.isActive = true;
45
+ }
46
+
47
+ stop(): void {
48
+ this.isActive = false;
49
+ }
50
+
51
+ getState(): TeleoperatorSpecificState {
52
+ return {};
53
+ }
54
+
55
+ /**
56
+ * Move motor to exact position (programmatic control)
57
+ */
58
+ async moveMotor(motorName: string, targetPosition: number): Promise<boolean> {
59
+ if (!this.isActive) return false;
60
+
61
+ const motorConfig = this.motorConfigs.find((m) => m.name === motorName);
62
+ if (!motorConfig) return false;
63
+
64
+ const clampedPosition = Math.max(
65
+ motorConfig.minPosition,
66
+ Math.min(motorConfig.maxPosition, targetPosition)
67
+ );
68
+
69
+ try {
70
+ await writeMotorPosition(
71
+ this.port,
72
+ motorConfig.id,
73
+ Math.round(clampedPosition)
74
+ );
75
+ motorConfig.currentPosition = clampedPosition;
76
+ return true;
77
+ } catch (error) {
78
+ console.warn(`Failed to move motor ${motorName}:`, error);
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Move multiple motors simultaneously
85
+ */
86
+ async moveMotors(
87
+ positions: { [motorName: string]: number }
88
+ ): Promise<{ [motorName: string]: boolean }> {
89
+ const results: { [motorName: string]: boolean } = {};
90
+
91
+ const promises = Object.entries(positions).map(
92
+ async ([motorName, position]) => {
93
+ const success = await this.moveMotor(motorName, position);
94
+ results[motorName] = success;
95
+ }
96
+ );
97
+
98
+ await Promise.all(promises);
99
+ return results;
100
+ }
101
+
102
+ /**
103
+ * Get current motor positions
104
+ */
105
+ getCurrentPositions(): { [motorName: string]: number } {
106
+ const positions: { [motorName: string]: number } = {};
107
+ for (const config of this.motorConfigs) {
108
+ positions[config.name] = config.currentPosition;
109
+ }
110
+ return positions;
111
+ }
112
+ }
packages/node/src/teleoperators/index.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Teleoperators barrel exports for Node.js
3
+ */
4
+
5
+ export {
6
+ BaseNodeTeleoperator,
7
+ type NodeTeleoperator,
8
+ type TeleoperatorSpecificState,
9
+ } from "./base-teleoperator.js";
10
+ export {
11
+ KeyboardTeleoperator,
12
+ KEYBOARD_TELEOPERATOR_DEFAULTS,
13
+ } from "./keyboard-teleoperator.js";
14
+ export { DirectTeleoperator } from "./direct-teleoperator.js";
packages/node/src/teleoperators/keyboard-teleoperator.ts ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Keyboard teleoperator for Node.js platform using stdin
3
+ */
4
+
5
+ import {
6
+ BaseNodeTeleoperator,
7
+ type TeleoperatorSpecificState,
8
+ } from "./base-teleoperator.js";
9
+ import type { KeyboardControl } from "../types/robot-config.js";
10
+ import type {
11
+ KeyboardTeleoperatorConfig,
12
+ MotorConfig,
13
+ TeleoperationState,
14
+ } from "../types/teleoperation.js";
15
+ import type { MotorCommunicationPort } from "../utils/motor-communication.js";
16
+ import {
17
+ readMotorPosition,
18
+ writeMotorPosition,
19
+ } from "../utils/motor-communication.js";
20
+
21
+ /**
22
+ * Default configuration values for keyboard teleoperator
23
+ */
24
+ export const KEYBOARD_TELEOPERATOR_DEFAULTS = {
25
+ stepSize: 8, // Keep browser demo step size
26
+ updateRate: 120, // Higher frequency for smoother movement (120 Hz)
27
+ keyTimeout: 150, // Shorter for better single taps, accept some gap on hold
28
+ } as const;
29
+
30
+ export class KeyboardTeleoperator extends BaseNodeTeleoperator {
31
+ private keyboardControls: { [key: string]: KeyboardControl } = {};
32
+ private updateInterval: NodeJS.Timeout | null = null;
33
+ private keyStates: {
34
+ [key: string]: { pressed: boolean; timestamp: number };
35
+ } = {};
36
+ private onStateUpdate?: (state: TeleoperationState) => void;
37
+
38
+ // Configuration values
39
+ private readonly stepSize: number;
40
+ private readonly updateRate: number;
41
+ private readonly keyTimeout: number;
42
+
43
+ constructor(
44
+ config: KeyboardTeleoperatorConfig,
45
+ port: MotorCommunicationPort,
46
+ motorConfigs: MotorConfig[],
47
+ keyboardControls: { [key: string]: KeyboardControl },
48
+ onStateUpdate?: (state: TeleoperationState) => void
49
+ ) {
50
+ super(port, motorConfigs);
51
+ this.keyboardControls = keyboardControls;
52
+ this.onStateUpdate = onStateUpdate;
53
+
54
+ // Set configuration values
55
+ this.stepSize = config.stepSize ?? KEYBOARD_TELEOPERATOR_DEFAULTS.stepSize;
56
+ this.updateRate =
57
+ config.updateRate ?? KEYBOARD_TELEOPERATOR_DEFAULTS.updateRate;
58
+ this.keyTimeout =
59
+ config.keyTimeout ?? KEYBOARD_TELEOPERATOR_DEFAULTS.keyTimeout;
60
+ }
61
+
62
+ async initialize(): Promise<void> {
63
+ // Set up stdin for raw keyboard input
64
+ if (process.stdin.setRawMode) {
65
+ process.stdin.setRawMode(true);
66
+ }
67
+ process.stdin.resume();
68
+ process.stdin.setEncoding("utf8");
69
+
70
+ // Set up keyboard input handler
71
+ process.stdin.on("data", this.handleKeyboardInput.bind(this));
72
+
73
+ // Read current motor positions
74
+ for (const config of this.motorConfigs) {
75
+ const position = await readMotorPosition(this.port, config.id);
76
+ if (position !== null) {
77
+ config.currentPosition = position;
78
+ }
79
+ }
80
+ }
81
+
82
+ start(): void {
83
+ if (this.isActive) return;
84
+
85
+ this.isActive = true;
86
+ this.updateInterval = setInterval(() => {
87
+ this.updateMotorPositions();
88
+ }, 1000 / this.updateRate);
89
+
90
+ // Display keyboard controls
91
+ this.displayControls();
92
+ }
93
+
94
+ stop(): void {
95
+ if (!this.isActive) return;
96
+
97
+ this.isActive = false;
98
+
99
+ if (this.updateInterval) {
100
+ clearInterval(this.updateInterval);
101
+ this.updateInterval = null;
102
+ }
103
+
104
+ // Clear all key states
105
+ this.keyStates = {};
106
+
107
+ // Notify of state change
108
+ if (this.onStateUpdate) {
109
+ this.onStateUpdate(this.buildTeleoperationState());
110
+ }
111
+ }
112
+
113
+ getState(): TeleoperatorSpecificState {
114
+ return {
115
+ keyStates: { ...this.keyStates },
116
+ };
117
+ }
118
+
119
+ updateKeyState(key: string, pressed: boolean): void {
120
+ this.keyStates[key] = {
121
+ pressed,
122
+ timestamp: Date.now(),
123
+ };
124
+ }
125
+
126
+ private handleKeyboardInput(key: string): void {
127
+ if (!this.isActive) return;
128
+
129
+ // Handle special keys
130
+ if (key === "\u0003") {
131
+ // Ctrl+C
132
+ process.exit(0);
133
+ }
134
+
135
+ if (key === "\u001b") {
136
+ // Escape
137
+ this.stop();
138
+ return;
139
+ }
140
+
141
+ // Handle regular keys - START IMMEDIATE CONTINUOUS MOVEMENT
142
+ const keyName = this.mapKeyToName(key);
143
+ if (keyName && this.keyboardControls[keyName]) {
144
+ // If key is already active, just refresh timestamp
145
+ if (this.keyStates[keyName]) {
146
+ this.keyStates[keyName].timestamp = Date.now();
147
+ } else {
148
+ // New key press - start continuous movement immediately
149
+ this.updateKeyState(keyName, true);
150
+
151
+ // Move immediately on first press (don't wait for interval)
152
+ this.moveMotorForKey(keyName);
153
+ }
154
+ }
155
+ }
156
+
157
+ private moveMotorForKey(keyName: string): void {
158
+ const control = this.keyboardControls[keyName];
159
+ if (!control || control.motor === "emergency_stop") return;
160
+
161
+ const motorConfig = this.motorConfigs.find((m) => m.name === control.motor);
162
+ if (!motorConfig) return;
163
+
164
+ // Calculate new position
165
+ const newPosition =
166
+ motorConfig.currentPosition + control.direction * this.stepSize;
167
+
168
+ // Apply limits
169
+ const clampedPosition = Math.max(
170
+ motorConfig.minPosition,
171
+ Math.min(motorConfig.maxPosition, newPosition)
172
+ );
173
+
174
+ // Send motor command immediately
175
+ writeMotorPosition(this.port, motorConfig.id, Math.round(clampedPosition))
176
+ .then(() => {
177
+ motorConfig.currentPosition = clampedPosition;
178
+ })
179
+ .catch((error) => {
180
+ console.warn(`Failed to move motor ${motorConfig.id}:`, error);
181
+ });
182
+ }
183
+
184
+ private updateMotorPositions(): void {
185
+ const now = Date.now();
186
+
187
+ // Clear timed-out keys
188
+ Object.keys(this.keyStates).forEach((key) => {
189
+ if (now - this.keyStates[key].timestamp > this.keyTimeout) {
190
+ delete this.keyStates[key];
191
+ }
192
+ });
193
+
194
+ // Process active keys
195
+ const activeKeys = Object.keys(this.keyStates).filter(
196
+ (key) =>
197
+ this.keyStates[key].pressed &&
198
+ now - this.keyStates[key].timestamp <= this.keyTimeout
199
+ );
200
+
201
+ // Emergency stop check
202
+ if (activeKeys.includes("Escape")) {
203
+ this.stop();
204
+ return;
205
+ }
206
+
207
+ // Calculate target positions based on active keys
208
+ const targetPositions: { [motorName: string]: number } = {};
209
+
210
+ for (const key of activeKeys) {
211
+ const control = this.keyboardControls[key];
212
+ if (!control || control.motor === "emergency_stop") continue;
213
+
214
+ const motorConfig = this.motorConfigs.find(
215
+ (m) => m.name === control.motor
216
+ );
217
+ if (!motorConfig) continue;
218
+
219
+ // Calculate new position
220
+ const currentTarget =
221
+ targetPositions[motorConfig.name] ?? motorConfig.currentPosition;
222
+ const newPosition = currentTarget + control.direction * this.stepSize;
223
+
224
+ // Apply limits
225
+ targetPositions[motorConfig.name] = Math.max(
226
+ motorConfig.minPosition,
227
+ Math.min(motorConfig.maxPosition, newPosition)
228
+ );
229
+ }
230
+
231
+ // Send motor commands and update positions
232
+ Object.entries(targetPositions).forEach(([motorName, targetPosition]) => {
233
+ const motorConfig = this.motorConfigs.find((m) => m.name === motorName);
234
+ if (motorConfig && targetPosition !== motorConfig.currentPosition) {
235
+ writeMotorPosition(
236
+ this.port,
237
+ motorConfig.id,
238
+ Math.round(targetPosition)
239
+ )
240
+ .then(() => {
241
+ motorConfig.currentPosition = targetPosition;
242
+ })
243
+ .catch((error) => {
244
+ console.warn(
245
+ `Failed to write motor ${motorConfig.id} position:`,
246
+ error
247
+ );
248
+ });
249
+ }
250
+ });
251
+ }
252
+
253
+ private mapKeyToName(key: string): string | null {
254
+ // Map stdin input to key names
255
+ const keyMap: { [key: string]: string } = {
256
+ "\u001b[A": "ArrowUp",
257
+ "\u001b[B": "ArrowDown",
258
+ "\u001b[C": "ArrowRight",
259
+ "\u001b[D": "ArrowLeft",
260
+ w: "w",
261
+ s: "s",
262
+ a: "a",
263
+ d: "d",
264
+ q: "q",
265
+ e: "e",
266
+ o: "o",
267
+ c: "c",
268
+ };
269
+
270
+ return keyMap[key] || null;
271
+ }
272
+
273
+ private displayControls(): void {
274
+ console.log("\n=== Robot Teleoperation Controls ===");
275
+ console.log("Arrow Keys: Shoulder pan/lift");
276
+ console.log("WASD: Elbow flex / Wrist flex");
277
+ console.log("Q/E: Wrist roll");
278
+ console.log("O/C: Gripper open/close");
279
+ console.log("ESC: Emergency stop");
280
+ console.log("Ctrl+C: Exit");
281
+ console.log("=====================================\n");
282
+ }
283
+
284
+ private buildTeleoperationState(): TeleoperationState {
285
+ return {
286
+ isActive: this.isActive,
287
+ motorConfigs: [...this.motorConfigs],
288
+ lastUpdate: Date.now(),
289
+ keyStates: { ...this.keyStates },
290
+ };
291
+ }
292
+ }
packages/node/src/types/calibration.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Calibration-related types for Node.js implementation
3
+ */
4
+
5
+ import type { RobotConnection } from "./robot-connection.js";
6
+
7
+ /**
8
+ * Live calibration data with current positions and ranges
9
+ */
10
+ export interface LiveCalibrationData {
11
+ [motorName: string]: {
12
+ current: number;
13
+ min: number;
14
+ max: number;
15
+ range: number;
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Config for calibrate function
21
+ */
22
+ export interface CalibrateConfig {
23
+ robot: RobotConnection;
24
+ onLiveUpdate?: (data: LiveCalibrationData) => void;
25
+ onProgress?: (message: string) => void;
26
+ outputPath?: string; // Node.js specific: custom output path for calibration file
27
+ }
28
+
29
+ /**
30
+ * Calibration results structure - Python lerobot compatible format
31
+ */
32
+ export interface CalibrationResults {
33
+ [motorName: string]: {
34
+ id: number;
35
+ drive_mode: number;
36
+ homing_offset: number;
37
+ range_min: number;
38
+ range_max: number;
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Calibration process control object
44
+ */
45
+ export interface CalibrationProcess {
46
+ stop(): void;
47
+ result: Promise<CalibrationResults>;
48
+ }
packages/node/src/types/port-discovery.ts ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Port discovery types for Node.js implementation using serialport
3
+ */
4
+
5
+ // Import types needed in this file
6
+ import type {
7
+ RobotConnection,
8
+ RobotConfig,
9
+ SerialPort,
10
+ SerialPortInfo,
11
+ } from "./robot-connection.js";
12
+
13
+ /**
14
+ * Config for findPort function
15
+ */
16
+ export interface FindPortConfig {
17
+ // Interactive mode: shows Python lerobot compatible prompts
18
+ interactive?: boolean;
19
+
20
+ // Auto-connect mode: provide robot configs to connect to
21
+ robotConfigs?: RobotConfig[];
22
+
23
+ // Callbacks
24
+ onMessage?: (message: string) => void;
25
+ }
26
+
27
+ /**
28
+ * Discovered port information (Node.js discovery-only mode)
29
+ */
30
+ export interface DiscoveredPort {
31
+ path: string; // Serial port path (e.g., "/dev/ttyUSB0", "COM4")
32
+ robotType: "so100_follower" | "so100_leader";
33
+ }
34
+
35
+ /**
36
+ * Process object returned by findPort
37
+ */
38
+ export interface FindPortProcess {
39
+ // Result promise - Node.js returns discovered ports, user calls connectPort() separately
40
+ result: Promise<DiscoveredPort[]>;
41
+
42
+ // Control
43
+ stop: () => void;
44
+ }
45
+
46
+ // Re-export commonly used types for convenience
47
+ export type { RobotConnection, RobotConfig, SerialPort, SerialPortInfo };
packages/node/src/types/robot-config.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Shared robot hardware configuration types
3
+ * Used across calibration, teleoperation, and other robot operations
4
+ */
5
+
6
+ /**
7
+ * Keyboard control mapping for teleoperation
8
+ */
9
+ export interface KeyboardControl {
10
+ motor: string;
11
+ direction: number;
12
+ description: string;
13
+ }
14
+
15
+ /**
16
+ * Robot hardware configuration interface
17
+ * Defines the contract that all robot configurations must implement
18
+ */
19
+ export interface RobotHardwareConfig {
20
+ deviceType: string;
21
+ motorNames: string[];
22
+ motorIds: number[];
23
+ driveModes: number[];
24
+
25
+ // Keyboard controls for teleoperation (robot-specific)
26
+ keyboardControls: { [key: string]: KeyboardControl };
27
+
28
+ protocol: {
29
+ resolution: number;
30
+ homingOffsetAddress: number;
31
+ homingOffsetLength: number;
32
+ presentPositionAddress: number;
33
+ presentPositionLength: number;
34
+ minPositionLimitAddress: number;
35
+ minPositionLimitLength: number;
36
+ maxPositionLimitAddress: number;
37
+ maxPositionLimitLength: number;
38
+ signMagnitudeBit: number;
39
+ };
40
+ }
packages/node/src/types/robot-connection.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Core robot connection types used across the lerobot.js Node.js library
3
+ * These types are shared between findPort, calibrate, teleoperate, and other modules
4
+ */
5
+
6
+ import type { RobotHardwareConfig } from "./robot-config.js";
7
+
8
+ /**
9
+ * Type definitions for Node.js serialport API
10
+ */
11
+ export interface SerialPort {
12
+ path: string;
13
+ write(buffer: Buffer): Promise<void>;
14
+ read(): Promise<Buffer | null>;
15
+ open(): Promise<void>;
16
+ close(): Promise<void>;
17
+ isOpen: boolean;
18
+ }
19
+
20
+ export interface SerialPortInfo {
21
+ path: string;
22
+ manufacturer?: string;
23
+ serialNumber?: string;
24
+ pnpId?: string;
25
+ locationId?: string;
26
+ productId?: string;
27
+ vendorId?: string;
28
+ }
29
+
30
+ export interface SerialOptions {
31
+ baudRate: number;
32
+ dataBits?: number;
33
+ stopBits?: number;
34
+ parity?: "none" | "even" | "odd";
35
+ }
36
+
37
+ /**
38
+ * Unified robot connection interface used across all functions
39
+ * This same object works for findPort, calibrate, teleoperate, etc.
40
+ * Includes all fields needed by CLI and other applications
41
+ */
42
+ export interface RobotConnection {
43
+ port: SerialPort;
44
+ name: string; // Display name for CLI
45
+ isConnected: boolean; // Connection status
46
+ robotType?: "so100_follower" | "so100_leader"; // Optional until user configures
47
+ robotId?: string; // Optional until user configures
48
+ serialNumber: string; // Always required for identification
49
+ error?: string; // Error message if connection failed
50
+ config?: RobotHardwareConfig; // Robot configuration (motorIds, controls, etc.) - set when robotType is configured
51
+ portInfo?: SerialPortInfo; // Node.js serial port information
52
+ }
53
+
54
+ /**
55
+ * Minimal robot config for finding/connecting to specific robots
56
+ */
57
+ export interface RobotConfig {
58
+ robotType: "so100_follower" | "so100_leader";
59
+ robotId: string;
60
+ serialNumber: string;
61
+ }
packages/node/src/types/teleoperation.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Teleoperation-related types for Node.js implementation
3
+ */
4
+
5
+ import type { RobotConnection } from "./robot-connection.js";
6
+ import type { NodeTeleoperator } from "../teleoperators/index.js";
7
+
8
+ /**
9
+ * Motor position and limits for teleoperation
10
+ */
11
+ export interface MotorConfig {
12
+ id: number;
13
+ name: string;
14
+ currentPosition: number;
15
+ minPosition: number;
16
+ maxPosition: number;
17
+ }
18
+
19
+ /**
20
+ * Teleoperation state
21
+ */
22
+ export interface TeleoperationState {
23
+ isActive: boolean;
24
+ motorConfigs: MotorConfig[];
25
+ lastUpdate: number;
26
+
27
+ // Teleoperator-specific state (optional fields for different types)
28
+ keyStates?: { [key: string]: { pressed: boolean; timestamp: number } }; // keyboard
29
+ leaderPositions?: { [motor: string]: number }; // leader arm
30
+ }
31
+
32
+ /**
33
+ * Teleoperation process control object
34
+ */
35
+ export interface TeleoperationProcess {
36
+ start(): void;
37
+ stop(): void;
38
+ updateKeyState(key: string, pressed: boolean): void;
39
+ getState(): TeleoperationState;
40
+ teleoperator: NodeTeleoperator;
41
+ disconnect(): Promise<void>;
42
+ }
43
+
44
+ /**
45
+ * Base interface for all teleoperator configurations
46
+ */
47
+ export interface BaseTeleoperatorConfig {
48
+ type: string;
49
+ }
50
+
51
+ /**
52
+ * Keyboard teleoperator configuration
53
+ */
54
+ export interface KeyboardTeleoperatorConfig extends BaseTeleoperatorConfig {
55
+ type: "keyboard";
56
+ stepSize?: number; // Default: KEYBOARD_TELEOPERATOR_DEFAULTS.stepSize
57
+ updateRate?: number; // Default: KEYBOARD_TELEOPERATOR_DEFAULTS.updateRate
58
+ keyTimeout?: number; // Default: KEYBOARD_TELEOPERATOR_DEFAULTS.keyTimeout
59
+ }
60
+
61
+ /**
62
+ * Leader arm teleoperator configuration (future)
63
+ */
64
+ export interface LeaderArmTeleoperatorConfig extends BaseTeleoperatorConfig {
65
+ type: "so100_leader";
66
+ port: string;
67
+ calibrationData?: any;
68
+ positionSmoothing?: boolean;
69
+ scaleFactor?: number;
70
+ }
71
+
72
+ /**
73
+ * Direct teleoperator configuration
74
+ */
75
+ export interface DirectTeleoperatorConfig extends BaseTeleoperatorConfig {
76
+ type: "direct";
77
+ }
78
+
79
+ /**
80
+ * Union type for all teleoperator configurations
81
+ */
82
+ export type TeleoperatorConfig =
83
+ | KeyboardTeleoperatorConfig
84
+ | LeaderArmTeleoperatorConfig
85
+ | DirectTeleoperatorConfig;
86
+
87
+ /**
88
+ * Main teleoperation configuration
89
+ */
90
+ export interface TeleoperateConfig {
91
+ robot: RobotConnection;
92
+ teleop: TeleoperatorConfig;
93
+ calibrationData?: { [motorName: string]: any };
94
+ onStateUpdate?: (state: TeleoperationState) => void;
95
+ }
{src/lerobot/node → packages/node/src}/utils/constants.ts RENAMED
@@ -1,15 +1,11 @@
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
 
1
  /**
2
+ * Constants for @lerobot/node
3
+ * Mirrors Python lerobot/common/constants.py for directory structure compatibility
4
  */
5
 
6
  import { homedir } from "os";
7
  import { join } from "path";
8
 
 
 
 
 
9
  /**
10
  * Get HF Home directory
11
  * Equivalent to Python's huggingface_hub.constants.HF_HOME
packages/node/src/utils/motor-calibration.ts ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Motor Calibration Utilities
3
+ * Specialized functions for motor calibration procedures
4
+ */
5
+
6
+ import { STS3215_PROTOCOL } from "./sts3215-protocol.js";
7
+ import { encodeSignMagnitude } from "./sign-magnitude.js";
8
+ import {
9
+ readAllMotorPositions,
10
+ writeMotorRegister,
11
+ type MotorCommunicationPort,
12
+ } from "./motor-communication.js";
13
+
14
+ /**
15
+ * Reset homing offsets to 0 for all motors
16
+ */
17
+ export async function resetHomingOffsets(
18
+ port: MotorCommunicationPort,
19
+ motorIds: number[]
20
+ ): Promise<void> {
21
+ for (let i = 0; i < motorIds.length; i++) {
22
+ const motorId = motorIds[i];
23
+
24
+ try {
25
+ const packet = new Uint8Array([
26
+ 0xff,
27
+ 0xff,
28
+ motorId,
29
+ 0x05,
30
+ 0x03,
31
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS,
32
+ 0x00, // Low byte of 0
33
+ 0x00, // High byte of 0
34
+ 0x00, // Checksum
35
+ ]);
36
+
37
+ const checksum =
38
+ ~(
39
+ motorId +
40
+ 0x05 +
41
+ 0x03 +
42
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS +
43
+ 0x00 +
44
+ 0x00
45
+ ) & 0xff;
46
+ packet[8] = checksum;
47
+
48
+ await port.write(packet);
49
+
50
+ try {
51
+ await port.read(200);
52
+ } catch (error) {
53
+ // Silent - response not required
54
+ }
55
+ } catch (error) {
56
+ throw new Error(`Failed to reset homing offset for motor ${motorId}`);
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Write homing offsets to motor registers immediately
63
+ */
64
+ export async function writeHomingOffsetsToMotors(
65
+ port: MotorCommunicationPort,
66
+ motorIds: number[],
67
+ motorNames: string[],
68
+ homingOffsets: { [motor: string]: number }
69
+ ): Promise<void> {
70
+ for (let i = 0; i < motorIds.length; i++) {
71
+ const motorId = motorIds[i];
72
+ const motorName = motorNames[i];
73
+ const homingOffset = homingOffsets[motorName];
74
+
75
+ try {
76
+ const encodedOffset = encodeSignMagnitude(
77
+ homingOffset,
78
+ STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT
79
+ );
80
+
81
+ const packet = new Uint8Array([
82
+ 0xff,
83
+ 0xff,
84
+ motorId,
85
+ 0x05,
86
+ 0x03,
87
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS,
88
+ encodedOffset & 0xff,
89
+ (encodedOffset >> 8) & 0xff,
90
+ 0x00,
91
+ ]);
92
+
93
+ const checksum =
94
+ ~(
95
+ motorId +
96
+ 0x05 +
97
+ 0x03 +
98
+ STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS +
99
+ (encodedOffset & 0xff) +
100
+ ((encodedOffset >> 8) & 0xff)
101
+ ) & 0xff;
102
+ packet[8] = checksum;
103
+
104
+ await port.write(packet);
105
+
106
+ try {
107
+ await port.read(200);
108
+ } catch (error) {
109
+ // Silent - response not required
110
+ }
111
+ } catch (error) {
112
+ throw new Error(`Failed to write homing offset for ${motorName}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Set homing offsets with immediate writing
119
+ */
120
+ export async function setHomingOffsets(
121
+ port: MotorCommunicationPort,
122
+ motorIds: number[],
123
+ motorNames: string[]
124
+ ): Promise<{ [motor: string]: number }> {
125
+ // Reset existing homing offsets to 0 first
126
+ await resetHomingOffsets(port, motorIds);
127
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second instead of 100ms
128
+
129
+ // Flush any cached position readings first
130
+ await readAllMotorPositions(port, motorIds); // Dummy read to flush cache
131
+ await new Promise((resolve) => setTimeout(resolve, 200)); // Small additional wait
132
+
133
+ // Read positions (which should now be true physical positions)
134
+ const currentPositions = await readAllMotorPositions(port, motorIds);
135
+
136
+ const homingOffsets: { [motor: string]: number } = {};
137
+ const halfTurn = Math.floor((STS3215_PROTOCOL.RESOLUTION - 1) / 2);
138
+
139
+ for (let i = 0; i < motorNames.length; i++) {
140
+ const motorName = motorNames[i];
141
+ const position = currentPositions[i];
142
+ const calculatedOffset = position - halfTurn;
143
+ homingOffsets[motorName] = calculatedOffset;
144
+ }
145
+
146
+ // Write homing offsets to motors immediately
147
+ await writeHomingOffsetsToMotors(port, motorIds, motorNames, homingOffsets);
148
+
149
+ return homingOffsets;
150
+ }
151
+
152
+ /**
153
+ * Write hardware position limits to motors
154
+ */
155
+ export async function writeHardwarePositionLimits(
156
+ port: MotorCommunicationPort,
157
+ motorIds: number[],
158
+ motorNames: string[],
159
+ rangeMins: { [motor: string]: number },
160
+ rangeMaxes: { [motor: string]: number }
161
+ ): Promise<void> {
162
+ for (let i = 0; i < motorIds.length; i++) {
163
+ const motorId = motorIds[i];
164
+ const motorName = motorNames[i];
165
+ const minLimit = rangeMins[motorName];
166
+ const maxLimit = rangeMaxes[motorName];
167
+
168
+ try {
169
+ // Write Min_Position_Limit register
170
+ await writeMotorRegister(
171
+ port,
172
+ motorId,
173
+ STS3215_PROTOCOL.MIN_POSITION_LIMIT_ADDRESS,
174
+ minLimit
175
+ );
176
+
177
+ // Write Max_Position_Limit register
178
+ await writeMotorRegister(
179
+ port,
180
+ motorId,
181
+ STS3215_PROTOCOL.MAX_POSITION_LIMIT_ADDRESS,
182
+ maxLimit
183
+ );
184
+ } catch (error) {
185
+ throw new Error(`Failed to write position limits for ${motorName}`);
186
+ }
187
+ }
188
+ }