diff --git a/README.md b/README.md index 3319659966fdc0b2ebd14e8ec6b24e0d4ac47791..ba25a3659a95cc4b425427b3d7233054059c9213 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,17 @@ ## Install ```bash -# Web library (available now) +# Web library npm install @lerobot/web -# Node.js library (coming soon) -# npm install @lerobot/node +# Node.js library +npm install @lerobot/node ``` ## Resources - **LeRobot.js**: [Introduction post on Hugging Face](https://huggingface.co/blog/NERDDISCO/lerobotjs) -- **Documentation**: See [`@lerobot/web` README](./packages/web/README.md) for complete API reference +- **Documentation**: + - [`@lerobot/web` README](./packages/web/README.md) - Browser (WebSerial + WebUSB) + - [`@lerobot/node` README](./packages/node/README.md) - Node.js (Serialport) - **Live Demo**: Try it online at [huggingface.co/spaces/NERDDISCO/LeRobot.js](https://huggingface.co/spaces/NERDDISCO/LeRobot.js) diff --git a/docs/conventions.md b/docs/conventions.md index 9df131b56ab575d1e5bf0f11333838791ce4e17e..500464a6cb62956145ca6f6a88ec007d259badfd 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -11,6 +11,7 @@ ## Core Rules - **Never Start/Stop Dev Server**: The development server is already managed by the user - never run commands to start, stop, or restart the server +- **Package Manager**: Always use `pnpm` for package management - never use `npm` or `yarn` in documentation, scripts, or commands - **Python lerobot Faithfulness**: Maintain exact UX/API compatibility with Python lerobot - commands, terminology, and workflows must match identically - **Serial API Separation**: Always use `serialport` package for Node.js and Web Serial API for browsers - never mix or bridge these incompatible APIs - **Minimal Console Output**: Only show essential information - reduce cognitive load for users @@ -74,7 +75,7 @@ - **Identical Commands**: `npx lerobot find-port` matches `python -m lerobot.find_port` - **Same Terminology**: Use "MotorsBus", not "robot arms" - keep Python's exact wording -- **Matching Output**: Error messages, prompts, and flow identical to Python version +- **Matching Output**: Error messages, prompts, and flow identical to Python lerobot - **Familiar Workflows**: Python lerobot users should feel immediately at home - **CLI Compatibility**: Direct migration path from Python CLI @@ -193,7 +194,7 @@ lerobot/ - **Integration Tests**: Test component interactions - **E2E Tests**: Playwright for full workflow testing - **Hardware Tests**: Mock/stub hardware interfaces for CI -- **UX Compatibility Tests**: Verify outputs match Python version +- **UX Compatibility Tests**: Verify outputs match Python lerobot ## Package Structure @@ -218,6 +219,69 @@ lerobot/ - **Hardware**: Platform-specific libraries for device access - **Development**: Vitest, ESLint, Prettier +### Package Architecture Standards + +**Multi-Platform Package Structure:** + +``` +packages/ +├── web/ # Browser-focused package (@lerobot/web) +│ ├── src/ +│ ├── package.json # Web Serial API, browser dependencies +│ └── README.md # Browser-specific examples +├── node/ # Node.js-focused package (@lerobot/node) +│ ├── src/ +│ ├── package.json # serialport dependency, library only +│ └── README.md # Node.js library examples +└── cli/ # CLI package (lerobot) + ├── src/ + ├── package.json # CLI binary, depends on @lerobot/node + └── README.md # Python lerobot compatible commands +``` + +**API Consistency Rules:** + +- Identical function signatures across packages where possible +- Platform-specific adaptations in types and implementations only +- Shared constants and protocols via dedicated utils +- Cross-platform compatibility for data formats (calibration files, etc.) + +**CLI Architecture & Separation of Concerns:** + +- **Library (`@lerobot/node`)**: Pure programmatic API for Node.js applications + + - `findPort()` returns robot connections programmatically + - No interactive prompts, CLI output, or user input handling + - Matches `@lerobot/web` API design for consistency + +- **CLI (`lerobot`)**: Python lerobot compatible command-line interface + + - Separate package in `packages/cli/` with `npx lerobot` binary + - Uses `@lerobot/node` library internally for all functionality + - Handles interactive prompts, user input, and CLI-specific UX + - Interactive by default - no flags required for standard workflows + - Identical command syntax and behavior to Python lerobot + +- **Architectural Principle**: Libraries provide capabilities, CLIs provide experience + - Interactive behavior belongs in CLI commands, not library functions + - Library users get clean APIs, CLI users get Python-compatible workflows + +### Local Package References + +When creating examples that depend on workspace packages, use `file:` references: + +```json +{ + "dependencies": { + "@lerobot/web": "file:../../packages/web", + "@lerobot/node": "file:../../packages/node", + "lerobot": "file:../../packages/cli" + } +} +``` + +**Never use `workspace:*` in examples** - this is only for the root workspace `package.json`. + ## Platform-Specific Implementation ### Node.js Implementation (Python-Compatible Foundation) @@ -239,6 +303,45 @@ lerobot/ - **Port Discovery**: Programmatic port enumeration without user dialogs - **Process Management**: Direct process control and system integration +#### Node.js Serial Communication (Critical Implementation Details) + +**Event-Driven Communication (Proven Working Approach):** + +- **Event-Based Reading**: Use `port.once('data')` with timeout promises +- **Never Use Wrapper Polling**: Avoid `port.read(timeout)` wrappers - they add latency and unreliability +- **Direct SerialPort Access**: Expose underlying `SerialPort` instance for event listening + +**Timing Constants for STS3215 Motors:** + +```typescript +const STS3215_PROTOCOL = { + WRITE_TO_READ_DELAY: 0, // No delay before read - immediate event listening + RETRY_DELAY: 50, // Base retry delay (multiplied by attempt number) + INTER_MOTOR_DELAY: 10, // Small delay between motor operations + MAX_RETRIES: 3, +}; +``` + +**Progressive Timeout Pattern:** + +```typescript +// Timeout increases with retry attempts: 100ms, 200ms, 300ms +const timeout = 100 * attempts; +const response = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("Read timeout")), timeout); + underlyingPort.once("data", (data) => { + clearTimeout(timer); + resolve(new Uint8Array(data)); + }); +}); +``` + +**Connection Architecture:** + +- **Only `findPort()` Creates Connections**: No other high-level function should create new serial connections +- **Initialized Port Returns**: `findPort()` must return ready-to-use, initialized ports +- **Connection Reuse**: All functions (`calibrate`, `teleoperate`, `releaseMotors`) use existing connection from `findPort` + ### Web Implementation (Modern Robotics Interface) **Web provides superior robotics UX by building on Node.js's proven hardware protocols** @@ -583,83 +686,124 @@ const STS3215_REGISTERS = { **This sequence debugging took extensive analysis to solve. Future implementations MUST follow this exact pattern to maintain Python compatibility.** -#### CRITICAL: Smooth Motor Control Recipe (PROVEN WORKING) +#### CRITICAL: Node.js Teleoperation Patterns (PROVEN WORKING) + +**These patterns provide smooth, responsive teleoperation in Node.js. Deviating from this recipe causes delays, stuttering, or poor user experience.** + +##### 🚀 Node.js Keyboard Control Architecture (FINAL SOLUTION) -**These patterns provide buttery-smooth, responsive motor control. Deviating from this recipe causes stuttering, lag, or poor responsiveness.** +**The Challenge: Node.js stdin vs Browser Keyboard Events** -##### 🚀 Performance Optimizations (KEEP THESE!) +- **Browser**: Has real `keydown` and `keyup` events → perfect control +- **Node.js**: Only has `keypress` events → must simulate keyup with timeouts +- **OS Keyboard Repeat**: ~250-500ms delay between first press and repeat stream + +**✅ PROVEN WORKING SOLUTION:** + +```typescript +// Optimal configuration values (DO NOT CHANGE without extensive testing) +export const KEYBOARD_TELEOPERATOR_DEFAULTS = { + stepSize: 8, // Match browser demo step size + updateRate: 120, // High frequency for smooth movement (120 Hz) + keyTimeout: 150, // Balance single taps vs continuous movement +} as const; +``` + +**1. Hybrid Movement Pattern** + +- **✅ PERFECT**: Immediate movement on first keypress + continuous interval updates +- **❌ WRONG**: Only immediate movement (no continuous) or only interval movement (has delay) + +```typescript +// On keypress: Move immediately + start/refresh continuous movement +private handleKeyboardInput(key: string): void { + const keyName = this.mapKeyToName(key); + if (keyName && this.keyboardControls[keyName]) { + if (this.keyStates[keyName]) { + // Key repeat - just refresh timestamp + this.keyStates[keyName].timestamp = Date.now(); + } else { + // New keypress - immediate movement + start continuous + this.updateKeyState(keyName, true); + this.moveMotorForKey(keyName); // ← IMMEDIATE, no delay + } + } +} +``` -**1. Optimal Step Size** +**2. Optimal Key Timeout Balance** -- **✅ PERFECT**: `25` units per keypress (responsive but not jumpy) -- **❌ WRONG**: `5` units (too sluggish) or `100` units (too aggressive) +- **✅ PERFECT**: `150ms` - Good single taps, minimal continuous gap +- **❌ WRONG**: `50ms` (single taps too short) or `600ms` (single taps too long) +- **Why 150ms**: Bridges most OS keyboard repeat delay without making single taps sluggish -**2. Minimal Motor Communication Delay** +**3. High-Frequency Updates** -- **✅ PERFECT**: `1ms` delay between motor commands -- **❌ WRONG**: `5ms+` delays cause stuttering +- **✅ PERFECT**: `120 Hz` update rate for smooth continuous movement +- **❌ WRONG**: `60 Hz` (visible stuttering) or `200+ Hz` (unnecessary CPU load) -**3. Smart Motor Updates (CRITICAL FOR SMOOTHNESS)** +##### 🎯 Development Workflow Optimization -- **✅ PERFECT**: Only send commands for motors that actually changed -- **✅ PERFECT**: Use `0.5` unit threshold to detect meaningful changes -- **❌ WRONG**: Send ALL motor positions every time (causes serial bus conflicts) +**Package Development Without Constant Rebuilding:** -**4. Change Detection Threshold** +**✅ PERFECT Development Setup:** -- **✅ PERFECT**: `0.5` units prevents micro-movements and unnecessary commands -- **❌ WRONG**: `0.1` units (too sensitive) or no threshold (constant spam) +1. **Terminal 1**: `cd packages/node && pnpm dev` (watch mode) +2. **Terminal 2**: `cd packages/cli && pnpm dev teleoperate ...` (direct TypeScript execution) -##### 🎯 Teleoperation Loop Best Practices +**❌ WRONG**: Constantly running `pnpm build` and clearing `node_modules` -**1. Eliminate Display Spam** +**Why This Works:** -- **✅ PERFECT**: Minimal loop with just duration checks and 100ms delay -- **❌ WRONG**: Constant position reading and display updates (causes 90ms+ lag) +- Node package rebuilds automatically on changes +- CLI dev mode uses `vite-node` to run TypeScript directly +- No package caching issues, immediate feedback -**2. Event-Driven Keyboard Input** +##### 🔧 CLI Architecture Lessons -- **✅ PERFECT**: Use `process.stdin.on("data")` for immediate response -- **❌ WRONG**: Polling-based input with timers (adds delay) +**1. No User-Facing Configuration** -##### 🔧 Hardware Communication Patterns +- **✅ PERFECT**: `stepSize` handled internally by teleoperator defaults +- **❌ WRONG**: Exposing `--step-size` CLI parameter (users don't understand motor units) -**1. Discrete Step-Based Control** +**2. Python lerobot Parameter Compatibility** -- **✅ PERFECT**: Immediate position updates on keypress -- **❌ WRONG**: Continuous/velocity-based control (causes complexity and lag) +- **✅ PERFECT**: `--robot.type`, `--robot.port`, `--robot.id`, `--teleop.type` +- **❌ WRONG**: Different parameter names or structure than Python lerobot -**2. Direct Motor Position Writing** +**3. Library vs CLI Separation** -- **✅ PERFECT**: Simple, immediate motor updates with position limits -- **❌ WRONG**: Complex interpolation, target positions, multiple update cycles +- **✅ PERFECT**: Library provides capabilities, CLI provides Python-compatible UX +- **❌ WRONG**: Library handling CLI concerns or CLI reimplementing library logic -##### 🎮 Proven Working Values +##### 🎮 Performance Characteristics (When Working Right) -**Key Configuration Values:** +- **First Keypress Response**: Immediate (0ms delay) +- **Continuous Movement**: Smooth 120 Hz updates +- **Single Tap Duration**: ~150ms (1-2 motor movements) +- **Key Repeat Transition**: Seamless (no gap) +- **User Experience**: "Almost perfect", "way better", "smooth movement" -- `stepSize = 25` (default in teleoperate.ts and keyboard_teleop.ts) -- `1ms` motor communication delay (so100_follower.ts) -- `0.5` unit change detection threshold -- `100ms` teleoperation loop delay +##### ⚠️ Node.js Teleoperation Anti-Patterns (NEVER DO THESE) -##### ⚠️ Performance Killers (NEVER DO THESE) +1. **❌ Only Timeout-Based Movement**: Causes initial delay on every keypress +2. **❌ Only Immediate Movement**: No continuous movement when holding keys +3. **❌ Long Key Timeouts (>300ms)**: Makes single taps feel sluggish +4. **❌ Short Key Timeouts (<100ms)**: Breaks continuous movement due to OS repeat delay +5. **❌ Low Update Rates (<100Hz)**: Visible stuttering during continuous movement +6. **❌ Exposing stepSize to CLI**: Users can't meaningfully configure motor position units -1. **❌ Display Updates in Main Loop**: Causes 90ms+ loop times -2. **❌ Continuous/Velocity Control**: Adds complexity without benefit for keyboard input -3. **❌ All-Motor Updates**: Sends unnecessary commands, overwhelms serial bus -4. **❌ Long Communication Delays**: 5ms+ delays cause stuttering -5. **❌ Complex Interpolation**: Adds latency for simple step-based control -6. **❌ No Change Detection**: Spams motors with identical positions +##### 📊 Debugging Keyboard Issues -##### 📊 Performance Metrics (When It's Working Right) +**Symptoms and Solutions:** -- **Keypress Response**: Immediate (< 10ms) -- **Motor Update**: Single command per changed motor -- **Loop Time**: < 5ms (when not reading positions) -- **User Experience**: "Buttery smooth", "fucking working and super perfect" +- **"Initial delay when holding key"** → OS keyboard repeat delay, increase keyTimeout +- **"Single taps move too far"** → keyTimeout too long, reduce to 150ms or less +- **"Stuttering during continuous movement"** → updateRate too low, increase to 120Hz +- **"No continuous movement"** → keyTimeout too short, increase above OS repeat delay +- **"Immediate movement missing"** → Must call `moveMotorForKey()` on first keypress -**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. +**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. ## Clean Library Architecture (Critical Lessons) diff --git a/docs/getting_started_with_so100.md b/docs/getting_started_with_so100.md index f0a61354150bf9ef6662bc2d83418a0b530bbef1..72b0b74bbd494637d5d7f2d07031f5635e84255f 100644 --- a/docs/getting_started_with_so100.md +++ b/docs/getting_started_with_so100.md @@ -34,13 +34,13 @@ Reconnect the USB cable. ### check follower arm ``` -python -m lerobot.check_motors --robot.port=COM4 +python -m lerobot.setup_motors --robot.port=COM4 --robot.type=so100_follower ``` ### check leader arm ``` -python -m lerobot.check_motors --teleop.port=COM3 +python -m lerobot.setup_motors --teleop.port=COM3 --robot.type=so100_leader ``` **If you see this - you're lucky! Skip to calibration:** diff --git a/docs/planning/006_packages_node.md b/docs/planning/006_packages_node.md new file mode 100644 index 0000000000000000000000000000000000000000..2200d238750a5bb4b337c25ff95e2ea9c821eb1c --- /dev/null +++ b/docs/planning/006_packages_node.md @@ -0,0 +1,541 @@ +# User Story 006: Node.js Package Architecture + +## Story + +**As a** robotics developer building server-side applications, CLI tools, and desktop robotics software +**I want** to use lerobot.js functionality directly from Node.js with the same API as the web version +**So that** I can build Node.js applications, command-line tools, and desktop software without browser constraints while maintaining familiar APIs + +## Background + +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. + +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: + +1. **Mirrors the Web API**: Provides the same function signatures and behavior as `@lerobot/web` +2. **Uses Node.js APIs**: Leverages `serialport` instead of Web Serial for hardware communication +3. **Python lerobot Faithfulness**: Maintains exact compatibility with Python lerobot CLI commands and behavior +4. **Server-Side Ready**: Enables robotics applications in Node.js servers, CLI tools, and desktop applications +5. **Reuses Proven Logic**: Builds on existing `src/lerobot/node` code that was already working + +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. + +## Acceptance Criteria + +### Core Functionality + +- [ ] **Same API Surface**: Mirror `@lerobot/web` API with identical function signatures where possible +- [ ] **Four Core Functions**: Implement `findPort`, `calibrate`, `releaseMotors`, and `teleoperate` +- [ ] **SerialPort Integration**: Use `serialport` package instead of Web Serial API +- [ ] **TypeScript Support**: Full TypeScript coverage with strict type checking +- [ ] **NPM Package**: Published as `@lerobot/node` with proper package.json + +### Platform Requirements + +- [ ] **Node.js 18+**: Support current LTS and newer versions +- [ ] **Cross-Platform**: Work on Windows, macOS, and Linux +- [ ] **ES Modules**: Use ES module format for consistency with web package +- [ ] **CLI Integration**: Enable `npx lerobot` commands using this package +- [ ] **No Browser Dependencies**: No Web API dependencies or browser-specific code + +### API Alignment + +- [ ] **Same Types**: Reuse or mirror types from `@lerobot/web` where appropriate +- [ ] **Same Exports**: Mirror the export structure of `@lerobot/web/index.ts` +- [ ] **Same Behavior**: Identical behavior for shared functionality (calibration algorithms, motor control) +- [ ] **Platform-Specific Adaptations**: Handle Node.js-specific differences (file system, process management) + +### Code Quality + +- [ ] **Reuse Existing Code**: Build on proven `src/lerobot/node` implementations +- [ ] **No Code Duplication**: Share logic with web package where possible (copy for now, per requirements) +- [ ] **Clean Architecture**: Follow the same patterns as `packages/web` +- [ ] **Comprehensive Testing**: Unit tests for all core functionality + +## Expected User Flow + +### Installation and Usage + +```bash +# Install the Node.js package +npm install @lerobot/node + +# Use in Node.js applications +import { findPort, calibrate, teleoperate } from "@lerobot/node"; +``` + +### Find Port (Node.js) + +```typescript +// Node.js - programmatic usage +import { findPort } from "@lerobot/node"; + +const portProcess = await findPort(); +const availablePorts = await portProcess.getAvailablePorts(); +console.log("Available ports:", availablePorts); + +// Interactive mode (CLI-like) - matches Python lerobot exactly +const portProcess = await findPort({ + interactive: true, // shows "disconnect cable" prompts like Python +}); +const detectedPort = await portProcess.detectPort(); +``` + +### Calibration (Node.js) + +```typescript +// Node.js - same API as web +import { calibrate } from "@lerobot/node"; + +const calibrationProcess = await calibrate({ + robot: { + type: "so100_follower", + port: "/dev/ttyUSB0", // or "COM4" on Windows + robotId: "my_follower_arm", + }, + onLiveUpdate: (data) => { + console.log("Live calibration data:", data); + }, +}); + +const results = await calibrationProcess.result; +console.log("Calibration completed:", results); +``` + +### Teleoperation (Node.js) + +```typescript +// Node.js - same API as web +import { teleoperate } from "@lerobot/node"; + +const teleoperationProcess = await teleoperate({ + robot: { + type: "so100_follower", + port: "/dev/ttyUSB0", + robotId: "my_follower_arm", + }, + teleop: { + type: "keyboard", + stepSize: 25, + }, + calibrationData: loadedCalibrationData, + onStateUpdate: (state) => { + console.log("Robot state:", state); + }, +}); + +teleoperationProcess.start(); +``` + +### Release Motors (Node.js) + +```typescript +// Node.js - same API as web +import { releaseMotors } from "@lerobot/node"; + +await releaseMotors({ + robot: { + type: "so100_follower", + port: "/dev/ttyUSB0", + robotId: "my_follower_arm", + }, +}); +``` + +### CLI Integration + +```bash +# Python lerobot compatibility - same commands work +npx lerobot find-port +npx lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 +npx lerobot teleoperate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 + +# Global installation also works +npm install -g @lerobot/node +lerobot find-port +lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 +``` + +## Implementation Details + +### File Structure + +``` +packages/node/ +├── package.json # NPM package configuration +├── tsconfig.build.json # TypeScript build configuration +├── README.md # Package documentation +├── CHANGELOG.md # Version history +└── src/ + ├── index.ts # Main exports (mirror web package) + ├── find_port.ts # Port discovery using serialport + ├── calibrate.ts # Calibration using Node.js APIs + ├── teleoperate.ts # Teleoperation using Node.js APIs + ├── release_motors.ts # Motor release using Node.js APIs + ├── types/ + │ ├── robot-connection.ts # Robot connection types + │ ├── port-discovery.ts # Port discovery types + │ ├── calibration.ts # Calibration types + │ ├── teleoperation.ts # Teleoperation types + │ └── robot-config.ts # Robot configuration types + ├── utils/ + │ ├── serial-port-wrapper.ts # SerialPort wrapper + │ ├── motor-communication.ts # Motor communication utilities + │ ├── motor-calibration.ts # Calibration utilities + │ └── sts3215-protocol.ts # Protocol constants + ├── robots/ + │ └── so100_config.ts # SO-100 configuration + └── teleoperators/ + ├── index.ts # Teleoperator exports + ├── base-teleoperator.ts # Base teleoperator class + └── keyboard-teleoperator.ts # Keyboard teleoperator +``` + +### Key Dependencies + +#### Core Dependencies + +- **serialport**: Node.js serial communication (replaces Web Serial API) +- **chalk**: Terminal colors and formatting +- **commander**: CLI argument parsing + +#### Development Dependencies + +- **typescript**: TypeScript compiler +- **@types/node**: Node.js type definitions +- **vitest**: Testing framework + +### Migration Strategy + +#### Phase 1: Package Setup + +- [ ] Create `packages/node` directory structure +- [ ] Set up package.json with proper exports +- [ ] Configure TypeScript build process +- [ ] Set up testing infrastructure + +#### Phase 2: Core Function Migration + +- [ ] Migrate `src/lerobot/node/find_port.ts` to `packages/node/src/find_port.ts` +- [ ] Migrate `src/lerobot/node/calibrate.ts` to `packages/node/src/calibrate.ts` +- [ ] Migrate `src/lerobot/node/teleoperate.ts` to `packages/node/src/teleoperate.ts` +- [ ] Create `release_motors.ts` using existing motor communication code + +#### Phase 3: API Alignment + +- [ ] Ensure all functions match `@lerobot/web` signatures +- [ ] Copy and adapt types from `packages/web/src/types/` +- [ ] Update utilities to use serialport instead of Web Serial +- [ ] Test API compatibility with existing web examples + +#### Phase 4: Testing and Documentation + +- [ ] Create comprehensive tests for all functions +- [ ] Update documentation and examples +- [ ] Validate Python lerobot CLI compatibility +- [ ] Test cross-platform compatibility + +### Core Functions to Implement + +#### Package Exports (Mirror Web Package) + +```typescript +// packages/node/src/index.ts +export { calibrate } from "./calibrate.js"; +export { teleoperate } from "./teleoperate.js"; +export { findPort } from "./find_port.js"; +export { releaseMotors } from "./release_motors.js"; + +// Types (mirror web package) +export type { + RobotConnection, + RobotConfig, + SerialPort, + SerialPortInfo, + SerialOptions, +} from "./types/robot-connection.js"; + +export type { + FindPortConfig, + FindPortProcess, +} from "./types/port-discovery.js"; + +export type { + CalibrateConfig, + CalibrationResults, + LiveCalibrationData, + CalibrationProcess, +} from "./types/calibration.js"; + +export type { + MotorConfig, + TeleoperationState, + TeleoperationProcess, + TeleoperateConfig, + TeleoperatorConfig, +} from "./types/teleoperation.js"; + +// Node.js utilities +export { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; +export { createSO100Config } from "./robots/so100_config.js"; +``` + +#### SerialPort Wrapper (Node.js) + +```typescript +// packages/node/src/utils/serial-port-wrapper.ts +import { SerialPort } from "serialport"; + +export class NodeSerialPortWrapper { + private port: SerialPort; + private isConnected: boolean = false; + + constructor(path: string, options: any = {}) { + this.port = new SerialPort({ + path, + baudRate: options.baudRate || 1000000, + dataBits: options.dataBits || 8, + parity: options.parity || "none", + stopBits: options.stopBits || 1, + autoOpen: false, + }); + } + + async initialize(): Promise { + return new Promise((resolve, reject) => { + this.port.open((err) => { + if (err) { + reject(err); + } else { + this.isConnected = true; + resolve(); + } + }); + }); + } + + async writeAndRead(data: Uint8Array): Promise { + return new Promise((resolve, reject) => { + this.port.write(Buffer.from(data), (err) => { + if (err) { + reject(err); + return; + } + + // Wait for response + setTimeout(() => { + this.port.read((readErr, readData) => { + if (readErr) { + reject(readErr); + } else { + resolve(new Uint8Array(readData || [])); + } + }); + }, 10); // 10ms delay for response + }); + }); + } + + async close(): Promise { + return new Promise((resolve) => { + this.port.close(() => { + this.isConnected = false; + resolve(); + }); + }); + } +} +``` + +#### Find Port Implementation + +```typescript +// packages/node/src/find_port.ts - Build on existing code +import { SerialPort } from "serialport"; + +export interface FindPortConfig { + interactive?: boolean; +} + +export interface FindPortProcess { + getAvailablePorts(): Promise; + detectPort(): Promise; // Interactive cable detection like Python +} + +export async function findPort( + config: FindPortConfig = {} +): Promise { + const { interactive = false } = config; + + return { + async getAvailablePorts(): Promise { + // Use existing implementation from src/lerobot/node/find_port.ts + const ports = await SerialPort.list(); + return ports.map((port) => port.path); + }, + + async detectPort(): Promise { + if (interactive) { + // Existing Python-compatible implementation from src/lerobot/node/find_port.ts + // Shows "disconnect cable" prompts and detects port automatically + console.log("Finding all available ports for the MotorsBus."); + + const portsBefore = await this.getAvailablePorts(); + console.log( + "Remove the USB cable from your MotorsBus and press Enter when done." + ); + // ... wait for user input ... + + const portsAfter = await this.getAvailablePorts(); + const portsDiff = portsBefore.filter( + (port) => !portsAfter.includes(port) + ); + + if (portsDiff.length === 1) { + return portsDiff[0]; + } else { + throw new Error("Could not detect port"); + } + } else { + // Programmatic mode - return first available port + const ports = await this.getAvailablePorts(); + return ports[0]; + } + }, + }; +} +``` + +### Technical Considerations + +#### API Compatibility with Web Package + +The Node.js package should maintain the same API surface as the web package where possible: + +```typescript +// Same function signatures +await calibrate(config); // Both packages +await teleoperate(config); // Both packages +await findPort(config); // Both packages +await releaseMotors(config); // Both packages +``` + +#### Platform-Specific Adaptations + +**File System Access:** + +- Node.js: Direct file system access for calibration data +- Web: localStorage/IndexedDB for calibration data + +**Process Management:** + +- Node.js: Process signals, stdin/stdout handling +- Web: Browser events, DOM keyboard handling + +**Error Handling:** + +- Node.js: Process exit codes, console.error +- Web: User-friendly error dialogs + +#### Python lerobot CLI Compatibility + +The Node.js package must maintain exact Python lerobot CLI compatibility: + +```bash +# These commands must work identically +npx lerobot find-port +npx lerobot calibrate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 +npx lerobot teleoperate --robot.type=so100_follower --robot.port=/dev/ttyUSB0 +``` + +#### Calibration Data Storage Location + +The CLI should store calibration data in the same location as Python lerobot: + +```bash +# Default location (matches Python lerobot) +~/.cache/huggingface/lerobot/calibration/robots/ +``` + +This ensures calibration files are compatible between Python lerobot and Node.js lerobot: + +```typescript +// Use HF_HOME environment variable like Python lerobot +const HF_HOME = + process.env.HF_HOME || path.join(os.homedir(), ".cache", "huggingface"); +const CALIBRATION_DIR = path.join(HF_HOME, "lerobot", "calibration", "robots"); +``` + +### Package Configuration + +#### package.json + +```json +{ + "name": "@lerobot/node", + "version": "0.1.0", + "description": "Node.js-based robotics control using SerialPort", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "lerobot": "./dist/cli.js" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./calibrate": { + "import": "./dist/calibrate.js", + "types": "./dist/calibrate.d.ts" + }, + "./teleoperate": { + "import": "./dist/teleoperate.js", + "types": "./dist/teleoperate.d.ts" + }, + "./find-port": { + "import": "./dist/find_port.js", + "types": "./dist/find_port.d.ts" + } + }, + "files": ["dist/**/*", "README.md"], + "keywords": [ + "robotics", + "serialport", + "hardware-control", + "nodejs", + "typescript" + ], + "scripts": { + "build": "tsc --project tsconfig.build.json", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "serialport": "^12.0.0", + "chalk": "^5.3.0", + "commander": "^11.0.0" + }, + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "engines": { + "node": ">=18.0.0" + } +} +``` + +## Definition of Done + +- [ ] **Package Structure**: Complete `packages/node` directory with proper NPM package setup +- [ ] **API Mirror**: All four core functions (`findPort`, `calibrate`, `releaseMotors`, `teleoperate`) implemented with same API as web package +- [ ] **SerialPort Integration**: All hardware communication uses `serialport` package instead of Web Serial +- [ ] **Type Safety**: Full TypeScript coverage with strict type checking +- [ ] **Code Migration**: Existing `src/lerobot/node` code successfully migrated and enhanced +- [ ] **Cross-Platform**: Works on Windows, macOS, and Linux with Node.js 18+ +- [ ] **CLI Integration**: `npx lerobot` commands work using the Node.js package +- [ ] **Python Compatibility**: CLI commands match Python lerobot behavior exactly +- [ ] **NPM Ready**: Package published as `@lerobot/node` with proper versioning +- [ ] **Documentation**: Complete README with usage examples and API documentation +- [ ] **Testing**: Comprehensive test suite covering all core functionality +- [ ] **No Regressions**: All existing Node.js functionality preserved and enhanced diff --git a/examples/cyberpunk-standalone/src/components/calibration-view.tsx b/examples/cyberpunk-standalone/src/components/calibration-view.tsx index 0e8145878ec85c12f53e4426e750741debf82459..8ff46e38141ffb2efee05cffdf8a1c10f0a5cf9c 100644 --- a/examples/cyberpunk-standalone/src/components/calibration-view.tsx +++ b/examples/cyberpunk-standalone/src/components/calibration-view.tsx @@ -19,7 +19,7 @@ import { releaseMotors, type CalibrationProcess, type LiveCalibrationData, - type WebCalibrationResults, + type CalibrationResults, type RobotConnection, } from "@lerobot/web"; import { @@ -42,7 +42,7 @@ export function CalibrationView({ robot }: CalibrationViewProps) { const [calibrationProcess, setCalibrationProcess] = useState(null); const [calibrationResults, setCalibrationResults] = - useState(null); + useState(null); const { toast } = useToast(); // Load existing calibration data from unified storage @@ -165,7 +165,7 @@ export function CalibrationView({ robot }: CalibrationViewProps) { readCount: Object.keys(liveData || {}).length > 0 ? 100 : 0, }; - // Use the result directly as WebCalibrationResults + // Use the result directly as CalibrationResults saveCalibrationData(robot.serialNumber, result, metadata); } diff --git a/examples/cyberpunk-standalone/src/components/docs-section.tsx b/examples/cyberpunk-standalone/src/components/docs-section.tsx index 0152755d3af6492a714f4b7efa76c145aa6b5650..2866687ead50f96f46631ab7590958a8566d357b 100644 --- a/examples/cyberpunk-standalone/src/components/docs-section.tsx +++ b/examples/cyberpunk-standalone/src/components/docs-section.tsx @@ -259,8 +259,8 @@ const calibrationData = await calibrationProcess.result;`}
  • - • result: Promise<WebCalibrationResults>{" "} - - Python-compatible format + • result: Promise<CalibrationResults> - + Python-compatible format
  • stop(): void - Stop calibration process diff --git a/examples/cyberpunk-standalone/src/lib/unified-storage.ts b/examples/cyberpunk-standalone/src/lib/unified-storage.ts index 28c145a6cba60def20073297a1aa08eca8aed66c..399ca64a9c6b5b468a424f3634a08547266985c1 100644 --- a/examples/cyberpunk-standalone/src/lib/unified-storage.ts +++ b/examples/cyberpunk-standalone/src/lib/unified-storage.ts @@ -3,7 +3,7 @@ * Manages device persistence using localStorage with serial numbers as keys */ -import type { WebCalibrationResults } from "@lerobot/web"; +import type { CalibrationResults } from "@lerobot/web"; export interface DeviceInfo { serialNumber: string; @@ -25,7 +25,7 @@ export interface CalibrationMetadata { export interface UnifiedRobotData { device_info: DeviceInfo; - calibration?: WebCalibrationResults & { + calibration?: CalibrationResults & { device_type?: string; device_id?: string; calibrated_at?: string; @@ -65,7 +65,7 @@ export function saveUnifiedRobotData( export function saveCalibrationData( serialNumber: string, - calibrationData: WebCalibrationResults, + calibrationData: CalibrationResults, metadata: CalibrationMetadata ): void { try { diff --git a/examples/cyberpunk-standalone/src/types/robot.ts b/examples/cyberpunk-standalone/src/types/robot.ts index d29e3cabcaa436005c2cf75296f683f0a7f6a3bf..ff747562ee79a3d39c412f8065e2929d600fdd6e 100644 --- a/examples/cyberpunk-standalone/src/types/robot.ts +++ b/examples/cyberpunk-standalone/src/types/robot.ts @@ -1,6 +1,6 @@ export type { RobotConnection, LiveCalibrationData, - WebCalibrationResults, + CalibrationResults, TeleoperationState, } from "@lerobot/web"; diff --git a/examples/node-quick-start/.gitignore b/examples/node-quick-start/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..8b65a3a3967f0ab5325120b57c2e9073e9b38991 --- /dev/null +++ b/examples/node-quick-start/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Environment files +.env +.env.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock \ No newline at end of file diff --git a/examples/node-quick-start/README.md b/examples/node-quick-start/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c60e8b9868f79f9ac1e8b118a4c4eaa8320ab805 --- /dev/null +++ b/examples/node-quick-start/README.md @@ -0,0 +1,130 @@ +# Node.js Quick Start Example (Vite) + +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. + +## What This Example Does + +1. **Find Port**: Discovers and connects to your robot hardware +2. **Release Motors**: Puts motors in free-movement mode for setup +3. **Calibrate**: Records motor ranges and sets homing positions +4. **Teleoperate**: Enables keyboard control of the robot + +## Hardware Requirements + +- SO-100 robotic arm with STS3215 servos +- USB connection to your computer +- Compatible with Windows (COM ports), macOS, and Linux (/dev/tty\* ports) + +## Installation + +```bash +# Install dependencies (from project root) +pnpm install +``` + +## Running the Examples + +### Full Workflow Demo + +```bash +# Complete robot setup workflow (interactive) +pnpm demo:full-workflow +``` + +### Individual Component Demos + +```bash +# Test port discovery +pnpm demo:find-port + +# Test calibration (requires connected robot) +pnpm demo:calibrate + +# Test keyboard control (requires calibrated robot) +pnpm demo:teleoperate +``` + +## Example Usage + +The main demo (`src/main.ts`) shows the complete workflow: + +```typescript +import { findPort, connectPort, releaseMotors, calibrate, teleoperate } from "@lerobot/node"; + +// 1. Find available robots +const findProcess = await findPort(); +const robots = await findProcess.result; + +// 2. Connect to first robot found +const robot = await connectPort(robots[0].path, "so100_follower", "my_robot_arm"); + +// 3. Release motors for manual positioning +await releaseMotors(robot); + +// 4. Calibrate motor ranges +const calibrationProcess = await calibrate({ + robot, + onProgress: (message) => console.log(message), +}); + +// 5. Control robot with keyboard +const teleop = await teleoperate({ + robot, + teleop: { type: "keyboard" }, +}); +``` + +## CLI Commands + +You can also use the CLI directly: + +```bash +# Find available ports +npx lerobot find-port + +# Calibrate robot +npx lerobot calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot + +# Control robot +npx lerobot teleoperate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot +``` + +## Development + +```bash +# Run with Vite Node (faster development with hot reload) +pnpm dev + +# Build with Vite and run compiled version +pnpm build && pnpm start +``` + +## Safety Notes + +⚠️ **Important Safety Guidelines:** + +- Always ensure robot is in a safe position before running examples +- Keep emergency stop accessible (ESC key during teleoperation) +- Start with small movements to test calibration +- Ensure robot has adequate workspace clearance + +## Troubleshooting + +**Port Not Found:** + +- Check USB connection +- Verify robot is powered on +- Try different USB ports/cables +- On Linux: Check user permissions for serial ports + +**Calibration Issues:** + +- Ensure motors are released and can move freely +- Move each joint through its full range slowly +- Avoid forcing motors past mechanical limits + +**Control Problems:** + +- Verify calibration completed successfully +- Check that calibration file was saved +- Restart the teleoperation if motors don't respond diff --git a/examples/node-quick-start/package.json b/examples/node-quick-start/package.json new file mode 100644 index 0000000000000000000000000000000000000000..1224ed0f0436eedd11497982e2dafdf8b8e0a7f8 --- /dev/null +++ b/examples/node-quick-start/package.json @@ -0,0 +1,36 @@ +{ + "name": "node-quick-start", + "version": "0.1.0", + "description": "Node.js Quick Start example for @lerobot/node package", + "type": "module", + "scripts": { + "dev": "vite-node src/main.ts", + "build": "vite build", + "start": "node dist/main.js", + "demo:find-port": "vite-node src/demo-find-port.ts", + "demo:calibrate": "vite-node src/demo-calibrate.ts", + "demo:teleoperate": "vite-node src/demo-teleoperate.ts", + "demo:full-workflow": "vite-node src/main.ts" + }, + "dependencies": { + "@lerobot/node": "file:../../packages/node" + }, + "devDependencies": { + "vite": "^6.3.5", + "vite-node": "^2.0.0", + "typescript": "^5.3.0", + "@types/node": "^18.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "robotics", + "lerobot", + "nodejs", + "example", + "demo" + ], + "author": "LeRobot.js Team", + "license": "Apache-2.0" +} diff --git a/examples/node-quick-start/pnpm-lock.yaml b/examples/node-quick-start/pnpm-lock.yaml new file mode 100644 index 0000000000000000000000000000000000000000..48ff1dc1cb00f53bbb348d5624e82f0773fb9eed --- /dev/null +++ b/examples/node-quick-start/pnpm-lock.yaml @@ -0,0 +1,1174 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@lerobot/node': + specifier: file:../../packages/node + version: file:../../packages/node(typescript@5.8.3) + devDependencies: + '@types/node': + specifier: ^18.0.0 + version: 18.19.120 + typescript: + specifier: ^5.3.0 + version: 5.8.3 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@18.19.120)(tsx@4.20.3) + vite-node: + specifier: ^2.0.0 + version: 2.1.9(@types/node@18.19.120) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@lerobot/node@file:../../packages/node': + resolution: {directory: ../../packages/node, type: directory} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=4.5.0' + + '@rollup/rollup-android-arm-eabi@4.46.0': + resolution: {integrity: sha512-9f3nSTFI2ivfxc7/tHBHcJ8pRnp8ROrELvsVprlQPVvcZ+j5zztYd+PTJGpyIOAdTvNwNrpCXswKSeoQcyGjMQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.46.0': + resolution: {integrity: sha512-tFZSEhqJ8Yrpe50TzOdeoYi72gi/jsnT7y8Qrozf3cNu28WX+s6I3XzEPUAqoaT9SAS8Xz9AzGTFlxxCH/w20w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.46.0': + resolution: {integrity: sha512-+DikIIs+p6yU2hF51UaWG8BnHbq90X0QIOt5zqSKSZxY+G3qqdLih214e9InJal21af2PuuxkDectetGfbVPJw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.46.0': + resolution: {integrity: sha512-5a+NofhdEB/WimSlFMskbFQn1vqz1FWryYpA99trmZGO6qEmiS0IsX6w4B3d91U878Q2ZQdiaFF1gxX4P147og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.46.0': + resolution: {integrity: sha512-igr/RlKPS3OCy4jD3XBmAmo3UAcNZkJSubRsw1JeM8bAbwf15k/3eMZXD91bnjheijJiOJcga3kfCLKjV8IXNg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.46.0': + resolution: {integrity: sha512-MdigWzPSHlQzB1xZ+MdFDWTAH+kcn7UxjEBoOKuaso7z1DRlnAnrknB1mTtNOQ+GdPI8xgExAGwHeqQjntR0Cg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.46.0': + resolution: {integrity: sha512-dmZseE0ZwA/4yy1+BwFrDqFTjjNg24GO9xSrb1weVbt6AFkhp5pz1gVS7IMtfIvoWy8yp6q/zN0bKnefRUImvQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.46.0': + resolution: {integrity: sha512-fzhfn6p9Cfm3W8UrWKIa4l7Wfjs/KGdgaswMBBE3KY3Ta43jg2XsPrAtfezHpsRk0Nx+TFuS3hZk/To2N5kFPQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.46.0': + resolution: {integrity: sha512-vVDD+iPDPmJQ5nAQ5Tifq3ywdv60FartglFI8VOCK+hcU9aoG0qlQTsDJP97O5yiTaTqlneZWoARMcVC5nyUoQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.46.0': + resolution: {integrity: sha512-0d0jx08fzDHCzXqrtCMEEyxKU0SvJrWmUjUDE2/KDQ2UDJql0tfiwYvEx1oHELClKO8CNdE+AGJj+RqXscZpdQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.0': + resolution: {integrity: sha512-XBYu9oW9eKJadWn8M7hkTZsD4yG+RrsTrVEgyKwb4L72cpJjRbRboTG9Lg9fec8MxJp/cfTHAocg4mnismQR8A==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.46.0': + resolution: {integrity: sha512-wJaRvcT17PoOK6Ggcfo3nouFlybHvARBS4jzT0PC/lg17fIJHcDS2fZz3sD+iA4nRlho2zE6OGbU0HvwATdokQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.46.0': + resolution: {integrity: sha512-GZ5bkMFteAGkcmh8x0Ok4LSa+L62Ez0tMsHPX6JtR0wl4Xc3bQcrFHDiR5DGLEDFtGrXih4Nd/UDaFqs968/wA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.0': + resolution: {integrity: sha512-7CjPw6FflFsVOUfWOrVrREiV3IYXG4RzZ1ZQUaT3BtSK8YXN6x286o+sruPZJESIaPebYuFowmg54ZdrkVBYog==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.46.0': + resolution: {integrity: sha512-nmvnl0ZiuysltcB/cKjUh40Rx4FbSyueERDsl2FLvLYr6pCgSsvGr3SocUT84svSpmloS7f1DRWqtRha74Gi1w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.46.0': + resolution: {integrity: sha512-Cv+moII5C8RM6gZbR3cb21o6rquVDZrN2o81maROg1LFzBz2dZUwIQSxFA8GtGZ/F2KtsqQ2z3eFPBb6akvQNg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.46.0': + resolution: {integrity: sha512-PHcMG8DZTM9RCIjp8QIfN0VYtX0TtBPnWOTRurFhoCDoi9zptUZL2k7pCs+5rgut7JAiUsYy+huyhVKPcmxoog==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.46.0': + resolution: {integrity: sha512-1SI/Rd47e8aQJeFWMDg16ET+fjvCcD/CzeaRmIEPmb05hx+3cCcwIF4ebUag4yTt/D1peE+Mgp0+Po3M358cAA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.46.0': + resolution: {integrity: sha512-JwOCYxmumFDfDhx4kNyz6kTVK3gWzBIvVdMNzQMRDubcoGRDniOOmo6DDNP42qwZx3Bp9/6vWJ+kNzNqXoHmeA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.46.0': + resolution: {integrity: sha512-IPMIfrfkG1GaEXi+JSsQEx8x9b4b+hRZXO7KYc2pKio3zO2/VDXDs6B9Ts/nnO+25Fk1tdAVtUn60HKKPPzDig==} + cpu: [x64] + os: [win32] + + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@18.19.120': + resolution: {integrity: sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.46.0: + resolution: {integrity: sha512-ONmkT3Ud3IfW15nl7l4qAZko5/2iZ5ALVBDh02ZSZ5IGVLJSYkRcRa3iB58VyEIyoofs9m2xdVrm+lTi97+3pw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.8': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.8': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.8': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.8': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.8': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.8': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.8': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.8': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.8': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.8': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.8': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.8': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.8': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.8': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.8': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.8': + optional: true + + '@esbuild/netbsd-arm64@0.25.8': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.8': + optional: true + + '@esbuild/openbsd-arm64@0.25.8': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.8': + optional: true + + '@esbuild/openharmony-arm64@0.25.8': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.8': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.8': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.8': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.8': + optional: true + + '@lerobot/node@file:../../packages/node(typescript@5.8.3)': + dependencies: + serialport: 12.0.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@rollup/rollup-android-arm-eabi@4.46.0': + optional: true + + '@rollup/rollup-android-arm64@4.46.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.46.0': + optional: true + + '@rollup/rollup-darwin-x64@4.46.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.46.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.46.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.46.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.46.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.46.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.46.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.46.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.46.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.46.0': + optional: true + + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-byte-length@12.0.0': {} + + '@serialport/parser-cctalk@12.0.0': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-inter-byte-timeout@12.0.0': {} + + '@serialport/parser-packet-length@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + + '@serialport/parser-ready@12.0.0': {} + + '@serialport/parser-regex@12.0.0': {} + + '@serialport/parser-slip-encoder@12.0.0': {} + + '@serialport/parser-spacepacket@12.0.0': {} + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@types/estree@1.0.8': {} + + '@types/node@18.19.120': + dependencies: + undici-types: 5.26.5 + + cac@6.7.14: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.8: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + optional: true + + ms@2.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-addon-api@7.0.0: {} + + node-gyp-build@4.6.0: {} + + pathe@1.1.2: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve-pkg-maps@1.0.0: + optional: true + + rollup@4.46.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.0 + '@rollup/rollup-android-arm64': 4.46.0 + '@rollup/rollup-darwin-arm64': 4.46.0 + '@rollup/rollup-darwin-x64': 4.46.0 + '@rollup/rollup-freebsd-arm64': 4.46.0 + '@rollup/rollup-freebsd-x64': 4.46.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.0 + '@rollup/rollup-linux-arm-musleabihf': 4.46.0 + '@rollup/rollup-linux-arm64-gnu': 4.46.0 + '@rollup/rollup-linux-arm64-musl': 4.46.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.0 + '@rollup/rollup-linux-ppc64-gnu': 4.46.0 + '@rollup/rollup-linux-riscv64-gnu': 4.46.0 + '@rollup/rollup-linux-riscv64-musl': 4.46.0 + '@rollup/rollup-linux-s390x-gnu': 4.46.0 + '@rollup/rollup-linux-x64-gnu': 4.46.0 + '@rollup/rollup-linux-x64-musl': 4.46.0 + '@rollup/rollup-win32-arm64-msvc': 4.46.0 + '@rollup/rollup-win32-ia32-msvc': 4.46.0 + '@rollup/rollup-win32-x64-msvc': 4.46.0 + fsevents: 2.3.3 + + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + tsx@4.20.3: + dependencies: + esbuild: 0.25.8 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + optional: true + + typescript@5.8.3: {} + + undici-types@5.26.5: {} + + vite-node@2.1.9(@types/node@18.19.120): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@18.19.120) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.19(@types/node@18.19.120): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.46.0 + optionalDependencies: + '@types/node': 18.19.120 + fsevents: 2.3.3 + + vite@6.3.5(@types/node@18.19.120)(tsx@4.20.3): + dependencies: + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.0 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 18.19.120 + fsevents: 2.3.3 + tsx: 4.20.3 diff --git a/examples/node-quick-start/src/demo-calibrate.ts b/examples/node-quick-start/src/demo-calibrate.ts new file mode 100644 index 0000000000000000000000000000000000000000..8eb1ae7c19fdc2c2610717fc12668096446b90db --- /dev/null +++ b/examples/node-quick-start/src/demo-calibrate.ts @@ -0,0 +1,125 @@ +/** + * Calibration Demo + * + * Demonstrates robot motor calibration with live feedback + */ + +import { findPort, connectPort, releaseMotors, calibrate } from "@lerobot/node"; +import type { RobotConnection, DiscoveredPort } from "@lerobot/node"; + +async function demoCalibrate() { + console.log("🎯 Calibration Demo"); + console.log("===================\n"); + + try { + // Step 1: Find available robot ports + console.log("📡 Looking for connected robots..."); + const findProcess = await findPort(); + const discoveredPorts = await findProcess.result; + + if (discoveredPorts.length === 0) { + throw new Error("No robots found. Please connect your robot first."); + } + + console.log(`✅ Found robot on ${discoveredPorts[0].path}`); + + // Step 2: Connect to robot + console.log("🔌 Connecting to robot..."); + const robot = await connectPort( + discoveredPorts[0].path, + "so100_follower", + "calibration_demo" + ); + console.log(`✅ Connected: ${robot.robotType} (ID: ${robot.robotId})\n`); + + // Step 3: Release motors + console.log("🔓 Releasing motors for calibration setup..."); + await releaseMotors(robot); + console.log("✅ Motors released - robot can now be moved by hand"); + + console.log("\n📍 Move robot to your preferred starting position..."); + console.log("Press any key to continue..."); + + // Simple key press handler without readline conflicts + process.stdin.setRawMode(true); + process.stdin.resume(); + + await new Promise((resolve) => { + const onData = () => { + process.stdin.setRawMode(false); + process.stdin.pause(); + process.stdin.removeListener("data", onData); + resolve(); + }; + process.stdin.once("data", onData); + }); + + // Step 4: Calibration process + console.log("\n🎯 Starting calibration process..."); + console.log("This will:"); + console.log("1. Set homing offsets (center positions)"); + console.log("2. Record range of motion for each motor"); + console.log("3. Write position limits to robot hardware"); + console.log("4. Save calibration data for future use\n"); + + const calibrationProcess = await calibrate({ + robot, + onProgress: (message) => { + console.log(`📊 ${message}`); + }, + onLiveUpdate: (data) => { + // Display real-time motor positions and ranges + const updates = Object.entries(data).map(([name, info]) => { + const range = info.max - info.min; + return `${name}: ${info.current} [${info.min}→${info.max}] (range: ${range})`; + }); + + console.clear(); + console.log("🔄 Live Calibration Data:"); + console.log("========================"); + updates.forEach((update) => console.log(` ${update}`)); + console.log("\n💡 Move each motor through its full range of motion"); + console.log(" Press Enter to complete calibration..."); + }, + }); + + // Wait for calibration to complete (it handles user input internally) + const calibrationData = await calibrationProcess.result; + + console.log("\n✅ Calibration completed successfully!"); + + // Display detailed results + console.log("\n📋 Detailed Calibration Results:"); + console.log("================================="); + Object.entries(calibrationData).forEach(([motorName, config]) => { + const range = config.range_max - config.range_min; + console.log(`${motorName}:`); + console.log(` Motor ID: ${config.id}`); + console.log(` Drive Mode: ${config.drive_mode}`); + console.log(` Homing Offset: ${config.homing_offset}`); + console.log( + ` Range: ${config.range_min} → ${config.range_max} (${range} steps)` + ); + console.log(` Degrees: ~${((range / 4096) * 360).toFixed(1)}°\n`); + }); + + console.log("💾 Calibration saved to HuggingFace cache directory"); + console.log("🔄 This file is compatible with Python lerobot"); + + console.log("\n🎉 Calibration demo completed!"); + console.log("💡 You can now use this calibration data for teleoperation"); + + // Ensure process can exit cleanly + process.exit(0); + } catch (error) { + console.error("\n❌ Calibration failed:", error.message); + console.log("\n🔧 Troubleshooting:"); + console.log("- Ensure robot is connected and responsive"); + console.log("- Check that motors can move freely during calibration"); + console.log("- Avoid forcing motors past their mechanical limits"); + console.log("- Try restarting the robot if motors become unresponsive"); + process.exit(1); + } +} + +demoCalibrate(); diff --git a/examples/node-quick-start/src/demo-find-port.ts b/examples/node-quick-start/src/demo-find-port.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d54e00db8303e94c222c90d31508d1f69392ce0 --- /dev/null +++ b/examples/node-quick-start/src/demo-find-port.ts @@ -0,0 +1,90 @@ +/** + * Port Discovery Demo + * + * Demonstrates how to find and connect to robot hardware programmatically + */ + +import { findPort, connectPort } from "@lerobot/node"; +import type { RobotConnection, DiscoveredPort } from "@lerobot/node"; + +async function demoFindPort() { + console.log("🔍 Port Discovery Demo"); + console.log("======================\n"); + + try { + // Demo 1: Basic port discovery + console.log("📋 Demo 1: Basic robot discovery"); + console.log("Looking for connected robots...\n"); + + const findProcess = await findPort({ + onMessage: (message) => console.log(` 📡 ${message}`), + }); + const discoveredPorts = await findProcess.result; + + if (discoveredPorts.length === 0) { + console.log("❌ No robots found."); + console.log("\n🔧 Make sure your robot is:"); + console.log(" - Connected via USB"); + console.log(" - Powered on"); + console.log(" - Using a working USB cable"); + return; + } + + console.log(`\n✅ Found ${discoveredPorts.length} robot port(s):`); + discoveredPorts.forEach((port, index) => { + console.log(` ${index + 1}. ${port.robotType} on ${port.path}`); + console.log(` Port: ${port.path}`); + console.log(` Type: ${port.robotType}`); + }); + + // Demo 2: Connect to discovered robot + console.log("\n🔌 Demo 2: Connecting to discovered robot"); + + const robot = await connectPort( + discoveredPorts[0].path, + "so100_follower", + "demo_robot" + ); + + console.log( + `✅ Connected to robot: ${robot.robotType} (ID: ${robot.robotId})` + ); + console.log(` Port: ${robot.port.path}`); + console.log(` Connected: ${robot.isConnected ? "✅" : "❌"}`); + console.log(` Baudrate: ${robot.port.baudRate}`); + + // Demo 3: Connection details + console.log("\n🔌 Demo 3: Connection details"); + console.log("Robot connection properties:"); + console.log(` Name: ${robot.name}`); + console.log(` Type: ${robot.robotType}`); + console.log(` ID: ${robot.robotId}`); + console.log(` Port: ${robot.port.path}`); + console.log(` Serial: ${robot.serialNumber}`); + console.log(` Connected: ${robot.isConnected ? "✅" : "❌"}`); + + // Demo 4: Silent discovery (no progress messages) + console.log("\n🤫 Demo 4: Silent discovery"); + console.log("Finding robots without progress messages..."); + + const silentProcess = await findPort(); // No onMessage callback + const silentRobots = await silentProcess.result; + + console.log(`Found ${silentRobots.length} robot(s) silently`); + + console.log("\n🎉 Port discovery demo completed!"); + console.log("\nℹ️ Note: For interactive port discovery, use the CLI:"); + console.log(" npx lerobot find-port"); + } catch (error) { + console.error("\n❌ Port discovery failed:", error.message); + console.log("\n🔧 Troubleshooting:"); + console.log("- Check USB connections"); + console.log("- Verify robot is powered on"); + console.log("- Try different USB ports/cables"); + console.log("- On Linux: Check serial port permissions"); + console.log("- For interactive port discovery, use: npx lerobot find-port"); + process.exit(1); + } +} + +demoFindPort(); diff --git a/examples/node-quick-start/src/demo-teleoperate.ts b/examples/node-quick-start/src/demo-teleoperate.ts new file mode 100644 index 0000000000000000000000000000000000000000..56a0fd115962a4cec249cd78959f9ec303092918 --- /dev/null +++ b/examples/node-quick-start/src/demo-teleoperate.ts @@ -0,0 +1,207 @@ +/** + * Teleoperation Demo + * + * Demonstrates different ways to control robot motors + */ + +import { findPort, connectPort, teleoperate } from "@lerobot/node"; +import type { RobotConnection, DiscoveredPort } from "@lerobot/node"; +import { createInterface } from "readline"; + +function askUser(question: string): Promise { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +async function demoTeleoperate() { + console.log("🎮 Teleoperation Demo"); + console.log("=====================\n"); + + try { + // Step 1: Find available robot ports + console.log("📡 Looking for connected robots..."); + const findProcess = await findPort(); + const discoveredPorts = await findProcess.result; + + if (discoveredPorts.length === 0) { + throw new Error("No robots found. Please connect your robot first."); + } + + console.log(`✅ Found robot on ${discoveredPorts[0].path}`); + + // Step 2: Connect to robot + console.log("🔌 Connecting to robot..."); + const robot = await connectPort( + discoveredPorts[0].path, + "so100_follower", + "teleop_demo" + ); + console.log(`✅ Connected: ${robot.robotType} (ID: ${robot.robotId})\n`); + + // Step 3: Choose teleoperation mode + console.log("🎯 Choose teleoperation mode:"); + console.log("1. Keyboard Control (interactive)"); + console.log("2. Direct Control (programmatic)"); + + const mode = await askUser("Enter choice (1 or 2): "); + + if (mode === "1") { + // Keyboard teleoperation demo + await demoKeyboardControl(robot); + } else if (mode === "2") { + // Direct control demo + await demoDirectControl(robot); + } else { + console.log("Invalid choice. Defaulting to keyboard control..."); + await demoKeyboardControl(robot); + } + } catch (error) { + console.error("\n❌ Teleoperation failed:", error.message); + console.log("\n🔧 Troubleshooting:"); + console.log("- Ensure robot is calibrated first"); + console.log("- Check that robot is connected and responsive"); + console.log("- Verify calibration data exists"); + console.log("- Try smaller step sizes if movements are too large"); + process.exit(1); + } +} + +async function demoKeyboardControl(robot: RobotConnection) { + console.log("\n⌨️ Keyboard Control Demo"); + console.log("========================="); + console.log("\n🎮 Robot Controls:"); + console.log(" Arrow Keys: Shoulder pan/lift"); + console.log(" W/S: Elbow flex/extend"); + console.log(" A/D: Wrist down/up"); + console.log(" Q/E: Wrist roll left/right"); + console.log(" O/C: Gripper open/close"); + console.log(" ESC: Emergency stop"); + console.log(" Ctrl+C: Exit demo\n"); + + const teleop = await teleoperate({ + robot, + teleop: { + type: "keyboard", + // Using optimized defaults for smooth control + }, + onStateUpdate: (state) => { + if (state.isActive) { + // Show live motor positions + const motorInfo = state.motorConfigs + .map((motor) => { + const pos = Math.round(motor.currentPosition); + const percent = ( + ((pos - motor.minPosition) / + (motor.maxPosition - motor.minPosition)) * + 100 + ).toFixed(0); + return `${motor.name}:${pos}(${percent}%)`; + }) + .join(" "); + process.stdout.write(`\r🤖 ${motorInfo}`); + } + }, + }); + + // Start keyboard control + teleop.start(); + console.log("✅ Keyboard control active!"); + console.log("💡 Move robot with keyboard, press Ctrl+C to exit"); + + // Handle graceful shutdown + process.on("SIGINT", async () => { + console.log("\n🛑 Stopping keyboard control..."); + teleop.stop(); + await teleop.disconnect(); + console.log("✅ Keyboard control demo completed!"); + process.exit(0); + }); + + // Keep demo running + await new Promise(() => {}); // Keep alive +} + +async function demoDirectControl(robot: RobotConnection) { + console.log("\n🎯 Direct Control Demo"); + console.log("======================"); + console.log("This demonstrates programmatic robot control\n"); + + const teleop = await teleoperate({ + robot, + teleop: { type: "direct" }, + onStateUpdate: (state) => { + const motorInfo = state.motorConfigs + .map((motor) => `${motor.name}:${Math.round(motor.currentPosition)}`) + .join(" "); + console.log(`🤖 ${motorInfo}`); + }, + }); + + teleop.start(); + + // Get direct control interface + const directController = teleop.teleoperator as any; + + console.log("🎬 Running automated movement sequence...\n"); + + try { + // Demo sequence: move different motors + const movements = [ + { + motor: "shoulder_pan", + position: 2048, + description: "Center shoulder pan", + }, + { motor: "shoulder_lift", position: 1500, description: "Lift shoulder" }, + { motor: "elbow_flex", position: 2500, description: "Flex elbow" }, + { motor: "wrist_flex", position: 2000, description: "Adjust wrist" }, + { motor: "wrist_roll", position: 2048, description: "Center wrist roll" }, + { motor: "gripper", position: 1800, description: "Adjust gripper" }, + ]; + + for (const movement of movements) { + console.log(`🎯 ${movement.description}...`); + await directController.moveMotor(movement.motor, movement.position); + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second + } + + console.log("\n🎉 Movement sequence completed!"); + + // Demo multi-motor movement + console.log("\n🎭 Demonstrating simultaneous multi-motor movement..."); + const results = await directController.moveMotors({ + shoulder_pan: 2048, + shoulder_lift: 2048, + elbow_flex: 2048, + wrist_flex: 2048, + }); + + console.log("📊 Movement results:"); + Object.entries(results).forEach(([motor, success]) => { + console.log(` ${motor}: ${success ? "✅" : "❌"}`); + }); + + // Show current positions + const positions = directController.getCurrentPositions(); + console.log("\n📍 Final positions:"); + Object.entries(positions).forEach(([motor, position]) => { + console.log(` ${motor}: ${Math.round(position as number)}`); + }); + } finally { + console.log("\n🛑 Stopping direct control..."); + teleop.stop(); + await teleop.disconnect(); + console.log("✅ Direct control demo completed!"); + } +} + +demoTeleoperate(); diff --git a/examples/node-quick-start/src/main.ts b/examples/node-quick-start/src/main.ts new file mode 100644 index 0000000000000000000000000000000000000000..72cd22ab2bee10f79edde8426479aa94769dd93f --- /dev/null +++ b/examples/node-quick-start/src/main.ts @@ -0,0 +1,183 @@ +/** + * Node.js Quick Start Example - Complete Workflow + * + * This example demonstrates the full robot control workflow: + * 1. Find and connect to robot hardware + * 2. Release motors for manual positioning + * 3. Calibrate motor ranges and homing positions + * 4. Control robot with keyboard teleoperation + */ + +import { + findPort, + connectPort, + releaseMotors, + calibrate, + teleoperate, +} from "@lerobot/node"; +import type { RobotConnection, DiscoveredPort } from "@lerobot/node"; + +// Utility for user confirmation +import { createInterface } from "readline"; + +function askUser(question: string): Promise { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +async function quickStartDemo() { + console.log("🤖 LeRobot.js Node.js Quick Start Demo"); + console.log("=====================================\n"); + + try { + // Step 1: Find available robot ports + console.log("📡 Step 1: Looking for connected robots..."); + const findProcess = await findPort(); + const discoveredPorts = await findProcess.result; + + if (discoveredPorts.length === 0) { + throw new Error("No robots found. Please check your connections."); + } + + console.log(`✅ Found robot on ${discoveredPorts[0].path}`); + + // Step 2: Connect to the first robot found + console.log("🔌 Step 2: Connecting to robot..."); + const robot = await connectPort( + discoveredPorts[0].path, + "so100_follower", + "demo_robot_arm" + ); + console.log(`✅ Connected: ${robot.robotType} (ID: ${robot.robotId})\n`); + + // Step 3: Release motors for calibration setup + const shouldRelease = await askUser( + "🔓 Release motors for manual positioning? (y/n): " + ); + if (shouldRelease.toLowerCase() === "y") { + console.log("🔓 Step 2: Releasing motors..."); + await releaseMotors(robot); + console.log("✅ Motors released - you can now move the robot by hand\n"); + + await askUser( + "Move robot to desired starting position, then press Enter to continue..." + ); + } + + // Step 4: Calibrate the robot + const shouldCalibrate = await askUser("🎯 Run calibration? (y/n): "); + if (shouldCalibrate.toLowerCase() === "y") { + console.log("\n🎯 Step 3: Starting calibration..."); + console.log( + "This will record the motor ranges and set homing positions.\n" + ); + + const calibrationProcess = await calibrate({ + robot: robot as RobotConnection, + onProgress: (message) => { + console.log(` 📊 ${message}`); + }, + onLiveUpdate: (data) => { + // Show live motor positions during range recording + const positions = Object.entries(data) + .map( + ([name, info]) => + `${name}:${info.current}(${info.min}-${info.max})` + ) + .join(" "); + process.stdout.write(`\r 🔄 Live: ${positions}`); + }, + }); + + const calibrationData = await calibrationProcess.result; + console.log("\n✅ Calibration completed!"); + + // Show calibration summary + console.log("\n📋 Calibration Results:"); + Object.entries(calibrationData).forEach(([motorName, config]) => { + console.log( + ` ${motorName}: range ${config.range_min}-${config.range_max}, offset ${config.homing_offset}` + ); + }); + } + + // Step 5: Teleoperation + const shouldTeleoperate = await askUser( + "\n🎮 Start keyboard teleoperation? (y/n): " + ); + if (shouldTeleoperate.toLowerCase() === "y") { + console.log("\n🎮 Step 4: Starting teleoperation..."); + console.log("Use keyboard to control the robot:\n"); + + const teleop = await teleoperate({ + robot: robot as RobotConnection, + teleop: { type: "keyboard" }, + onStateUpdate: (state) => { + if (state.isActive) { + const motorInfo = state.motorConfigs + .map( + (motor) => `${motor.name}:${Math.round(motor.currentPosition)}` + ) + .join(" "); + process.stdout.write(`\r🤖 Motors: ${motorInfo}`); + } + }, + }); + + // Start keyboard control + teleop.start(); + + console.log("✅ Teleoperation active!"); + console.log("🎯 Use arrow keys, WASD, Q/E, O/C to control"); + console.log("⚠️ Press ESC for emergency stop, Ctrl+C to exit\n"); + + // Handle graceful shutdown + process.on("SIGINT", async () => { + console.log("\n🛑 Shutting down teleoperation..."); + teleop.stop(); + await teleop.disconnect(); + console.log("✅ Demo completed successfully!"); + process.exit(0); + }); + + // Keep the demo running + console.log("Demo is running... Press Ctrl+C to stop"); + await new Promise(() => {}); // Keep alive + } + + console.log("\n🎉 Quick Start Demo completed!"); + console.log( + "You can now integrate @lerobot/node into your own applications." + ); + } catch (error) { + console.error("\n❌ Demo failed:", error.message); + console.log("\n🔧 Troubleshooting tips:"); + console.log("- Check robot is connected and powered on"); + console.log("- Verify correct serial port permissions"); + console.log("- Try running 'npx lerobot find-port' to test connection"); + process.exit(1); + } +} + +// Handle uncaught errors gracefully +process.on("uncaughtException", (error) => { + console.error("\n💥 Unexpected error:", error.message); + process.exit(1); +}); + +process.on("unhandledRejection", (error) => { + console.error("\n💥 Unhandled promise rejection:", error); + process.exit(1); +}); + +// Run the demo +quickStartDemo(); diff --git a/examples/node-quick-start/test-homing-offsets.json b/examples/node-quick-start/test-homing-offsets.json new file mode 100644 index 0000000000000000000000000000000000000000..0f2ac546d98c39984728ce4255403a22dbe2e01b --- /dev/null +++ b/examples/node-quick-start/test-homing-offsets.json @@ -0,0 +1,44 @@ +{ + "shoulder_pan": { + "id": 1, + "drive_mode": 0, + "homing_offset": 0, + "range_min": 0, + "range_max": 4095 + }, + "shoulder_lift": { + "id": 2, + "drive_mode": 0, + "homing_offset": 0, + "range_min": 0, + "range_max": 4095 + }, + "elbow_flex": { + "id": 3, + "drive_mode": 0, + "homing_offset": 0, + "range_min": 0, + "range_max": 4095 + }, + "wrist_flex": { + "id": 4, + "drive_mode": 0, + "homing_offset": 0, + "range_min": 0, + "range_max": 4095 + }, + "wrist_roll": { + "id": 5, + "drive_mode": 0, + "homing_offset": 0, + "range_min": 0, + "range_max": 4095 + }, + "gripper": { + "id": 6, + "drive_mode": 0, + "homing_offset": 0, + "range_min": 0, + "range_max": 4095 + } +} \ No newline at end of file diff --git a/examples/node-quick-start/tsconfig.json b/examples/node-quick-start/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..ca519a6fe63c000dbdcb8a50df01f4cd5b76854a --- /dev/null +++ b/examples/node-quick-start/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "rootDir": "./src", + "declaration": false, + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "allowJs": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} diff --git a/examples/node-quick-start/vite.config.ts b/examples/node-quick-start/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..c838cff1a9378bba5b718ff3dabadeaab71ca2c3 --- /dev/null +++ b/examples/node-quick-start/vite.config.ts @@ -0,0 +1,39 @@ +import { defineConfig } from "vite"; +import { resolve } from "path"; + +export default defineConfig({ + build: { + target: "node18", + lib: { + entry: { + main: resolve(__dirname, "src/main.ts"), + "demo-find-port": resolve(__dirname, "src/demo-find-port.ts"), + "demo-calibrate": resolve(__dirname, "src/demo-calibrate.ts"), + "demo-teleoperate": resolve(__dirname, "src/demo-teleoperate.ts"), + }, + formats: ["es"], + fileName: (format, entryName) => `${entryName}.js`, + }, + rollupOptions: { + external: [ + // Node.js built-ins + "fs", + "fs/promises", + "path", + "os", + "readline", + "process", + // Dependencies that should remain external + "serialport", + "@lerobot/node", + ], + }, + outDir: "dist", + emptyOutDir: true, + }, + resolve: { + alias: { + "@": resolve(__dirname, "src"), + }, + }, +}); diff --git a/package.json b/package.json index e23ae90039ad2c771102f790aa6760441563f459..f62e70ac469b693eb60c165bfa1ab0c686c0264c 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,7 @@ "workspaces": [ "packages/*" ], - "files": [ - "dist/**/*", - "README.md" - ], + "private": true, "keywords": [ "robotics", "ai", @@ -18,14 +15,13 @@ "lerobot" ], "scripts": { - "cli:find-port": "tsx src/cli/index.ts find-port", - "cli:calibrate": "tsx src/cli/index.ts calibrate", - "cli:teleoperate": "tsx src/cli/index.ts teleoperate", "example:cyberpunk": "cd examples/cyberpunk-standalone && pnpm dev", "example:iframe-test": "cd examples/iframe-dialog-test && pnpm dev", "example:sequential-test": "cd examples/test-sequential-operations && pnpm dev", - "build": "pnpm run build:cli", - "build:cli": "tsc --project tsconfig.cli.json", + "dev": "pnpm run dev:node", + "dev:node": "pnpm -C packages/node dev", + "dev:cli": "pnpm -C packages/cli dev", + "build:all": "pnpm -C packages/node build && pnpm -C packages/cli build", "build:cyberpunk": "cd examples/cyberpunk-standalone && pnpm install && pnpm build", "changeset": "changeset", "changeset:version": "changeset version", diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3d8730522b18c2cd02b280d27d0cc82c84e3a01e --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Build output +dist/ +build/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Coverage reports +coverage/ +*.lcov + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000000000000000000000000000000000000..539418e5f64fcbdc08e616abeb5088639139c41f --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,191 @@ +# lerobot + +Python lerobot compatible CLI for Node.js. Provides the same command-line interface as Python lerobot with identical behavior and syntax. + +## Installation + +```bash +# Use directly with npx (recommended) +npx lerobot find-port + +# Or install globally +npm install -g lerobot +lerobot find-port +``` + +## Commands + +### Find Port + +Discover robot port with interactive cable detection. Matches Python lerobot's `find_port.py` exactly. + +```bash +# Interactive cable detection (always enabled, like Python lerobot) +lerobot find-port +``` + +This command follows Python lerobot's behavior exactly: + +1. Lists initial ports +2. Prompts to unplug USB cable +3. Detects which port disappeared +4. Prompts to reconnect cable +5. Verifies port is restored + +### Calibrate + +Calibrate robot motors and save calibration data to Hugging Face cache. + +```bash +lerobot calibrate \ + --robot.type=so100_follower \ + --robot.port=/dev/ttyUSB0 \ + --robot.id=my_follower_arm +``` + +**Options:** + +- `--robot.type` - Robot type (e.g., `so100_follower`) +- `--robot.port` - Serial port (e.g., `/dev/ttyUSB0`, `COM4`) +- `--robot.id` - Robot identifier (default: `default`) +- `--output` - Custom output path for calibration file + +**Compatible with:** `python -m lerobot calibrate` + +### Teleoperate + +Control robot through keyboard teleoperation. + +```bash +lerobot teleoperate \ + --robot.type=so100_follower \ + --robot.port=/dev/ttyUSB0 \ + --robot.id=my_follower_arm +``` + +**Options:** + +- `--robot.type` - Robot type (e.g., `so100_follower`) +- `--robot.port` - Serial port (e.g., `/dev/ttyUSB0`, `COM4`) +- `--robot.id` - Robot identifier (default: `default`) +- `--teleop.type` - Teleoperator type (default: `keyboard`) +- `--teleop.stepSize` - Step size for keyboard control (default: `25`) +- `--duration` - Duration in seconds, 0 = unlimited (default: `0`) + +**Controls:** + +- `w/s` - Motor 1 up/down +- `a/d` - Motor 2 left/right +- `q/e` - Motor 3 up/down +- `r/f` - Motor 4 forward/back +- `t/g` - Motor 5 up/down +- `y/h` - Motor 6 open/close +- `Ctrl+C` - Stop and exit + +**Compatible with:** `python -m lerobot teleoperate` + +### Release Motors + +Release robot motors for manual movement. + +```bash +lerobot release-motors \ + --robot.type=so100_follower \ + --robot.port=/dev/ttyUSB0 +``` + +**Options:** + +- `--robot.type` - Robot type (e.g., `so100_follower`) +- `--robot.port` - Serial port (e.g., `/dev/ttyUSB0`, `COM4`) +- `--robot.id` - Robot identifier (default: `default`) +- `--motors` - Specific motor IDs to release (comma-separated) + +**Compatible with:** `python -m lerobot release-motors` + +## Python lerobot Compatibility + +This CLI provides 100% compatible commands with Python lerobot: + +| Python lerobot | Node.js lerobot | Status | +| ---------------------------------- | ---------------------------- | ------------- | +| `python -m lerobot find_port` | `npx lerobot find-port` | ✅ Compatible | +| `python -m lerobot calibrate` | `npx lerobot calibrate` | ✅ Compatible | +| `python -m lerobot teleoperate` | `npx lerobot teleoperate` | ✅ Compatible | +| `python -m lerobot release-motors` | `npx lerobot release-motors` | ✅ Compatible | + +### Calibration Data Compatibility + +Calibration files are saved to the same location as Python lerobot: + +``` +~/.cache/huggingface/lerobot/calibration/robots/{robot_type}/{robot_id}.json +``` + +This ensures calibration data is shared between Python and Node.js implementations. + +## Examples + +### Complete Workflow + +```bash +# 1. Find your robot (interactive mode) +npx lerobot find-port --interactive +# Output: Detected port: /dev/ttyUSB0 + +# 2. Calibrate the robot +npx lerobot calibrate \ + --robot.type=so100_follower \ + --robot.port=/dev/ttyUSB0 \ + --robot.id=my_arm + +# 3. Control the robot +npx lerobot teleoperate \ + --robot.type=so100_follower \ + --robot.port=/dev/ttyUSB0 \ + --robot.id=my_arm + +# 4. Release motors when done +npx lerobot release-motors \ + --robot.type=so100_follower \ + --robot.port=/dev/ttyUSB0 +``` + +### Automation Scripts + +```bash +#!/bin/bash +# Automated calibration script + +ROBOT_TYPE="so100_follower" +ROBOT_PORT="/dev/ttyUSB0" +ROBOT_ID="production_arm_1" + +echo "Starting automated calibration..." +npx lerobot calibrate \ + --robot.type=$ROBOT_TYPE \ + --robot.port=$ROBOT_PORT \ + --robot.id=$ROBOT_ID + +echo "Calibration complete. Starting teleoperation..." +npx lerobot teleoperate \ + --robot.type=$ROBOT_TYPE \ + --robot.port=$ROBOT_PORT \ + --robot.id=$ROBOT_ID \ + --duration=60 # Run for 60 seconds +``` + +## Requirements + +- Node.js 18+ +- Compatible with Windows, macOS, and Linux +- Same hardware requirements as Python lerobot + +## Related Packages + +- **[@lerobot/node](../node/)** - Node.js library for programmatic control +- **[@lerobot/web](../web/)** - Browser library for web applications + +## License + +Apache-2.0 diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000000000000000000000000000000000000..8514499d9a5da8f87494c57dd5b24c46604c5fa8 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,52 @@ +{ + "name": "lerobot", + "version": "0.1.0", + "description": "CLI for lerobot.js", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "lerobot": "./dist/cli.js" + }, + "files": [ + "dist/**/*", + "README.md" + ], + "keywords": [ + "robotics", + "cli", + "lerobot", + "python-compatible", + "hardware-control" + ], + "scripts": { + "build": "vite build", + "dev": "vite-node src/cli.ts", + "prepublishOnly": "npm run build", + "test": "echo 'No tests defined'" + }, + "dependencies": { + "@lerobot/node": "file:../node", + "chalk": "^5.3.0", + "commander": "^11.0.0", + "serialport": "^12.0.0" + }, + "devDependencies": { + "vite": "^6.3.5", + "vite-node": "^2.0.0", + "typescript": "^5.3.0", + "@types/node": "^18.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/timpietrusky/lerobot.js/tree/main/packages/cli" + }, + "license": "Apache-2.0", + "author": "Tim Pietrusky", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cli/pnpm-lock.yaml b/packages/cli/pnpm-lock.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5b653fc88c09f8f699b6467993f11b49b4960ead --- /dev/null +++ b/packages/cli/pnpm-lock.yaml @@ -0,0 +1,1167 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@lerobot/node': + specifier: file:../node + version: file:../node(typescript@5.8.3) + chalk: + specifier: ^5.3.0 + version: 5.4.1 + commander: + specifier: ^11.0.0 + version: 11.1.0 + serialport: + specifier: ^12.0.0 + version: 12.0.0 + devDependencies: + '@types/node': + specifier: ^18.0.0 + version: 18.19.121 + typescript: + specifier: ^5.3.0 + version: 5.8.3 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@18.19.121) + vite-node: + specifier: ^2.0.0 + version: 2.1.9(@types/node@18.19.121) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@lerobot/node@file:../node': + resolution: {directory: ../node, type: directory} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=4.5.0' + + '@rollup/rollup-android-arm-eabi@4.46.2': + resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.46.2': + resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.46.2': + resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.46.2': + resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.46.2': + resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.46.2': + resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.46.2': + resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.46.2': + resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.46.2': + resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.46.2': + resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} + cpu: [x64] + os: [win32] + + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@18.19.121': + resolution: {integrity: sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.46.2: + resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.8': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.8': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.8': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.8': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.8': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.8': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.8': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.8': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.8': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.8': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.8': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.8': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.8': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.8': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.8': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.8': + optional: true + + '@esbuild/netbsd-arm64@0.25.8': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.8': + optional: true + + '@esbuild/openbsd-arm64@0.25.8': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.8': + optional: true + + '@esbuild/openharmony-arm64@0.25.8': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.8': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.8': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.8': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.8': + optional: true + + '@lerobot/node@file:../node(typescript@5.8.3)': + dependencies: + serialport: 12.0.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@rollup/rollup-android-arm-eabi@4.46.2': + optional: true + + '@rollup/rollup-android-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.46.2': + optional: true + + '@rollup/rollup-darwin-x64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.46.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.46.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.46.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.46.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.46.2': + optional: true + + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-byte-length@12.0.0': {} + + '@serialport/parser-cctalk@12.0.0': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-inter-byte-timeout@12.0.0': {} + + '@serialport/parser-packet-length@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + + '@serialport/parser-ready@12.0.0': {} + + '@serialport/parser-regex@12.0.0': {} + + '@serialport/parser-slip-encoder@12.0.0': {} + + '@serialport/parser-spacepacket@12.0.0': {} + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@types/estree@1.0.8': {} + + '@types/node@18.19.121': + dependencies: + undici-types: 5.26.5 + + cac@6.7.14: {} + + chalk@5.4.1: {} + + commander@11.1.0: {} + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.8: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + ms@2.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-addon-api@7.0.0: {} + + node-gyp-build@4.6.0: {} + + pathe@1.1.2: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.46.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.2 + '@rollup/rollup-android-arm64': 4.46.2 + '@rollup/rollup-darwin-arm64': 4.46.2 + '@rollup/rollup-darwin-x64': 4.46.2 + '@rollup/rollup-freebsd-arm64': 4.46.2 + '@rollup/rollup-freebsd-x64': 4.46.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 + '@rollup/rollup-linux-arm-musleabihf': 4.46.2 + '@rollup/rollup-linux-arm64-gnu': 4.46.2 + '@rollup/rollup-linux-arm64-musl': 4.46.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 + '@rollup/rollup-linux-ppc64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-gnu': 4.46.2 + '@rollup/rollup-linux-riscv64-musl': 4.46.2 + '@rollup/rollup-linux-s390x-gnu': 4.46.2 + '@rollup/rollup-linux-x64-gnu': 4.46.2 + '@rollup/rollup-linux-x64-musl': 4.46.2 + '@rollup/rollup-win32-arm64-msvc': 4.46.2 + '@rollup/rollup-win32-ia32-msvc': 4.46.2 + '@rollup/rollup-win32-x64-msvc': 4.46.2 + fsevents: 2.3.3 + + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + typescript@5.8.3: {} + + undici-types@5.26.5: {} + + vite-node@2.1.9(@types/node@18.19.121): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@18.19.121) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.19(@types/node@18.19.121): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.46.2 + optionalDependencies: + '@types/node': 18.19.121 + fsevents: 2.3.3 + + vite@6.3.5(@types/node@18.19.121): + dependencies: + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.2 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 18.19.121 + fsevents: 2.3.3 diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 0000000000000000000000000000000000000000..6388a99ef0e1c49e874512afdc75786744d1ab10 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,441 @@ +#!/usr/bin/env node + +/** + * lerobot CLI - Python lerobot compatible command-line interface + * Uses @lerobot/node library for core functionality with CLI-specific interactive features + */ + +import { program } from "commander"; +import chalk from "chalk"; +import { + findPort, + calibrate, + teleoperate, + releaseMotors, + connectPort, +} from "@lerobot/node"; +import type { RobotConnection } from "@lerobot/node"; +import { SerialPort } from "serialport"; +import { createInterface } from "readline"; +import { platform } from "os"; +import { readdir } from "fs/promises"; +import { join } from "path"; + +/** + * CLI-specific function to list available serial ports + * Only used by the CLI, not part of the library API + */ +async function findAvailablePorts(): Promise { + if (platform() === "win32") { + // List COM ports using serialport library (equivalent to pyserial) + const ports = await SerialPort.list(); + return ports.map((port) => port.path); + } else { + // List /dev/tty* ports for Unix-based systems (Linux/macOS) + try { + const devFiles = await readdir("/dev"); + const ttyPorts = devFiles + .filter((file) => file.startsWith("tty")) + .map((file) => join("/dev", file)); + return ttyPorts; + } catch (error) { + // Fallback to serialport library if /dev reading fails + const ports = await SerialPort.list(); + return ports.map((port) => port.path); + } + } +} + +/** + * CLI-specific interactive port detection for Python lerobot compatibility + * Matches Python lerobot's unplug/replug cable detection exactly + */ +async function detectPortInteractive( + onMessage?: (message: string) => void +): Promise { + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + function waitForInput(prompt: string): Promise { + return new Promise((resolve) => { + rl.question(prompt, (answer: string) => { + resolve(answer); + }); + }); + } + + try { + const message = "Finding all available ports for the MotorsBus."; + if (onMessage) onMessage(message); + else console.log(message); + + // Get initial port list + const portsBefore = await findAvailablePorts(); + + // Show initial ports (Python lerobot style) + const portsMessage = `Ports before disconnecting: [${portsBefore + .map((p) => `'${p}'`) + .join(", ")}]`; + if (onMessage) onMessage(portsMessage); + else console.log(portsMessage); + + const disconnectPrompt = + "Remove the USB cable from your MotorsBus and press Enter when done."; + await waitForInput(disconnectPrompt); + + // Get port list after disconnect + const portsAfter = await findAvailablePorts(); + + // Find the difference + const portsDiff = portsBefore.filter((port) => !portsAfter.includes(port)); + + if (portsDiff.length === 1) { + const detectedPort = portsDiff[0]; + + // Show empty line then the result (Python lerobot style) + if (onMessage) { + onMessage(""); + onMessage(`The port of this MotorsBus is '${detectedPort}'`); + onMessage("Reconnect the USB cable."); + } else { + console.log(""); + console.log(`The port of this MotorsBus is '${detectedPort}'`); + console.log("Reconnect the USB cable."); + } + + return detectedPort; + } else if (portsDiff.length === 0) { + throw new Error( + "No port difference detected. Please check cable connection." + ); + } else { + throw new Error( + `Multiple ports detected: ${portsDiff.join( + ", " + )}. Please disconnect other devices.` + ); + } + } finally { + rl.close(); + } +} + +/** + * Create robot connection directly from specified port (Python lerobot style) + */ +async function connectToSpecificPort( + portPath: string, + robotType: string, + robotId: string +): Promise { + console.log(chalk.gray(`📡 Connecting to ${portPath}...`)); + + const connection = await connectPort(portPath); + + if (!connection.isConnected) { + throw new Error( + `Failed to connect to port ${portPath}: ${connection.error}` + ); + } + + // Configure the robot with CLI parameters + connection.robotType = robotType; + connection.robotId = robotId; + connection.name = `${robotType} on ${portPath}`; + + console.log(chalk.green(`✅ Connected to ${robotType} on ${portPath}`)); + return connection; +} + +/** + * Find port command - matches Python lerobot CLI exactly + * Always interactive by default (like Python lerobot) + */ +program + .command("find-port") + .description( + "Find robot port with interactive cable detection (Python lerobot compatible)" + ) + .action(async () => { + try { + console.log(chalk.blue("🔍 Finding robot port...")); + + // Always use interactive cable detection (Python lerobot behavior) + await detectPortInteractive((message) => + console.log(chalk.gray(message)) + ); + // No additional success message - detectPortInteractive already shows the result + } catch (error) { + console.error( + chalk.red(`❌ Error: ${error instanceof Error ? error.message : error}`) + ); + process.exit(1); + } + }); + +/** + * Calibrate command - matches Python lerobot exactly + */ +program + .command("calibrate") + .description("Calibrate robot motors") + .requiredOption("--robot.type ", "Robot type (e.g., so100_follower)") + .requiredOption( + "--robot.port ", + "Serial port (e.g., /dev/ttyUSB0, COM4)" + ) + .option("--robot.id ", "Robot ID", "default") + .option("--output ", "Output calibration file path") + .action(async (options) => { + try { + const robotType = options["robot.type"]; + const robotPort = options["robot.port"]; + const robotId = options["robot.id"] || "default"; + + console.log(chalk.blue(`🔧 Starting calibration for ${robotType}...`)); + + // Step 1: Connect directly to specified port (Python lerobot style) + const robot = await connectToSpecificPort(robotPort, robotType, robotId); + + // Step 2: Release motors + console.log(chalk.gray("🔓 Releasing motors for calibration setup...")); + await releaseMotors(robot); + console.log( + chalk.green("✅ Motors released - robot can now be moved by hand") + ); + + // Step 3: Wait for user to position robot + console.log( + chalk.yellow( + "\n📍 Move robot to your preferred starting position, then press Enter..." + ) + ); + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + await new Promise((resolve) => { + rl.question("", () => { + rl.close(); + resolve(); + }); + }); + + console.log(chalk.blue("\n🎯 Starting calibration process...")); + const calibrationProcess = await calibrate({ + robot, + outputPath: options.output, + onProgress: (message) => console.log(chalk.gray(message)), + onLiveUpdate: (data) => { + // Clear previous output and display live data as table + process.stdout.write("\x1B[2J\x1B[0f"); // Clear screen and move cursor to top + + console.log(chalk.cyan("📊 Live Motor Data:")); + console.log( + "┌─────────────────┬─────────┬─────────┬─────────┬─────────┐" + ); + console.log( + "│ Motor │ Current │ Min │ Max │ Range │" + ); + console.log( + "├─────────────────┼─────────┼─────────┼─────────┼─────────┤" + ); + + Object.entries(data).forEach(([name, info]) => { + const motorName = name.padEnd(15); + const current = info.current.toString().padStart(7); + const min = info.min.toString().padStart(7); + const max = info.max.toString().padStart(7); + const range = info.range.toString().padStart(7); + console.log( + `│ ${motorName} │ ${current} │ ${min} │ ${max} │ ${range} │` + ); + }); + + console.log( + "└─────────────────┴─────────┴─────────┴─────────┴─────────┘" + ); + console.log( + chalk.yellow( + "Move motors through full range, then press Enter when done..." + ) + ); + }, + }); + + const results = await calibrationProcess.result; + console.log(chalk.green("\n✅ Calibration completed successfully!")); + + // CRITICAL: Close robot connection to allow process to exit + if (robot.port && robot.port.close) { + await robot.port.close(); + } + } catch (error) { + console.error( + chalk.red( + `❌ Calibration failed: ${ + error instanceof Error ? error.message : error + }` + ) + ); + + // Close robot connection even on error + try { + if (robot && robot.port && robot.port.close) { + await robot.port.close(); + } + } catch (closeError) { + // Ignore close errors + } + + process.exit(1); + } + }); + +/** + * Teleoperate command - matches Python lerobot exactly + */ +program + .command("teleoperate") + .alias("teleop") + .description("Control robot through teleoperation") + .requiredOption("--robot.type ", "Robot type (e.g., so100_follower)") + .requiredOption( + "--robot.port ", + "Serial port (e.g., /dev/ttyUSB0, COM4)" + ) + .option("--robot.id ", "Robot ID", "default") + .option("--teleop.type ", "Teleoperator type", "keyboard") + .option("--duration ", "Duration in seconds (0 = unlimited)", "0") + .action(async (options) => { + try { + const robotType = options["robot.type"]; + const robotPort = options["robot.port"]; + const robotId = options["robot.id"] || "default"; + const teleopType = options["teleop.type"] || "keyboard"; + + console.log(chalk.blue(`🎮 Starting teleoperation for ${robotType}...`)); + + // Connect directly to specified port (Python lerobot style) + const robot = await connectToSpecificPort(robotPort, robotType, robotId); + + const teleoperationProcess = await teleoperate({ + robot, + teleop: { + type: teleopType, + }, + onStateUpdate: (state) => { + if (state.isActive) { + const motorInfo = state.motorConfigs + .map( + (motor) => `${motor.name}:${Math.round(motor.currentPosition)}` + ) + .join(" "); + process.stdout.write(`\r${chalk.cyan("🤖 Motors:")} ${motorInfo}`); + } + }, + }); + + // Start teleoperation + teleoperationProcess.start(); + + // Handle duration limit + const duration = parseInt(options.duration || "0"); + if (duration > 0) { + setTimeout(() => { + console.log( + chalk.yellow( + `\n⏰ Duration limit reached (${duration}s). Stopping...` + ) + ); + teleoperationProcess.stop(); + process.exit(0); + }, duration * 1000); + } + + // Handle process termination + process.on("SIGINT", async () => { + console.log(chalk.yellow("\n🛑 Stopping teleoperation...")); + teleoperationProcess.stop(); + await teleoperationProcess.disconnect(); + process.exit(0); + }); + + console.log(chalk.green("✅ Teleoperation started successfully!")); + console.log(chalk.gray("Press Ctrl+C to stop")); + } catch (error) { + console.error( + chalk.red( + `❌ Teleoperation failed: ${ + error instanceof Error ? error.message : error + }` + ) + ); + process.exit(1); + } + }); + +/** + * Release motors command + */ +program + .command("release-motors") + .description("Release robot motors for manual movement") + .requiredOption("--robot.type ", "Robot type (e.g., so100_follower)") + .requiredOption( + "--robot.port ", + "Serial port (e.g., /dev/ttyUSB0, COM4)" + ) + .option("--robot.id ", "Robot ID", "default") + .option("--motors ", "Specific motor IDs to release (comma-separated)") + .action(async (options) => { + try { + const robotType = options["robot.type"]; + const robotPort = options["robot.port"]; + const robotId = options["robot.id"] || "default"; + + console.log(chalk.blue(`🔓 Releasing motors for ${robotType}...`)); + + // Connect directly to specified port (Python lerobot style) + const robot = await connectToSpecificPort(robotPort, robotType, robotId); + + const motorIds = options.motors + ? options.motors.split(",").map((id: string) => parseInt(id.trim())) + : undefined; + + await releaseMotors(robot, motorIds); + + console.log(chalk.green("✅ Motors released successfully!")); + console.log(chalk.gray("Motors can now be moved freely by hand.")); + } catch (error) { + console.error( + chalk.red( + `❌ Failed to release motors: ${ + error instanceof Error ? error.message : error + }` + ) + ); + process.exit(1); + } + }); + +/** + * Version and help setup + */ +program + .name("lerobot") + .description("Node.js robotics control CLI - Python lerobot compatible") + .version("0.1.0"); + +/** + * Parse CLI arguments and run + */ +program.parse(); + +// Show help if no command provided +if (!process.argv.slice(2).length) { + program.outputHelp(); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb23524f6d5fa5dbaf922fb2685f589141f60413 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,13 @@ +/** + * @lerobot/cli - Python lerobot compatible CLI commands + * + * This package provides CLI commands that match Python lerobot exactly: + * - lerobot find-port (with interactive cable detection) + * - lerobot calibrate + * - lerobot teleoperate + * - lerobot release-motors + * + * Uses @lerobot/node library for core functionality + */ + +export * from "./cli.js"; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..aea5aa52a5b67bcb965061e8242a559e6eff459e --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "noEmit": false, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "dist", "node_modules"] +} diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e9bd87062fbd58edbbced29983e567e116a9ed8 --- /dev/null +++ b/packages/cli/vite.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + target: "node18", + lib: { + entry: { + cli: "src/cli.ts", + index: "src/index.ts", + }, + formats: ["es"], + }, + rollupOptions: { + external: [ + // Node.js built-ins + "fs", + "fs/promises", + "path", + "os", + "readline", + "util", + "events", + "stream", + // External dependencies + "@lerobot/node", + "chalk", + "commander", + "serialport", + ], + }, + minify: false, + sourcemap: true, + }, +}); diff --git a/packages/node/.eslintrc.json b/packages/node/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..a913b471920c6a15d8be6a94bbead88bab8ef092 --- /dev/null +++ b/packages/node/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "@typescript-eslint/recommended"], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "env": { + "node": true, + "es2022": true + }, + "rules": { + "@typescript-eslint/no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_" } + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-assertion": "warn" + }, + "ignorePatterns": ["dist/", "node_modules/"] +} diff --git a/packages/node/.gitignore b/packages/node/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6701f09b4bb0067f4d33c64488ac8c62c067f39b --- /dev/null +++ b/packages/node/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.production + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Test coverage +coverage/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock \ No newline at end of file diff --git a/packages/node/README.md b/packages/node/README.md new file mode 100644 index 0000000000000000000000000000000000000000..891cf669f5290b44e0daf6cc5587df4b0f406622 --- /dev/null +++ b/packages/node/README.md @@ -0,0 +1,408 @@ +# @lerobot/node + +Control robots with Node.js (serialport), inspired by [LeRobot](https://github.com/huggingface/lerobot) + +🚀 **[Try the live (web) demo →](https://huggingface.co/spaces/NERDDISCO/LeRobot.js)** + +## Installation + +```bash +# pnpm +pnpm add @lerobot/node + +# npm +npm install @lerobot/node + +# yarn +yarn add @lerobot/node +``` + +## Quick Start + +```typescript +import { + findPort, + connectPort, + releaseMotors, + calibrate, + teleoperate, +} from "@lerobot/node"; + +// 1. find available robot ports +console.log("🔍 finding available robot ports..."); +const findProcess = await findPort(); +const robots = await findProcess.result; + +if (robots.length === 0) { + console.log("❌ no robots found. check connections."); + process.exit(1); +} + +// 2. connect to the first robot found +console.log(`✅ found ${robots.length} robot(s). connecting to first one...`); +const robot = await connectPort(robots[0].path, robots[0].robotType); + +// 3. release motors for manual positioning +console.log("🔓 releasing motors for manual positioning..."); +await releaseMotors(robot); + +// 4. calibrate motors by moving through full range +console.log("⚙️ starting calibration..."); +const calibrationProcess = await calibrate({ + robot, + onProgress: (message) => console.log(message), + onLiveUpdate: (data) => console.log("live positions:", data), +}); + +// move robot through its range, then stop calibration +console.log("👋 move robot through full range, press enter when done..."); +process.stdin.once("data", () => { + calibrationProcess.stop(); +}); + +const calibrationData = await calibrationProcess.result; +console.log("✅ calibration complete!"); + +// 5. control robot with keyboard +console.log("🎮 starting keyboard control..."); +const teleop = await teleoperate({ + robot, + calibrationData, + teleop: { type: "keyboard" }, +}); +teleop.start(); + +// stop control after 30 seconds +setTimeout(() => { + teleop.stop(); + console.log("🛑 control stopped"); +}, 30000); +``` + +## How It Works + +### **Beginner Flow: `findPort()` → `connectPort()` → Use Robot** + +Most users should start with `findPort()` for discovery, then `connectPort()` for connection: + +```typescript +// ✅ recommended: discover then connect +const findProcess = await findPort(); +const robots = await findProcess.result; +const robot = await connectPort(robots[0].path, robots[0].robotType); +``` + +### **Advanced: Direct Connection with `connectPort()`** + +Only use `connectPort()` when you already know the exact port: + +```typescript +// ⚡ advanced: direct connection to known port +const robot = await connectPort("/dev/ttyUSB0", "so100_follower"); +``` + +## Core API + +### `findPort(config?): Promise` + +Discovers available robotics hardware on serial ports. Unlike the web version, this only discovers ports - connection happens separately with `connectPort()`. + +```typescript +// Discover all available robots +const findProcess = await findPort(); +const robots = await findProcess.result; + +console.log(`Found ${robots.length} robot(s):`); +robots.forEach((robot) => { + console.log(`- ${robot.robotType} on ${robot.path}`); +}); + +// Connect to specific robot +const robot = await connectPort(robots[0].path, robots[0].robotType); +``` + +#### Options + +- `config?: FindPortConfig` - Optional configuration + - `onMessage?: (message: string) => void` - Progress messages callback + +#### Returns: `FindPortProcess` + +- `result: Promise` - Array of discovered robots with `path`, `robotType`, and other metadata +- `stop(): void` - Cancel discovery process + +#### DiscoveredRobot Structure + +```typescript +interface DiscoveredRobot { + path: string; // Serial port path (e.g., "/dev/ttyUSB0") + robotType: "so100_follower" | "so100_leader"; + // Additional metadata... +} +``` + +--- + +### `connectPort(port): Promise` + +Creates a connection to a robot on the specified serial port. + +```typescript +// Connect to SO-100 follower arm +const robot = await connectPort( + "/dev/ttyUSB0", // Serial port path + "so100_follower", // Robot type + "my_robot_arm" // Custom robot ID +); + +// Windows +const robot = await connectPort("COM4", "so100_follower", "my_robot"); + +// Connection is ready to use +console.log(`Connected to ${robot.robotType} on ${robot.port.path}`); +``` + +#### Parameters + +- `port: string` - Serial port path (e.g., `/dev/ttyUSB0`, `COM4`) +- `robotType: "so100_follower" | "so100_leader"` - Type of robot +- `robotId: string` - Custom identifier for your robot + +#### Returns: `Promise` + +- Initialized robot connection ready for calibration/teleoperation +- Includes configured motor IDs, keyboard controls, and hardware settings + +--- + +### `calibrate(config): Promise` + +Calibrates motor homing offsets and records range of motion. **Identical to Python lerobot behavior.** + +```typescript +const calibrationProcess = await calibrate({ + robot, + onProgress: (message) => { + console.log(message); // "⚙️ Setting motor homing offsets" + }, + onLiveUpdate: (data) => { + // Real-time motor positions during range recording + Object.entries(data).forEach(([motor, info]) => { + console.log(`${motor}: ${info.current} (range: ${info.range})`); + }); + }, +}); + +// Move robot through full range of motion... +// When finished, stop calibration +calibrationProcess.stop(); + +const calibrationData = await calibrationProcess.result; + +// Save to file (Python-compatible format) +import { writeFileSync } from "fs"; +writeFileSync( + "./my_robot_calibration.json", + JSON.stringify(calibrationData, null, 2) +); +``` + +#### Options + +- `config: CalibrateConfig` + - `robot: RobotConnection` - Connected robot from `connectPort()` + - `onProgress?: (message: string) => void` - Progress messages + - `onLiveUpdate?: (data: LiveCalibrationData) => void` - Real-time position updates + +#### Returns: `CalibrationProcess` + +- `result: Promise` - **Python-compatible** calibration data +- `stop(): void` - Stop calibration process + +#### Calibration Data Format + +**Python Compatible**: This format is identical to Python lerobot calibration files - you can use the same calibration data across both implementations. + +```json +{ + "shoulder_pan": { + "id": 1, + "drive_mode": 0, + "homing_offset": 14, + "range_min": 1015, + "range_max": 3128 + }, + "shoulder_lift": { + "id": 2, + "drive_mode": 0, + "homing_offset": 989, + "range_min": 965, + "range_max": 3265 + }, + "elbow_flex": { + "id": 3, + "drive_mode": 0, + "homing_offset": -879, + "range_min": 820, + "range_max": 3051 + }, + "wrist_flex": { + "id": 4, + "drive_mode": 0, + "homing_offset": 31, + "range_min": 758, + "range_max": 3277 + }, + "wrist_roll": { + "id": 5, + "drive_mode": 0, + "homing_offset": -37, + "range_min": 2046, + "range_max": 3171 + }, + "gripper": { + "id": 6, + "drive_mode": 0, + "homing_offset": -1173, + "range_min": 2038, + "range_max": 3528 + } +} +``` + +--- + +### `teleoperate(config): Promise` + +Real-time robot control with keyboard input. **Smooth, responsive movement** optimized for Node.js. + +#### Keyboard Teleoperation + +```typescript +const teleop = await teleoperate({ + robot, + teleop: { type: "keyboard" }, + onStateUpdate: (state) => { + console.log(`Active: ${state.isActive}`); + state.motorConfigs.forEach((motor) => { + console.log(`${motor.name}: ${motor.currentPosition}`); + }); + }, +}); + +// Start keyboard control +teleop.start(); + +// Control will be active until stopped +setTimeout(() => teleop.stop(), 60000); +``` + +#### Options + +- `config: TeleoperateConfig` + - `robot: RobotConnection` - Connected robot + - `teleop: TeleoperatorConfig` - Teleoperator configuration: + - `{ type: "keyboard" }` - Keyboard control with optimized defaults + - `onStateUpdate?: (state: TeleoperationState) => void` - State change callback + +#### Returns: `TeleoperationProcess` + +- `start(): void` - Begin teleoperation (shows keyboard controls) +- `stop(): void` - Stop teleoperation +- `getState(): TeleoperationState` - Current state and motor positions + +#### Keyboard Controls (SO-100) + +``` +Arrow Keys: Shoulder pan/lift +WASD: Elbow flex, wrist flex +Q/E: Wrist roll +O/C: Gripper open/close +ESC: Emergency stop +Ctrl+C: Exit +``` + +#### Performance Characteristics + +- **120 Hz update rate** for smooth movement +- **Immediate response** on keypress (no delay) +- **8-unit step size** matching browser demo +- **150ms key timeout** for optimal single-tap vs hold behavior + +--- + +### `releaseMotors(robot): Promise` + +Releases motor torque so robot can be moved freely by hand. + +```typescript +// Release all motors for calibration +await releaseMotors(robot); +console.log("Motors released - you can now move the robot freely"); +``` + +#### Parameters + +- `robot: RobotConnection` - Connected robot + +--- + +## CLI Usage + +For command-line usage, install the CLI package: + +```bash +# Install CLI globally +pnpm add -g lerobot + +# Find and connect to robot +npx lerobot find-port + +# Calibrate robot +npx lerobot calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot + +# Control robot with keyboard +npx lerobot teleoperate --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot + +# Release motors +npx lerobot release-motors --robot.type so100_follower --robot.port /dev/ttyUSB0 --robot.id my_robot +``` + +**CLI commands are identical to Python lerobot** - same syntax, same behavior, seamless migration. + +## Node.js Requirements + +- **Node.js 18+** +- **Serial port access** (may require permissions on Linux/macOS) +- **Supported platforms**: Windows, macOS, Linux + +### Serial Port Permissions + +**Linux/macOS:** + +```bash +# Add user to dialout group (Linux) +sudo usermod -a -G dialout $USER + +# Set permissions (macOS) +sudo chmod 666 /dev/tty.usbserial-* +``` + +**Windows:** No additional setup required. + +## Hardware Support + +Currently supports SO-100 follower and leader arms with STS3215 motors. More devices coming soon. + +## Migration from lerobot.py + +```python +# Python lerobot +python -m lerobot.calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0 + +# Node.js equivalent +npx lerobot calibrate --robot.type so100_follower --robot.port /dev/ttyUSB0 +``` + +- **Same commands** - just replace `python -m lerobot.` with `npx lerobot` +- **Same calibration files** - Python and Node.js calibrations are interchangeable diff --git a/packages/node/package.json b/packages/node/package.json new file mode 100644 index 0000000000000000000000000000000000000000..13536524553864446d1f2e84713682bf0fd51e92 --- /dev/null +++ b/packages/node/package.json @@ -0,0 +1,76 @@ +{ + "name": "@lerobot/node", + "version": "0.1.0", + "description": "Node.js-based robotics control using SerialPort", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./calibrate": { + "import": "./dist/calibrate.js", + "types": "./dist/calibrate.d.ts" + }, + "./teleoperate": { + "import": "./dist/teleoperate.js", + "types": "./dist/teleoperate.d.ts" + }, + "./find-port": { + "import": "./dist/find_port.js", + "types": "./dist/find_port.d.ts" + } + }, + "files": [ + "dist/**/*", + "README.md" + ], + "keywords": [ + "robotics", + "serialport", + "hardware-control", + "nodejs", + "typescript" + ], + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "lint": "eslint src --ext .ts,.tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint src --ext .ts,.tsx --fix", + "prepublishOnly": "npm run build", + "test": "vitest run", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage" + }, + "dependencies": { + "serialport": "^12.0.0" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@typescript-eslint/eslint-plugin": "^8.41.0", + "@typescript-eslint/parser": "^8.41.0", + "@vitest/ui": "^2.0.0", + "eslint": "^9.34.0", + "typescript": "^5.3.0", + "vite": "^6.3.5", + "vite-node": "^2.0.0", + "vitest": "^2.0.0" + }, + "peerDependencies": { + "typescript": ">=4.5.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/timpietrusky/lerobot.js/tree/main/packages/node" + }, + "license": "Apache-2.0", + "author": "Tim Pietrusky", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/node/pnpm-lock.yaml b/packages/node/pnpm-lock.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c1370152908680549e1c4a2bde191f61b9c19b05 --- /dev/null +++ b/packages/node/pnpm-lock.yaml @@ -0,0 +1,2396 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + serialport: + specifier: ^12.0.0 + version: 12.0.0 + devDependencies: + '@types/node': + specifier: ^18.0.0 + version: 18.19.123 + '@typescript-eslint/eslint-plugin': + specifier: ^8.41.0 + version: 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0)(typescript@5.8.3))(eslint@9.34.0)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.41.0 + version: 8.41.0(eslint@9.34.0)(typescript@5.8.3) + '@vitest/ui': + specifier: ^2.0.0 + version: 2.1.9(vitest@2.1.9) + eslint: + specifier: ^9.34.0 + version: 9.34.0 + typescript: + specifier: ^5.3.0 + version: 5.8.3 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@18.19.123) + vite-node: + specifier: ^2.0.0 + version: 2.1.9(@types/node@18.19.123) + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@18.19.123)(@vitest/ui@2.1.9) + +packages: + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.34.0': + resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rollup/rollup-android-arm-eabi@4.46.0': + resolution: {integrity: sha512-9f3nSTFI2ivfxc7/tHBHcJ8pRnp8ROrELvsVprlQPVvcZ+j5zztYd+PTJGpyIOAdTvNwNrpCXswKSeoQcyGjMQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.46.0': + resolution: {integrity: sha512-tFZSEhqJ8Yrpe50TzOdeoYi72gi/jsnT7y8Qrozf3cNu28WX+s6I3XzEPUAqoaT9SAS8Xz9AzGTFlxxCH/w20w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.46.0': + resolution: {integrity: sha512-+DikIIs+p6yU2hF51UaWG8BnHbq90X0QIOt5zqSKSZxY+G3qqdLih214e9InJal21af2PuuxkDectetGfbVPJw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.46.0': + resolution: {integrity: sha512-5a+NofhdEB/WimSlFMskbFQn1vqz1FWryYpA99trmZGO6qEmiS0IsX6w4B3d91U878Q2ZQdiaFF1gxX4P147og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.46.0': + resolution: {integrity: sha512-igr/RlKPS3OCy4jD3XBmAmo3UAcNZkJSubRsw1JeM8bAbwf15k/3eMZXD91bnjheijJiOJcga3kfCLKjV8IXNg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.46.0': + resolution: {integrity: sha512-MdigWzPSHlQzB1xZ+MdFDWTAH+kcn7UxjEBoOKuaso7z1DRlnAnrknB1mTtNOQ+GdPI8xgExAGwHeqQjntR0Cg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.46.0': + resolution: {integrity: sha512-dmZseE0ZwA/4yy1+BwFrDqFTjjNg24GO9xSrb1weVbt6AFkhp5pz1gVS7IMtfIvoWy8yp6q/zN0bKnefRUImvQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.46.0': + resolution: {integrity: sha512-fzhfn6p9Cfm3W8UrWKIa4l7Wfjs/KGdgaswMBBE3KY3Ta43jg2XsPrAtfezHpsRk0Nx+TFuS3hZk/To2N5kFPQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.46.0': + resolution: {integrity: sha512-vVDD+iPDPmJQ5nAQ5Tifq3ywdv60FartglFI8VOCK+hcU9aoG0qlQTsDJP97O5yiTaTqlneZWoARMcVC5nyUoQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.46.0': + resolution: {integrity: sha512-0d0jx08fzDHCzXqrtCMEEyxKU0SvJrWmUjUDE2/KDQ2UDJql0tfiwYvEx1oHELClKO8CNdE+AGJj+RqXscZpdQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.46.0': + resolution: {integrity: sha512-XBYu9oW9eKJadWn8M7hkTZsD4yG+RrsTrVEgyKwb4L72cpJjRbRboTG9Lg9fec8MxJp/cfTHAocg4mnismQR8A==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.46.0': + resolution: {integrity: sha512-wJaRvcT17PoOK6Ggcfo3nouFlybHvARBS4jzT0PC/lg17fIJHcDS2fZz3sD+iA4nRlho2zE6OGbU0HvwATdokQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.46.0': + resolution: {integrity: sha512-GZ5bkMFteAGkcmh8x0Ok4LSa+L62Ez0tMsHPX6JtR0wl4Xc3bQcrFHDiR5DGLEDFtGrXih4Nd/UDaFqs968/wA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.46.0': + resolution: {integrity: sha512-7CjPw6FflFsVOUfWOrVrREiV3IYXG4RzZ1ZQUaT3BtSK8YXN6x286o+sruPZJESIaPebYuFowmg54ZdrkVBYog==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.46.0': + resolution: {integrity: sha512-nmvnl0ZiuysltcB/cKjUh40Rx4FbSyueERDsl2FLvLYr6pCgSsvGr3SocUT84svSpmloS7f1DRWqtRha74Gi1w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.46.0': + resolution: {integrity: sha512-Cv+moII5C8RM6gZbR3cb21o6rquVDZrN2o81maROg1LFzBz2dZUwIQSxFA8GtGZ/F2KtsqQ2z3eFPBb6akvQNg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.46.0': + resolution: {integrity: sha512-PHcMG8DZTM9RCIjp8QIfN0VYtX0TtBPnWOTRurFhoCDoi9zptUZL2k7pCs+5rgut7JAiUsYy+huyhVKPcmxoog==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.46.0': + resolution: {integrity: sha512-1SI/Rd47e8aQJeFWMDg16ET+fjvCcD/CzeaRmIEPmb05hx+3cCcwIF4ebUag4yTt/D1peE+Mgp0+Po3M358cAA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.46.0': + resolution: {integrity: sha512-JwOCYxmumFDfDhx4kNyz6kTVK3gWzBIvVdMNzQMRDubcoGRDniOOmo6DDNP42qwZx3Bp9/6vWJ+kNzNqXoHmeA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.46.0': + resolution: {integrity: sha512-IPMIfrfkG1GaEXi+JSsQEx8x9b4b+hRZXO7KYc2pKio3zO2/VDXDs6B9Ts/nnO+25Fk1tdAVtUn60HKKPPzDig==} + cpu: [x64] + os: [win32] + + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@18.19.123': + resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==} + + '@typescript-eslint/eslint-plugin@8.41.0': + resolution: {integrity: sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.41.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.41.0': + resolution: {integrity: sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.41.0': + resolution: {integrity: sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.41.0': + resolution: {integrity: sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.41.0': + resolution: {integrity: sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.41.0': + resolution: {integrity: sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.41.0': + resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.41.0': + resolution: {integrity: sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.41.0': + resolution: {integrity: sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.41.0': + resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/ui@2.1.9': + resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==} + peerDependencies: + vitest: 2.1.9 + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.34.0: + resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@3.2.0: + resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.46.0: + resolution: {integrity: sha512-ONmkT3Ud3IfW15nl7l4qAZko5/2iZ5ALVBDh02ZSZ5IGVLJSYkRcRa3iB58VyEIyoofs9m2xdVrm+lTi97+3pw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.8': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.8': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.8': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.8': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.8': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.8': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.8': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.8': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.8': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.8': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.8': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.8': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.8': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.8': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.8': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.8': + optional: true + + '@esbuild/netbsd-arm64@0.25.8': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.8': + optional: true + + '@esbuild/openbsd-arm64@0.25.8': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.8': + optional: true + + '@esbuild/openharmony-arm64@0.25.8': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.8': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.8': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.8': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.8': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0)': + dependencies: + eslint: 9.34.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.34.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@polka/url@1.0.0-next.29': {} + + '@rollup/rollup-android-arm-eabi@4.46.0': + optional: true + + '@rollup/rollup-android-arm64@4.46.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.46.0': + optional: true + + '@rollup/rollup-darwin-x64@4.46.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.46.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.46.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.46.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.46.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.46.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.46.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.46.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.46.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.46.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.46.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.46.0': + optional: true + + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-byte-length@12.0.0': {} + + '@serialport/parser-cctalk@12.0.0': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-inter-byte-timeout@12.0.0': {} + + '@serialport/parser-packet-length@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + + '@serialport/parser-ready@12.0.0': {} + + '@serialport/parser-regex@12.0.0': {} + + '@serialport/parser-slip-encoder@12.0.0': {} + + '@serialport/parser-spacepacket@12.0.0': {} + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@18.19.123': + dependencies: + undici-types: 5.26.5 + + '@typescript-eslint/eslint-plugin@8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0)(typescript@5.8.3))(eslint@9.34.0)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.41.0(eslint@9.34.0)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/type-utils': 8.41.0(eslint@9.34.0)(typescript@5.8.3) + '@typescript-eslint/utils': 8.41.0(eslint@9.34.0)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.41.0 + eslint: 9.34.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.41.0(eslint@9.34.0)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.41.0 + debug: 4.4.1 + eslint: 9.34.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.41.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.8.3) + '@typescript-eslint/types': 8.41.0 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.41.0': + dependencies: + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/visitor-keys': 8.41.0 + + '@typescript-eslint/tsconfig-utils@8.41.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.41.0(eslint@9.34.0)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.41.0(eslint@9.34.0)(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.34.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.41.0': {} + + '@typescript-eslint/typescript-estree@8.41.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.41.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.8.3) + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/visitor-keys': 8.41.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.41.0(eslint@9.34.0)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0) + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.8.3) + eslint: 9.34.0 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.41.0': + dependencies: + '@typescript-eslint/types': 8.41.0 + eslint-visitor-keys: 4.2.1 + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@18.19.123))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@18.19.123) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.17 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/ui@2.1.9(vitest@2.1.9)': + dependencies: + '@vitest/utils': 2.1.9 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 1.1.2 + sirv: 3.0.1 + tinyglobby: 0.2.14 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@18.19.123)(@vitest/ui@2.1.9) + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.0 + tinyrainbow: 1.2.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + cac@6.7.14: {} + + callsites@3.1.0: {} + + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.0 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.25.8: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.34.0: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.34.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.2.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fflate@0.8.2: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fsevents@2.3.3: + optional: true + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@3.2.0: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + mrmime@2.0.1: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-addon-api@7.0.0: {} + + node-gyp-build@4.6.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + resolve-from@4.0.0: {} + + reusify@1.1.0: {} + + rollup@4.46.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.46.0 + '@rollup/rollup-android-arm64': 4.46.0 + '@rollup/rollup-darwin-arm64': 4.46.0 + '@rollup/rollup-darwin-x64': 4.46.0 + '@rollup/rollup-freebsd-arm64': 4.46.0 + '@rollup/rollup-freebsd-x64': 4.46.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.46.0 + '@rollup/rollup-linux-arm-musleabihf': 4.46.0 + '@rollup/rollup-linux-arm64-gnu': 4.46.0 + '@rollup/rollup-linux-arm64-musl': 4.46.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.46.0 + '@rollup/rollup-linux-ppc64-gnu': 4.46.0 + '@rollup/rollup-linux-riscv64-gnu': 4.46.0 + '@rollup/rollup-linux-riscv64-musl': 4.46.0 + '@rollup/rollup-linux-s390x-gnu': 4.46.0 + '@rollup/rollup-linux-x64-gnu': 4.46.0 + '@rollup/rollup-linux-x64-musl': 4.46.0 + '@rollup/rollup-win32-arm64-msvc': 4.46.0 + '@rollup/rollup-win32-ia32-msvc': 4.46.0 + '@rollup/rollup-win32-x64-msvc': 4.46.0 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + semver@7.7.2: {} + + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.9.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@5.8.3: {} + + undici-types@5.26.5: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@2.1.9(@types/node@18.19.123): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@18.19.123) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.19(@types/node@18.19.123): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.46.0 + optionalDependencies: + '@types/node': 18.19.123 + fsevents: 2.3.3 + + vite@6.3.5(@types/node@18.19.123): + dependencies: + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.46.0 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 18.19.123 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@18.19.123)(@vitest/ui@2.1.9): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@18.19.123)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@18.19.123) + vite-node: 2.1.9(@types/node@18.19.123) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 18.19.123 + '@vitest/ui': 2.1.9(vitest@2.1.9) + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} diff --git a/packages/node/src/calibrate.ts b/packages/node/src/calibrate.ts new file mode 100644 index 0000000000000000000000000000000000000000..4748c541eb26d7c22a2863bf968dde00c3f4872d --- /dev/null +++ b/packages/node/src/calibrate.ts @@ -0,0 +1,288 @@ +/** + * Node.js calibration functionality using serialport API + * Provides both Python lerobot compatible CLI behavior and programmatic usage + * Uses proven calibration algorithms with web-compatible API + */ + +import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; +import { createSO100Config } from "./robots/so100_config.js"; +import { + readAllMotorPositions, + releaseMotors as releaseMotorsLowLevel, + type MotorCommunicationPort, +} from "./utils/motor-communication.js"; +import { + setHomingOffsets, + writeHardwarePositionLimits, +} from "./utils/motor-calibration.js"; +import { createInterface } from "readline"; +import { writeFile } from "fs/promises"; +import { join } from "path"; +import { homedir } from "os"; + +// Debug logging removed - calibration working perfectly +import type { + CalibrateConfig, + CalibrationResults, + LiveCalibrationData, + CalibrationProcess, +} from "./types/calibration.js"; +import type { RobotConnection } from "./types/robot-connection.js"; + +/** + * Get calibration file path (matches Python lerobot location) + */ +function getCalibrationFilePath(robotType: string, robotId: string): string { + const HF_HOME = + process.env.HF_HOME || join(homedir(), ".cache", "huggingface"); + const calibrationDir = join( + HF_HOME, + "lerobot", + "calibration", + "robots", + robotType + ); + return join(calibrationDir, `${robotId}.json`); +} + +/** + * Create readline interface for user input + */ +function createReadlineInterface() { + return createInterface({ + input: process.stdin, + output: process.stdout, + }); +} + +/** + * Wait for user input with a prompt + */ +function waitForInput(rl: any, prompt: string): Promise { + return new Promise((resolve) => { + rl.question(prompt, (answer: string) => { + resolve(answer); + }); + }); +} + +/** + * Record ranges of motion with live updates + */ +async function recordRangesOfMotion( + port: MotorCommunicationPort, + motorIds: number[], + motorNames: string[], + shouldStop: () => boolean, + onLiveUpdate?: (data: LiveCalibrationData) => void, + onProgress?: (message: string) => void +): Promise<{ + rangeMins: { [motor: string]: number }; + rangeMaxes: { [motor: string]: number }; +}> { + const rangeMins: { [motor: string]: number } = {}; + const rangeMaxes: { [motor: string]: number } = {}; + + // Read actual current positions (now centered due to applied homing offsets) + const startPositions = await readAllMotorPositions(port, motorIds); + + for (let i = 0; i < motorNames.length; i++) { + const motorName = motorNames[i]; + rangeMins[motorName] = startPositions[i]; + rangeMaxes[motorName] = startPositions[i]; + } + + if (onProgress) { + onProgress( + "Move each motor through its full range of motion. The ranges will be recorded automatically." + ); + onProgress( + "Press Enter when you have finished moving all motors through their ranges." + ); + } else { + console.log( + "Move each motor through its full range of motion. The ranges will be recorded automatically." + ); + console.log( + "Press Enter when you have finished moving all motors through their ranges." + ); + } + + // Set up readline for user input + const rl = createReadlineInterface(); + let isRecording = true; + + // Start recording in background + const recordingInterval = setInterval(async () => { + if (!isRecording) return; + + try { + const currentPositions = await readAllMotorPositions(port, motorIds); + const liveData: LiveCalibrationData = {}; + + for (let i = 0; i < motorNames.length; i++) { + const motorName = motorNames[i]; + const position = currentPositions[i]; + + // Update ranges + rangeMins[motorName] = Math.min(rangeMins[motorName], position); + rangeMaxes[motorName] = Math.max(rangeMaxes[motorName], position); + + // Build live data + liveData[motorName] = { + current: position, + min: rangeMins[motorName], + max: rangeMaxes[motorName], + range: rangeMaxes[motorName] - rangeMins[motorName], + }; + } + + if (onLiveUpdate) { + onLiveUpdate(liveData); + } + } catch (error) { + // Silent - continue recording + } + }, 100); // Update every 100ms + + // Wait for user to finish + try { + await waitForInput(rl, ""); + // IMMEDIATELY stop recording and live updates + isRecording = false; + clearInterval(recordingInterval); + } finally { + // Ensure cleanup even if there's an error + isRecording = false; + clearInterval(recordingInterval); + rl.close(); + } + + return { rangeMins, rangeMaxes }; +} + +/** + * Main calibrate function with web-compatible API + */ +export async function calibrate( + config: CalibrateConfig +): Promise { + const { robot, onLiveUpdate, onProgress, outputPath } = config; + + // Validate robot configuration + if (!robot.robotType) { + throw new Error( + "Robot type is required for calibration. Please configure the robot first." + ); + } + + if (!robot.isConnected || !robot.port) { + throw new Error( + "Robot is not connected. Please use findPort() to connect first." + ); + } + + let shouldStop = false; + let port: NodeSerialPortWrapper | null = null; + + const calibrationPromise = (async (): Promise => { + try { + // Use the EXISTING port connection (don't create new one!) + port = robot.port; + + // Get robot-specific configuration + let robotConfig; + if (robot.robotType.startsWith("so100")) { + robotConfig = createSO100Config(robot.robotType); + } else { + throw new Error(`Unsupported robot type: ${robot.robotType}`); + } + + const { motorIds, motorNames, driveModes } = robotConfig; + + // Debug logging removed - calibration working perfectly + + // Starting calibration silently + + // Step 1: Set homing offsets (motors should already be released and positioned) + // Note: Motors should be released BEFORE calling calibrate(), not inside it + // Setting homing offsets silently + const homingOffsets = await setHomingOffsets(port, motorIds, motorNames); + + // Early debug test removed - calibration working perfectly + + if (shouldStop) throw new Error("Calibration stopped by user"); + + // Step 2: Record ranges of motion silently + const { rangeMins, rangeMaxes } = await recordRangesOfMotion( + port, + motorIds, + motorNames, + () => shouldStop, + onLiveUpdate, + onProgress + ); + + if (shouldStop) throw new Error("Calibration stopped by user"); + + // Step 3: Write hardware position limits silently + await writeHardwarePositionLimits( + port, + motorIds, + motorNames, + rangeMins, + rangeMaxes + ); + + // Step 4: Skip motor locking (Python lerobot doesn't lock motors after calibration) + + // Build calibration results (Python lerobot compatible format) + + const calibrationResults: CalibrationResults = {}; + for (let i = 0; i < motorNames.length; i++) { + const motorName = motorNames[i]; + const homingOffsetValue = homingOffsets[motorName]; + + calibrationResults[motorName] = { + id: motorIds[i], + drive_mode: driveModes[i], + homing_offset: homingOffsetValue, + range_min: rangeMins[motorName], + range_max: rangeMaxes[motorName], + }; + } + + // Save calibration file + const calibrationPath = + outputPath || + getCalibrationFilePath(robot.robotType, robot.robotId || "default"); + + // Ensure directory exists + const { mkdir } = await import("fs/promises"); + const { dirname } = await import("path"); + await mkdir(dirname(calibrationPath), { recursive: true }); + + await writeFile( + calibrationPath, + JSON.stringify(calibrationResults, null, 2) + ); + + if (onProgress) { + onProgress(`Calibration complete! Saved to: ${calibrationPath}`); + } else { + console.log(`Calibration complete! Saved to: ${calibrationPath}`); + } + + return calibrationResults; + } finally { + // Note: Don't close the port - it belongs to the robot connection + } + })(); + + return { + stop(): void { + shouldStop = true; + }, + result: calibrationPromise, + }; +} diff --git a/packages/node/src/find_port.ts b/packages/node/src/find_port.ts new file mode 100644 index 0000000000000000000000000000000000000000..786928ce9f159ff8d568f3af44ba2eb5eb07d39d --- /dev/null +++ b/packages/node/src/find_port.ts @@ -0,0 +1,299 @@ +/** + * Node.js port discovery using serialport API + * Provides programmatic port discovery compatible with @lerobot/web API + */ + +import { SerialPort } from "serialport"; +import { platform } from "os"; +import { readdir } from "fs/promises"; +import { join } from "path"; +import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; +import type { + FindPortConfig, + FindPortProcess, + DiscoveredPort, + RobotConnection, +} from "./types/port-discovery.js"; + +/** + * Find available serial ports on the system + * Mirrors Python's find_available_ports() function + * Exported for CLI usage + */ +export async function findAvailablePorts(): Promise { + if (platform() === "win32") { + // List COM ports using serialport library (equivalent to pyserial) + const ports = await SerialPort.list(); + return ports.map((port) => port.path); + } else { + // List /dev/tty* ports for Unix-based systems (Linux/macOS) + try { + const devFiles = await readdir("/dev"); + const ttyPorts = devFiles + .filter((file) => file.startsWith("tty")) + .map((file) => join("/dev", file)); + return ttyPorts; + } catch (error) { + // Fallback to serialport library if /dev reading fails + const ports = await SerialPort.list(); + return ports.map((port) => port.path); + } + } +} + +/** + * Connect directly to a robot port (Python lerobot compatible) + * Equivalent to robot.connect() in Python lerobot + */ +export async function connectPort( + portPath: string, + robotType: "so100_follower" | "so100_leader" = "so100_follower", + robotId: string = "robot" +): Promise { + // Test connection + const port = new NodeSerialPortWrapper(portPath); + let isConnected = false; + + try { + await port.initialize(); + isConnected = true; + await port.close(); + } catch (error) { + // Connection failed + } + + // Return the ACTUAL working port, properly initialized! + const workingPort = new NodeSerialPortWrapper(portPath); + + // Initialize the working port if connection test succeeded + if (isConnected) { + try { + await workingPort.initialize(); + } catch (error) { + isConnected = false; + } + } + + return { + port: workingPort, // ← Return the initialized working port! + name: `Robot on ${portPath}`, + robotType, + robotId, + isConnected, + serialNumber: portPath, // Use port path as serial number for Node.js + error: isConnected ? undefined : "Connection failed", + }; +} + +/** + * Interactive mode: Return discovered robot ports (Node.js style) + * Unlike web version, this only discovers - user must call connectPort() separately + */ +async function findPortInteractive( + options: FindPortConfig +): Promise { + const { onMessage } = options; + + onMessage?.("🔍 Searching for available robot ports..."); + + // Get all available ports + const availablePorts = await findAvailablePorts(); + + if (availablePorts.length === 0) { + throw new Error("No serial ports found"); + } + + onMessage?.( + `Found ${availablePorts.length} port(s), first available: ${availablePorts[0]}` + ); + + // Return discovered ports (no connection attempt) + return availablePorts.map((path) => ({ + path, + robotType: "so100_follower" as const, // Default type, user can override + })); +} + +/** + * Auto-connect mode: Connect to robots by serial number/port path + * Returns all connection attempts (successful and failed) + */ +async function findPortAutoConnect( + robotConfigs: NonNullable, + options: FindPortConfig +): Promise { + const { onMessage } = options; + const results: RobotConnection[] = []; + + onMessage?.(`🔍 Auto-connecting to ${robotConfigs.length} robot(s)...`); + + for (const config of robotConfigs) { + try { + onMessage?.( + `Connecting to ${config.robotId} (${config.serialNumber})...` + ); + + // Use serialNumber as port path for Node.js + const connection = await connectPort(config.serialNumber); + + if (connection.isConnected) { + onMessage?.(`✅ Connected to ${config.robotId}`); + results.push({ + ...connection, + robotType: config.robotType, + robotId: config.robotId, + serialNumber: config.serialNumber, + }); + } else { + onMessage?.(`❌ Failed to connect to ${config.robotId}`); + results.push({ + ...connection, + robotType: config.robotType, + robotId: config.robotId, + serialNumber: config.serialNumber, + isConnected: false, + error: connection.error || "Connection failed", + }); + } + } catch (error) { + onMessage?.( + `❌ Error connecting to ${config.robotId}: ${ + error instanceof Error ? error.message : error + }` + ); + results.push({ + port: { + path: config.serialNumber, + write: async () => {}, + read: async () => null, + open: async () => {}, + close: async () => {}, + isOpen: false, + }, + name: `Failed: ${config.robotId}`, + isConnected: false, + robotType: config.robotType, + robotId: config.robotId, + serialNumber: config.serialNumber, + error: error instanceof Error ? error.message : "Unknown error", + }); + } + } + + const successCount = results.filter((r) => r.isConnected).length; + onMessage?.( + `🎯 Connected to ${successCount}/${robotConfigs.length} robot(s)` + ); + + return results; +} + +/** + * Main findPort function - Node.js discovery-only API + * + * Discovers available robot ports without connecting. + * User must call connectPort() separately to establish connections. + */ +export async function findPort( + config: FindPortConfig = {} +): Promise { + const { onMessage } = config; + let stopped = false; + + onMessage?.("🤖 Interactive port discovery started"); + + // Create result promise + const resultPromise = (async () => { + if (stopped) { + throw new Error("Port discovery was stopped"); + } + + return await findPortInteractive(config); + })(); + + // Return process object + return { + result: resultPromise, + stop: () => { + stopped = true; + onMessage?.("🛑 Port discovery stopped"); + }, + }; +} + +/** + * Interactive port detection for CLI usage only + * Matches Python lerobot's unplug/replug cable detection exactly + * This function should only be used by the CLI, not the library + */ +export async function detectPortInteractive( + onMessage?: (message: string) => void +): Promise { + const { createInterface } = await import("readline"); + + const rl = createInterface({ + input: process.stdin, + output: process.stdout, + }); + + function waitForInput(prompt: string): Promise { + return new Promise((resolve) => { + rl.question(prompt, (answer: string) => { + resolve(answer); + }); + }); + } + + try { + const message = "Finding all available ports for the MotorsBus."; + if (onMessage) onMessage(message); + else console.log(message); + + // Get initial port list + const portsBefore = await findAvailablePorts(); + + const disconnectPrompt = + "Remove the USB cable from your MotorsBus and press Enter when done."; + await waitForInput(disconnectPrompt); + + // Get port list after disconnect + const portsAfter = await findAvailablePorts(); + + // Find the difference + const portsDiff = portsBefore.filter((port) => !portsAfter.includes(port)); + + if (portsDiff.length === 1) { + const detectedPort = portsDiff[0]; + const successMessage = `Detected port: ${detectedPort}`; + if (onMessage) onMessage(successMessage); + else console.log(successMessage); + + const reconnectPrompt = + "Reconnect the USB cable to your MotorsBus and press Enter when done."; + await waitForInput(reconnectPrompt); + + // Verify the port is back + const portsReconnected = await findAvailablePorts(); + if (portsReconnected.includes(detectedPort)) { + const verifyMessage = `Verified port: ${detectedPort}`; + if (onMessage) onMessage(verifyMessage); + else console.log(verifyMessage); + return detectedPort; + } else { + throw new Error("Port not found after reconnection"); + } + } else if (portsDiff.length === 0) { + throw new Error( + "No port difference detected. Please check cable connection." + ); + } else { + throw new Error( + `Multiple ports detected: ${portsDiff.join( + ", " + )}. Please disconnect other devices.` + ); + } + } finally { + rl.close(); + } +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..63a39e4f8829d209489d164d81ae458f469a8234 --- /dev/null +++ b/packages/node/src/index.ts @@ -0,0 +1,64 @@ +/** + * @lerobot/node - Node.js-based robotics control using SerialPort API + * + * Control robotics hardware directly from Node.js applications, CLI tools, and desktop software. + */ + +// Core functions +export { calibrate } from "./calibrate.js"; +export { teleoperate } from "./teleoperate.js"; +export { findPort, connectPort } from "./find_port.js"; +export { releaseMotors } from "./release_motors.js"; + +// Types +export type { + RobotConnection, + RobotConfig, + SerialPort, + SerialPortInfo, + SerialOptions, +} from "./types/robot-connection.js"; + +export type { + FindPortConfig, + FindPortProcess, + DiscoveredPort, +} from "./types/port-discovery.js"; + +export type { + CalibrateConfig, + CalibrationResults, + LiveCalibrationData, + CalibrationProcess, +} from "./types/calibration.js"; + +export type { + MotorConfig, + TeleoperationState, + TeleoperationProcess, + TeleoperateConfig, + TeleoperatorConfig, + DirectTeleoperatorConfig, +} from "./types/teleoperation.js"; + +export type { + RobotHardwareConfig, + KeyboardControl, +} from "./types/robot-config.js"; + +// Utilities (advanced users) +export { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; +export { + readAllMotorPositions, + readMotorPosition, +} from "./utils/motor-communication.js"; +export { + createSO100Config, + SO100_KEYBOARD_CONTROLS, +} from "./robots/so100_config.js"; +export { KEYBOARD_TELEOPERATOR_DEFAULTS } from "./teleoperators/index.js"; +export { + getHfHome, + getHfLerobotHome, + getCalibrationDir, +} from "./utils/constants.js"; diff --git a/packages/node/src/release_motors.ts b/packages/node/src/release_motors.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c6c1451ff6d556b0c55a7450b928d4dde29fcc2 --- /dev/null +++ b/packages/node/src/release_motors.ts @@ -0,0 +1,70 @@ +/** + * User-facing motor release functionality for Node.js + * Simple API - pass in robotConnection, motors get released + * + * Handles robot configuration and port management internally + */ + +import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; +import { createSO100Config } from "./robots/so100_config.js"; +import { releaseMotors as releaseMotorsLowLevel } from "./utils/motor-communication.js"; +import type { RobotConnection } from "./types/robot-connection.js"; + +/** + * Release robot motors (allows free movement by hand) + * Perfect for calibration setup or manual positioning + * + * @param robotConnection - Connected robot with configured type + * @param motorIds - Optional specific motor IDs to release (defaults to all motors for robot type) + * @throws Error if robot type not configured or motorIds invalid + */ +export async function releaseMotors( + robotConnection: RobotConnection, + motorIds?: number[] +): Promise { + // Validate robot type is configured + if (!robotConnection.robotType) { + throw new Error( + "Robot type is required to release motors. Please configure the robot first." + ); + } + + // Validate robot connection + if (!robotConnection.isConnected || !robotConnection.port) { + throw new Error( + "Robot is not connected. Please use findPort() to connect first." + ); + } + + // Use the EXISTING port connection (don't create new one!) + const port = robotConnection.port; + + // Get robot-specific configuration + let robotConfig; + if (robotConnection.robotType.startsWith("so100")) { + robotConfig = createSO100Config(robotConnection.robotType); + } else { + throw new Error(`Unsupported robot type: ${robotConnection.robotType}`); + } + + // Determine which motors to release + const motorsToRelease = motorIds || robotConfig.motorIds; + + // Validate motorIds are valid for this robot type + if (motorIds) { + const invalidMotors = motorIds.filter( + (id) => !robotConfig.motorIds.includes(id) + ); + if (invalidMotors.length > 0) { + throw new Error( + `Invalid motor IDs [${invalidMotors.join(", ")}] for ${ + robotConnection.robotType + }. Valid IDs: [${robotConfig.motorIds.join(", ")}]` + ); + } + } + + // Release the motors using low-level function + await releaseMotorsLowLevel(port, motorsToRelease); + // Note: Don't close the port - it belongs to the robot connection +} diff --git a/packages/node/src/robots/so100_config.ts b/packages/node/src/robots/so100_config.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd7ea170c5b2059f3ab4ed7b412a161deea2b864 --- /dev/null +++ b/packages/node/src/robots/so100_config.ts @@ -0,0 +1,98 @@ +/** + * SO-100 specific hardware configuration + */ + +import type { RobotHardwareConfig } from "../types/robot-config.js"; + +/** + * STS3215 Protocol Configuration for SO-100 devices + */ +export const NODE_STS3215_PROTOCOL = { + resolution: 4096, // 12-bit resolution (0-4095) + homingOffsetAddress: 31, // Address for Homing_Offset register + homingOffsetLength: 2, // 2 bytes for Homing_Offset + presentPositionAddress: 56, // Address for Present_Position register + presentPositionLength: 2, // 2 bytes for Present_Position + minPositionLimitAddress: 9, // Address for Min_Position_Limit register + minPositionLimitLength: 2, // 2 bytes for Min_Position_Limit + maxPositionLimitAddress: 11, // Address for Max_Position_Limit register + maxPositionLimitLength: 2, // 2 bytes for Max_Position_Limit + signMagnitudeBit: 11, // Bit 11 is sign bit for Homing_Offset encoding +} as const; + +/** + * SO-100 Device Configuration + * Motor names, IDs, and drive modes for both follower and leader + */ +export const SO100_CONFIG = { + motorNames: [ + "shoulder_pan", + "shoulder_lift", + "elbow_flex", + "wrist_flex", + "wrist_roll", + "gripper", + ], + motorIds: [1, 2, 3, 4, 5, 6], + // All SO-100 motors use drive_mode=0 + driveModes: [0, 0, 0, 0, 0, 0], +}; + +/** + * SO-100 Keyboard Controls for Teleoperation + * Robot-specific mapping optimized for SO-100 joint layout + */ +export const SO100_KEYBOARD_CONTROLS = { + // Shoulder controls + ArrowUp: { motor: "shoulder_lift", direction: 1, description: "Shoulder up" }, + ArrowDown: { + motor: "shoulder_lift", + direction: -1, + description: "Shoulder down", + }, + ArrowLeft: { + motor: "shoulder_pan", + direction: -1, + description: "Shoulder left", + }, + ArrowRight: { + motor: "shoulder_pan", + direction: 1, + description: "Shoulder right", + }, + + // WASD controls + w: { motor: "elbow_flex", direction: 1, description: "Elbow flex" }, + s: { motor: "elbow_flex", direction: -1, description: "Elbow extend" }, + a: { motor: "wrist_flex", direction: -1, description: "Wrist down" }, + d: { motor: "wrist_flex", direction: 1, description: "Wrist up" }, + + // Wrist roll and gripper + q: { motor: "wrist_roll", direction: -1, description: "Wrist roll left" }, + e: { motor: "wrist_roll", direction: 1, description: "Wrist roll right" }, + o: { motor: "gripper", direction: 1, description: "Gripper open" }, + c: { motor: "gripper", direction: -1, description: "Gripper close" }, + + // Emergency stop + Escape: { + motor: "emergency_stop", + direction: 0, + description: "Emergency stop", + }, +} as const; + +/** + * Create SO-100 hardware configuration + */ +export function createSO100Config( + deviceType: "so100_follower" | "so100_leader" +): RobotHardwareConfig { + return { + deviceType, + motorNames: SO100_CONFIG.motorNames, + motorIds: SO100_CONFIG.motorIds, + driveModes: SO100_CONFIG.driveModes, + keyboardControls: SO100_KEYBOARD_CONTROLS, + protocol: NODE_STS3215_PROTOCOL, + }; +} \ No newline at end of file diff --git a/packages/node/src/teleoperate.ts b/packages/node/src/teleoperate.ts new file mode 100644 index 0000000000000000000000000000000000000000..98681dd0434c726921958cddee0aa7d49fb4204e --- /dev/null +++ b/packages/node/src/teleoperate.ts @@ -0,0 +1,223 @@ +/** + * Node.js teleoperation functionality using serialport API + * Provides both Python lerobot compatible CLI behavior and programmatic usage + * Uses proven teleoperator classes with web-compatible API + */ + +import { NodeSerialPortWrapper } from "./utils/serial-port-wrapper.js"; +import { createSO100Config } from "./robots/so100_config.js"; +import { readAllMotorPositions } from "./utils/motor-communication.js"; +import { + KeyboardTeleoperator, + DirectTeleoperator, +} from "./teleoperators/index.js"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import { homedir } from "os"; +import { existsSync } from "fs"; +import type { + TeleoperateConfig, + TeleoperationProcess, + MotorConfig, + TeleoperationState, +} from "./types/teleoperation.js"; +import type { RobotConnection } from "./types/robot-connection.js"; +import type { CalibrationResults } from "./types/calibration.js"; + +/** + * Get calibration file path (matches Python lerobot location) + */ +function getCalibrationFilePath(robotType: string, robotId: string): string { + const HF_HOME = + process.env.HF_HOME || join(homedir(), ".cache", "huggingface"); + const calibrationDir = join( + HF_HOME, + "lerobot", + "calibration", + "robots", + robotType + ); + return join(calibrationDir, `${robotId}.json`); +} + +/** + * Load calibration data from file system + */ +async function loadCalibrationData( + robotType: string, + robotId: string +): Promise { + const calibrationPath = getCalibrationFilePath(robotType, robotId); + + if (!existsSync(calibrationPath)) { + return null; + } + + try { + const calibrationJson = await readFile(calibrationPath, "utf-8"); + return JSON.parse(calibrationJson) as CalibrationResults; + } catch (error) { + console.warn( + `Failed to load calibration data from ${calibrationPath}:`, + error + ); + return null; + } +} + +/** + * Build motor configurations from robot config and calibration data + */ +function buildMotorConfigs( + robotConfig: any, + calibrationData?: CalibrationResults | null +): MotorConfig[] { + const motorConfigs: MotorConfig[] = []; + + for (let i = 0; i < robotConfig.motorNames.length; i++) { + const motorName = robotConfig.motorNames[i]; + const motorId = robotConfig.motorIds[i]; + + let minPosition = 0; + let maxPosition = robotConfig.protocol.resolution - 1; // Default full range + + // Use calibration data if available + if (calibrationData && calibrationData[motorName]) { + minPosition = calibrationData[motorName].range_min; + maxPosition = calibrationData[motorName].range_max; + } + + motorConfigs.push({ + id: motorId, + name: motorName, + currentPosition: Math.floor((minPosition + maxPosition) / 2), // Start at center + minPosition, + maxPosition, + }); + } + + return motorConfigs; +} + +/** + * Main teleoperate function with web-compatible API + */ +export async function teleoperate( + config: TeleoperateConfig +): Promise { + const { robot, teleop, calibrationData, onStateUpdate } = config; + + // Validate robot configuration + if (!robot.robotType) { + throw new Error( + "Robot type is required for teleoperation. Please configure the robot first." + ); + } + + if (!robot.isConnected || !robot.port) { + throw new Error( + "Robot is not connected. Please use findPort() to connect first." + ); + } + + // Use the EXISTING port connection (don't create new one!) + const port = robot.port; + + // Get robot-specific configuration + let robotConfig; + if (robot.robotType.startsWith("so100")) { + robotConfig = createSO100Config(robot.robotType); + } else { + throw new Error(`Unsupported robot type: ${robot.robotType}`); + } + + // Load or use provided calibration data + let effectiveCalibrationData = calibrationData; + if (!effectiveCalibrationData && robot.robotId) { + effectiveCalibrationData = await loadCalibrationData( + robot.robotType, + robot.robotId + ); + } + + if (!effectiveCalibrationData) { + console.warn( + "No calibration data found. Using default motor ranges. Consider running calibration first." + ); + } + + // Build motor configurations + const motorConfigs = buildMotorConfigs(robotConfig, effectiveCalibrationData); + + // Read current motor positions + try { + const currentPositions = await readAllMotorPositions( + port, + robotConfig.motorIds + ); + for (let i = 0; i < motorConfigs.length; i++) { + motorConfigs[i].currentPosition = currentPositions[i]; + } + } catch (error) { + console.warn("Failed to read initial motor positions:", error); + } + + // Create appropriate teleoperator based on configuration + let teleoperator; + switch (teleop.type) { + case "keyboard": + teleoperator = new KeyboardTeleoperator( + teleop, + port, + motorConfigs, + robotConfig.keyboardControls, + onStateUpdate + ); + break; + + case "direct": + teleoperator = new DirectTeleoperator(teleop, port, motorConfigs); + break; + + default: + throw new Error(`Unsupported teleoperator type: ${(teleop as any).type}`); + } + + // Initialize teleoperator + await teleoperator.initialize(); + + // Build process object + const process: TeleoperationProcess = { + start(): void { + teleoperator.start(); + }, + + stop(): void { + teleoperator.stop(); + }, + + updateKeyState(key: string, pressed: boolean): void { + if ("updateKeyState" in teleoperator) { + (teleoperator as any).updateKeyState(key, pressed); + } + }, + + getState(): TeleoperationState { + const teleoperatorSpecificState = teleoperator.getState(); + return { + isActive: teleoperator.isActiveTeleoperator, + motorConfigs: [...teleoperator.motorConfigs], + lastUpdate: Date.now(), + ...teleoperatorSpecificState, + }; + }, + + teleoperator: teleoperator, + + async disconnect(): Promise { + await teleoperator.disconnect(); + }, + }; + + return process; +} diff --git a/packages/node/src/teleoperators/base-teleoperator.ts b/packages/node/src/teleoperators/base-teleoperator.ts new file mode 100644 index 0000000000000000000000000000000000000000..26153966bbb168407d01f9313d721ecd60cbf6cf --- /dev/null +++ b/packages/node/src/teleoperators/base-teleoperator.ts @@ -0,0 +1,62 @@ +/** + * Base teleoperator interface and abstract class for Node.js platform + * Defines the contract that all teleoperators must implement + */ + +import type { MotorConfig } from "../types/teleoperation.js"; +import type { MotorCommunicationPort } from "../utils/motor-communication.js"; + +/** + * Base interface that all Node.js teleoperators must implement + */ +export interface NodeTeleoperator { + initialize(): Promise; + start(): void; + stop(): void; + disconnect(): Promise; + getState(): TeleoperatorSpecificState; + onMotorConfigsUpdate(motorConfigs: MotorConfig[]): void; + motorConfigs: MotorConfig[]; +} + +/** + * Teleoperator-specific state (union type for different teleoperator types) + */ +export type TeleoperatorSpecificState = { + keyStates?: { [key: string]: { pressed: boolean; timestamp: number } }; // keyboard + leaderPositions?: { [motor: string]: number }; // leader arm +}; + +/** + * Base abstract class with common functionality for all teleoperators + */ +export abstract class BaseNodeTeleoperator implements NodeTeleoperator { + protected port: MotorCommunicationPort; + public motorConfigs: MotorConfig[] = []; + protected isActive: boolean = false; + + constructor(port: MotorCommunicationPort, motorConfigs: MotorConfig[]) { + this.port = port; + this.motorConfigs = motorConfigs; + } + + abstract initialize(): Promise; + abstract start(): void; + abstract stop(): void; + abstract getState(): TeleoperatorSpecificState; + + async disconnect(): Promise { + this.stop(); + if (this.port && "close" in this.port) { + await (this.port as any).close(); + } + } + + onMotorConfigsUpdate(motorConfigs: MotorConfig[]): void { + this.motorConfigs = motorConfigs; + } + + get isActiveTeleoperator(): boolean { + return this.isActive; + } +} \ No newline at end of file diff --git a/packages/node/src/teleoperators/direct-teleoperator.ts b/packages/node/src/teleoperators/direct-teleoperator.ts new file mode 100644 index 0000000000000000000000000000000000000000..e09d9dcdf16c89dcd453b6b4c799ad49b25ab608 --- /dev/null +++ b/packages/node/src/teleoperators/direct-teleoperator.ts @@ -0,0 +1,112 @@ +/** + * Direct teleoperator for Node.js platform + * Provides programmatic control without user interface + */ + +import { + BaseNodeTeleoperator, + type TeleoperatorSpecificState, +} from "./base-teleoperator.js"; +import type { + DirectTeleoperatorConfig, + MotorConfig, +} from "../types/teleoperation.js"; +import type { MotorCommunicationPort } from "../utils/motor-communication.js"; +import { + readMotorPosition, + writeMotorPosition, +} from "../utils/motor-communication.js"; + +/** + * Direct teleoperator provides programmatic motor control + * Use this when you want to control motors directly from code + */ +export class DirectTeleoperator extends BaseNodeTeleoperator { + constructor( + config: DirectTeleoperatorConfig, + port: MotorCommunicationPort, + motorConfigs: MotorConfig[] + ) { + super(port, motorConfigs); + } + + async initialize(): Promise { + // Read current motor positions + for (const config of this.motorConfigs) { + const position = await readMotorPosition(this.port, config.id); + if (position !== null) { + config.currentPosition = position; + } + } + } + + start(): void { + this.isActive = true; + } + + stop(): void { + this.isActive = false; + } + + getState(): TeleoperatorSpecificState { + return {}; + } + + /** + * Move motor to exact position (programmatic control) + */ + async moveMotor(motorName: string, targetPosition: number): Promise { + if (!this.isActive) return false; + + const motorConfig = this.motorConfigs.find((m) => m.name === motorName); + if (!motorConfig) return false; + + const clampedPosition = Math.max( + motorConfig.minPosition, + Math.min(motorConfig.maxPosition, targetPosition) + ); + + try { + await writeMotorPosition( + this.port, + motorConfig.id, + Math.round(clampedPosition) + ); + motorConfig.currentPosition = clampedPosition; + return true; + } catch (error) { + console.warn(`Failed to move motor ${motorName}:`, error); + return false; + } + } + + /** + * Move multiple motors simultaneously + */ + async moveMotors( + positions: { [motorName: string]: number } + ): Promise<{ [motorName: string]: boolean }> { + const results: { [motorName: string]: boolean } = {}; + + const promises = Object.entries(positions).map( + async ([motorName, position]) => { + const success = await this.moveMotor(motorName, position); + results[motorName] = success; + } + ); + + await Promise.all(promises); + return results; + } + + /** + * Get current motor positions + */ + getCurrentPositions(): { [motorName: string]: number } { + const positions: { [motorName: string]: number } = {}; + for (const config of this.motorConfigs) { + positions[config.name] = config.currentPosition; + } + return positions; + } +} \ No newline at end of file diff --git a/packages/node/src/teleoperators/index.ts b/packages/node/src/teleoperators/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1954e88221b87c1eaba0870b6196519b9284e6ba --- /dev/null +++ b/packages/node/src/teleoperators/index.ts @@ -0,0 +1,14 @@ +/** + * Teleoperators barrel exports for Node.js + */ + +export { + BaseNodeTeleoperator, + type NodeTeleoperator, + type TeleoperatorSpecificState, +} from "./base-teleoperator.js"; +export { + KeyboardTeleoperator, + KEYBOARD_TELEOPERATOR_DEFAULTS, +} from "./keyboard-teleoperator.js"; +export { DirectTeleoperator } from "./direct-teleoperator.js"; \ No newline at end of file diff --git a/packages/node/src/teleoperators/keyboard-teleoperator.ts b/packages/node/src/teleoperators/keyboard-teleoperator.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ddaa8e4e7ca479427031ea14dcb0e3727cc2508 --- /dev/null +++ b/packages/node/src/teleoperators/keyboard-teleoperator.ts @@ -0,0 +1,292 @@ +/** + * Keyboard teleoperator for Node.js platform using stdin + */ + +import { + BaseNodeTeleoperator, + type TeleoperatorSpecificState, +} from "./base-teleoperator.js"; +import type { KeyboardControl } from "../types/robot-config.js"; +import type { + KeyboardTeleoperatorConfig, + MotorConfig, + TeleoperationState, +} from "../types/teleoperation.js"; +import type { MotorCommunicationPort } from "../utils/motor-communication.js"; +import { + readMotorPosition, + writeMotorPosition, +} from "../utils/motor-communication.js"; + +/** + * Default configuration values for keyboard teleoperator + */ +export const KEYBOARD_TELEOPERATOR_DEFAULTS = { + stepSize: 8, // Keep browser demo step size + updateRate: 120, // Higher frequency for smoother movement (120 Hz) + keyTimeout: 150, // Shorter for better single taps, accept some gap on hold +} as const; + +export class KeyboardTeleoperator extends BaseNodeTeleoperator { + private keyboardControls: { [key: string]: KeyboardControl } = {}; + private updateInterval: NodeJS.Timeout | null = null; + private keyStates: { + [key: string]: { pressed: boolean; timestamp: number }; + } = {}; + private onStateUpdate?: (state: TeleoperationState) => void; + + // Configuration values + private readonly stepSize: number; + private readonly updateRate: number; + private readonly keyTimeout: number; + + constructor( + config: KeyboardTeleoperatorConfig, + port: MotorCommunicationPort, + motorConfigs: MotorConfig[], + keyboardControls: { [key: string]: KeyboardControl }, + onStateUpdate?: (state: TeleoperationState) => void + ) { + super(port, motorConfigs); + this.keyboardControls = keyboardControls; + this.onStateUpdate = onStateUpdate; + + // Set configuration values + this.stepSize = config.stepSize ?? KEYBOARD_TELEOPERATOR_DEFAULTS.stepSize; + this.updateRate = + config.updateRate ?? KEYBOARD_TELEOPERATOR_DEFAULTS.updateRate; + this.keyTimeout = + config.keyTimeout ?? KEYBOARD_TELEOPERATOR_DEFAULTS.keyTimeout; + } + + async initialize(): Promise { + // Set up stdin for raw keyboard input + if (process.stdin.setRawMode) { + process.stdin.setRawMode(true); + } + process.stdin.resume(); + process.stdin.setEncoding("utf8"); + + // Set up keyboard input handler + process.stdin.on("data", this.handleKeyboardInput.bind(this)); + + // Read current motor positions + for (const config of this.motorConfigs) { + const position = await readMotorPosition(this.port, config.id); + if (position !== null) { + config.currentPosition = position; + } + } + } + + start(): void { + if (this.isActive) return; + + this.isActive = true; + this.updateInterval = setInterval(() => { + this.updateMotorPositions(); + }, 1000 / this.updateRate); + + // Display keyboard controls + this.displayControls(); + } + + stop(): void { + if (!this.isActive) return; + + this.isActive = false; + + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + + // Clear all key states + this.keyStates = {}; + + // Notify of state change + if (this.onStateUpdate) { + this.onStateUpdate(this.buildTeleoperationState()); + } + } + + getState(): TeleoperatorSpecificState { + return { + keyStates: { ...this.keyStates }, + }; + } + + updateKeyState(key: string, pressed: boolean): void { + this.keyStates[key] = { + pressed, + timestamp: Date.now(), + }; + } + + private handleKeyboardInput(key: string): void { + if (!this.isActive) return; + + // Handle special keys + if (key === "\u0003") { + // Ctrl+C + process.exit(0); + } + + if (key === "\u001b") { + // Escape + this.stop(); + return; + } + + // Handle regular keys - START IMMEDIATE CONTINUOUS MOVEMENT + const keyName = this.mapKeyToName(key); + if (keyName && this.keyboardControls[keyName]) { + // If key is already active, just refresh timestamp + if (this.keyStates[keyName]) { + this.keyStates[keyName].timestamp = Date.now(); + } else { + // New key press - start continuous movement immediately + this.updateKeyState(keyName, true); + + // Move immediately on first press (don't wait for interval) + this.moveMotorForKey(keyName); + } + } + } + + private moveMotorForKey(keyName: string): void { + const control = this.keyboardControls[keyName]; + if (!control || control.motor === "emergency_stop") return; + + const motorConfig = this.motorConfigs.find((m) => m.name === control.motor); + if (!motorConfig) return; + + // Calculate new position + const newPosition = + motorConfig.currentPosition + control.direction * this.stepSize; + + // Apply limits + const clampedPosition = Math.max( + motorConfig.minPosition, + Math.min(motorConfig.maxPosition, newPosition) + ); + + // Send motor command immediately + writeMotorPosition(this.port, motorConfig.id, Math.round(clampedPosition)) + .then(() => { + motorConfig.currentPosition = clampedPosition; + }) + .catch((error) => { + console.warn(`Failed to move motor ${motorConfig.id}:`, error); + }); + } + + private updateMotorPositions(): void { + const now = Date.now(); + + // Clear timed-out keys + Object.keys(this.keyStates).forEach((key) => { + if (now - this.keyStates[key].timestamp > this.keyTimeout) { + delete this.keyStates[key]; + } + }); + + // Process active keys + const activeKeys = Object.keys(this.keyStates).filter( + (key) => + this.keyStates[key].pressed && + now - this.keyStates[key].timestamp <= this.keyTimeout + ); + + // Emergency stop check + if (activeKeys.includes("Escape")) { + this.stop(); + return; + } + + // Calculate target positions based on active keys + const targetPositions: { [motorName: string]: number } = {}; + + for (const key of activeKeys) { + const control = this.keyboardControls[key]; + if (!control || control.motor === "emergency_stop") continue; + + const motorConfig = this.motorConfigs.find( + (m) => m.name === control.motor + ); + if (!motorConfig) continue; + + // Calculate new position + const currentTarget = + targetPositions[motorConfig.name] ?? motorConfig.currentPosition; + const newPosition = currentTarget + control.direction * this.stepSize; + + // Apply limits + targetPositions[motorConfig.name] = Math.max( + motorConfig.minPosition, + Math.min(motorConfig.maxPosition, newPosition) + ); + } + + // Send motor commands and update positions + Object.entries(targetPositions).forEach(([motorName, targetPosition]) => { + const motorConfig = this.motorConfigs.find((m) => m.name === motorName); + if (motorConfig && targetPosition !== motorConfig.currentPosition) { + writeMotorPosition( + this.port, + motorConfig.id, + Math.round(targetPosition) + ) + .then(() => { + motorConfig.currentPosition = targetPosition; + }) + .catch((error) => { + console.warn( + `Failed to write motor ${motorConfig.id} position:`, + error + ); + }); + } + }); + } + + private mapKeyToName(key: string): string | null { + // Map stdin input to key names + const keyMap: { [key: string]: string } = { + "\u001b[A": "ArrowUp", + "\u001b[B": "ArrowDown", + "\u001b[C": "ArrowRight", + "\u001b[D": "ArrowLeft", + w: "w", + s: "s", + a: "a", + d: "d", + q: "q", + e: "e", + o: "o", + c: "c", + }; + + return keyMap[key] || null; + } + + private displayControls(): void { + console.log("\n=== Robot Teleoperation Controls ==="); + console.log("Arrow Keys: Shoulder pan/lift"); + console.log("WASD: Elbow flex / Wrist flex"); + console.log("Q/E: Wrist roll"); + console.log("O/C: Gripper open/close"); + console.log("ESC: Emergency stop"); + console.log("Ctrl+C: Exit"); + console.log("=====================================\n"); + } + + private buildTeleoperationState(): TeleoperationState { + return { + isActive: this.isActive, + motorConfigs: [...this.motorConfigs], + lastUpdate: Date.now(), + keyStates: { ...this.keyStates }, + }; + } +} diff --git a/packages/node/src/types/calibration.ts b/packages/node/src/types/calibration.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc8c6252b9138855002e01be0ea6b8995dfc7c00 --- /dev/null +++ b/packages/node/src/types/calibration.ts @@ -0,0 +1,48 @@ +/** + * Calibration-related types for Node.js implementation + */ + +import type { RobotConnection } from "./robot-connection.js"; + +/** + * Live calibration data with current positions and ranges + */ +export interface LiveCalibrationData { + [motorName: string]: { + current: number; + min: number; + max: number; + range: number; + }; +} + +/** + * Config for calibrate function + */ +export interface CalibrateConfig { + robot: RobotConnection; + onLiveUpdate?: (data: LiveCalibrationData) => void; + onProgress?: (message: string) => void; + outputPath?: string; // Node.js specific: custom output path for calibration file +} + +/** + * Calibration results structure - Python lerobot compatible format + */ +export interface CalibrationResults { + [motorName: string]: { + id: number; + drive_mode: number; + homing_offset: number; + range_min: number; + range_max: number; + }; +} + +/** + * Calibration process control object + */ +export interface CalibrationProcess { + stop(): void; + result: Promise; +} diff --git a/packages/node/src/types/port-discovery.ts b/packages/node/src/types/port-discovery.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9ad9e8a4cb0993021b6a12afbe466a9307283fc --- /dev/null +++ b/packages/node/src/types/port-discovery.ts @@ -0,0 +1,47 @@ +/** + * Port discovery types for Node.js implementation using serialport + */ + +// Import types needed in this file +import type { + RobotConnection, + RobotConfig, + SerialPort, + SerialPortInfo, +} from "./robot-connection.js"; + +/** + * Config for findPort function + */ +export interface FindPortConfig { + // Interactive mode: shows Python lerobot compatible prompts + interactive?: boolean; + + // Auto-connect mode: provide robot configs to connect to + robotConfigs?: RobotConfig[]; + + // Callbacks + onMessage?: (message: string) => void; +} + +/** + * Discovered port information (Node.js discovery-only mode) + */ +export interface DiscoveredPort { + path: string; // Serial port path (e.g., "/dev/ttyUSB0", "COM4") + robotType: "so100_follower" | "so100_leader"; +} + +/** + * Process object returned by findPort + */ +export interface FindPortProcess { + // Result promise - Node.js returns discovered ports, user calls connectPort() separately + result: Promise; + + // Control + stop: () => void; +} + +// Re-export commonly used types for convenience +export type { RobotConnection, RobotConfig, SerialPort, SerialPortInfo }; diff --git a/packages/node/src/types/robot-config.ts b/packages/node/src/types/robot-config.ts new file mode 100644 index 0000000000000000000000000000000000000000..5b2016a82254abdbf94c8d534ac352d0d649bd8e --- /dev/null +++ b/packages/node/src/types/robot-config.ts @@ -0,0 +1,40 @@ +/** + * Shared robot hardware configuration types + * Used across calibration, teleoperation, and other robot operations + */ + +/** + * Keyboard control mapping for teleoperation + */ +export interface KeyboardControl { + motor: string; + direction: number; + description: string; +} + +/** + * Robot hardware configuration interface + * Defines the contract that all robot configurations must implement + */ +export interface RobotHardwareConfig { + deviceType: string; + motorNames: string[]; + motorIds: number[]; + driveModes: number[]; + + // Keyboard controls for teleoperation (robot-specific) + keyboardControls: { [key: string]: KeyboardControl }; + + protocol: { + resolution: number; + homingOffsetAddress: number; + homingOffsetLength: number; + presentPositionAddress: number; + presentPositionLength: number; + minPositionLimitAddress: number; + minPositionLimitLength: number; + maxPositionLimitAddress: number; + maxPositionLimitLength: number; + signMagnitudeBit: number; + }; +} \ No newline at end of file diff --git a/packages/node/src/types/robot-connection.ts b/packages/node/src/types/robot-connection.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9d5806432357bb18ab0257d5097e3849a5522ce --- /dev/null +++ b/packages/node/src/types/robot-connection.ts @@ -0,0 +1,61 @@ +/** + * Core robot connection types used across the lerobot.js Node.js library + * These types are shared between findPort, calibrate, teleoperate, and other modules + */ + +import type { RobotHardwareConfig } from "./robot-config.js"; + +/** + * Type definitions for Node.js serialport API + */ +export interface SerialPort { + path: string; + write(buffer: Buffer): Promise; + read(): Promise; + open(): Promise; + close(): Promise; + isOpen: boolean; +} + +export interface SerialPortInfo { + path: string; + manufacturer?: string; + serialNumber?: string; + pnpId?: string; + locationId?: string; + productId?: string; + vendorId?: string; +} + +export interface SerialOptions { + baudRate: number; + dataBits?: number; + stopBits?: number; + parity?: "none" | "even" | "odd"; +} + +/** + * Unified robot connection interface used across all functions + * This same object works for findPort, calibrate, teleoperate, etc. + * Includes all fields needed by CLI and other applications + */ +export interface RobotConnection { + port: SerialPort; + name: string; // Display name for CLI + isConnected: boolean; // Connection status + robotType?: "so100_follower" | "so100_leader"; // Optional until user configures + robotId?: string; // Optional until user configures + serialNumber: string; // Always required for identification + error?: string; // Error message if connection failed + config?: RobotHardwareConfig; // Robot configuration (motorIds, controls, etc.) - set when robotType is configured + portInfo?: SerialPortInfo; // Node.js serial port information +} + +/** + * Minimal robot config for finding/connecting to specific robots + */ +export interface RobotConfig { + robotType: "so100_follower" | "so100_leader"; + robotId: string; + serialNumber: string; +} \ No newline at end of file diff --git a/packages/node/src/types/teleoperation.ts b/packages/node/src/types/teleoperation.ts new file mode 100644 index 0000000000000000000000000000000000000000..a478dad0cb8448ecf82d1b1cae65705ee1251b5a --- /dev/null +++ b/packages/node/src/types/teleoperation.ts @@ -0,0 +1,95 @@ +/** + * Teleoperation-related types for Node.js implementation + */ + +import type { RobotConnection } from "./robot-connection.js"; +import type { NodeTeleoperator } from "../teleoperators/index.js"; + +/** + * Motor position and limits for teleoperation + */ +export interface MotorConfig { + id: number; + name: string; + currentPosition: number; + minPosition: number; + maxPosition: number; +} + +/** + * Teleoperation state + */ +export interface TeleoperationState { + isActive: boolean; + motorConfigs: MotorConfig[]; + lastUpdate: number; + + // Teleoperator-specific state (optional fields for different types) + keyStates?: { [key: string]: { pressed: boolean; timestamp: number } }; // keyboard + leaderPositions?: { [motor: string]: number }; // leader arm +} + +/** + * Teleoperation process control object + */ +export interface TeleoperationProcess { + start(): void; + stop(): void; + updateKeyState(key: string, pressed: boolean): void; + getState(): TeleoperationState; + teleoperator: NodeTeleoperator; + disconnect(): Promise; +} + +/** + * Base interface for all teleoperator configurations + */ +export interface BaseTeleoperatorConfig { + type: string; +} + +/** + * Keyboard teleoperator configuration + */ +export interface KeyboardTeleoperatorConfig extends BaseTeleoperatorConfig { + type: "keyboard"; + stepSize?: number; // Default: KEYBOARD_TELEOPERATOR_DEFAULTS.stepSize + updateRate?: number; // Default: KEYBOARD_TELEOPERATOR_DEFAULTS.updateRate + keyTimeout?: number; // Default: KEYBOARD_TELEOPERATOR_DEFAULTS.keyTimeout +} + +/** + * Leader arm teleoperator configuration (future) + */ +export interface LeaderArmTeleoperatorConfig extends BaseTeleoperatorConfig { + type: "so100_leader"; + port: string; + calibrationData?: any; + positionSmoothing?: boolean; + scaleFactor?: number; +} + +/** + * Direct teleoperator configuration + */ +export interface DirectTeleoperatorConfig extends BaseTeleoperatorConfig { + type: "direct"; +} + +/** + * Union type for all teleoperator configurations + */ +export type TeleoperatorConfig = + | KeyboardTeleoperatorConfig + | LeaderArmTeleoperatorConfig + | DirectTeleoperatorConfig; + +/** + * Main teleoperation configuration + */ +export interface TeleoperateConfig { + robot: RobotConnection; + teleop: TeleoperatorConfig; + calibrationData?: { [motorName: string]: any }; + onStateUpdate?: (state: TeleoperationState) => void; +} \ No newline at end of file diff --git a/src/lerobot/node/utils/constants.ts b/packages/node/src/utils/constants.ts similarity index 83% rename from src/lerobot/node/utils/constants.ts rename to packages/node/src/utils/constants.ts index bd7599ca4f6fbf2cdeafe37f9f14acdbcb09a9c5..77a1893fb9a5332ad59cf87d8cadc47b20d5497e 100644 --- a/src/lerobot/node/utils/constants.ts +++ b/packages/node/src/utils/constants.ts @@ -1,15 +1,11 @@ /** - * Constants for lerobot.js - * Mirrors Python lerobot/common/constants.py + * Constants for @lerobot/node + * Mirrors Python lerobot/common/constants.py for directory structure compatibility */ import { homedir } from "os"; import { join } from "path"; -// Device types -export const ROBOTS = "robots"; -export const TELEOPERATORS = "teleoperators"; - /** * Get HF Home directory * Equivalent to Python's huggingface_hub.constants.HF_HOME diff --git a/packages/node/src/utils/motor-calibration.ts b/packages/node/src/utils/motor-calibration.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1d8ff3b112cef2a18f26fafb01da679953f8f45 --- /dev/null +++ b/packages/node/src/utils/motor-calibration.ts @@ -0,0 +1,188 @@ +/** + * Motor Calibration Utilities + * Specialized functions for motor calibration procedures + */ + +import { STS3215_PROTOCOL } from "./sts3215-protocol.js"; +import { encodeSignMagnitude } from "./sign-magnitude.js"; +import { + readAllMotorPositions, + writeMotorRegister, + type MotorCommunicationPort, +} from "./motor-communication.js"; + +/** + * Reset homing offsets to 0 for all motors + */ +export async function resetHomingOffsets( + port: MotorCommunicationPort, + motorIds: number[] +): Promise { + for (let i = 0; i < motorIds.length; i++) { + const motorId = motorIds[i]; + + try { + const packet = new Uint8Array([ + 0xff, + 0xff, + motorId, + 0x05, + 0x03, + STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS, + 0x00, // Low byte of 0 + 0x00, // High byte of 0 + 0x00, // Checksum + ]); + + const checksum = + ~( + motorId + + 0x05 + + 0x03 + + STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS + + 0x00 + + 0x00 + ) & 0xff; + packet[8] = checksum; + + await port.write(packet); + + try { + await port.read(200); + } catch (error) { + // Silent - response not required + } + } catch (error) { + throw new Error(`Failed to reset homing offset for motor ${motorId}`); + } + } +} + +/** + * Write homing offsets to motor registers immediately + */ +export async function writeHomingOffsetsToMotors( + port: MotorCommunicationPort, + motorIds: number[], + motorNames: string[], + homingOffsets: { [motor: string]: number } +): Promise { + for (let i = 0; i < motorIds.length; i++) { + const motorId = motorIds[i]; + const motorName = motorNames[i]; + const homingOffset = homingOffsets[motorName]; + + try { + const encodedOffset = encodeSignMagnitude( + homingOffset, + STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT + ); + + const packet = new Uint8Array([ + 0xff, + 0xff, + motorId, + 0x05, + 0x03, + STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS, + encodedOffset & 0xff, + (encodedOffset >> 8) & 0xff, + 0x00, + ]); + + const checksum = + ~( + motorId + + 0x05 + + 0x03 + + STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS + + (encodedOffset & 0xff) + + ((encodedOffset >> 8) & 0xff) + ) & 0xff; + packet[8] = checksum; + + await port.write(packet); + + try { + await port.read(200); + } catch (error) { + // Silent - response not required + } + } catch (error) { + throw new Error(`Failed to write homing offset for ${motorName}`); + } + } +} + +/** + * Set homing offsets with immediate writing + */ +export async function setHomingOffsets( + port: MotorCommunicationPort, + motorIds: number[], + motorNames: string[] +): Promise<{ [motor: string]: number }> { + // Reset existing homing offsets to 0 first + await resetHomingOffsets(port, motorIds); + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second instead of 100ms + + // Flush any cached position readings first + await readAllMotorPositions(port, motorIds); // Dummy read to flush cache + await new Promise((resolve) => setTimeout(resolve, 200)); // Small additional wait + + // Read positions (which should now be true physical positions) + const currentPositions = await readAllMotorPositions(port, motorIds); + + const homingOffsets: { [motor: string]: number } = {}; + const halfTurn = Math.floor((STS3215_PROTOCOL.RESOLUTION - 1) / 2); + + for (let i = 0; i < motorNames.length; i++) { + const motorName = motorNames[i]; + const position = currentPositions[i]; + const calculatedOffset = position - halfTurn; + homingOffsets[motorName] = calculatedOffset; + } + + // Write homing offsets to motors immediately + await writeHomingOffsetsToMotors(port, motorIds, motorNames, homingOffsets); + + return homingOffsets; +} + +/** + * Write hardware position limits to motors + */ +export async function writeHardwarePositionLimits( + port: MotorCommunicationPort, + motorIds: number[], + motorNames: string[], + rangeMins: { [motor: string]: number }, + rangeMaxes: { [motor: string]: number } +): Promise { + for (let i = 0; i < motorIds.length; i++) { + const motorId = motorIds[i]; + const motorName = motorNames[i]; + const minLimit = rangeMins[motorName]; + const maxLimit = rangeMaxes[motorName]; + + try { + // Write Min_Position_Limit register + await writeMotorRegister( + port, + motorId, + STS3215_PROTOCOL.MIN_POSITION_LIMIT_ADDRESS, + minLimit + ); + + // Write Max_Position_Limit register + await writeMotorRegister( + port, + motorId, + STS3215_PROTOCOL.MAX_POSITION_LIMIT_ADDRESS, + maxLimit + ); + } catch (error) { + throw new Error(`Failed to write position limits for ${motorName}`); + } + } +} diff --git a/packages/node/src/utils/motor-communication.ts b/packages/node/src/utils/motor-communication.ts new file mode 100644 index 0000000000000000000000000000000000000000..690584d578c0f928e40a9a70e4b7a1bca38545d9 --- /dev/null +++ b/packages/node/src/utils/motor-communication.ts @@ -0,0 +1,298 @@ +/** + * Motor Communication Utilities + * STS3215 motor reading and writing operations for Node.js + */ + +import { STS3215_PROTOCOL } from "./sts3215-protocol.js"; +import type { NodeSerialPortWrapper } from "./serial-port-wrapper.js"; + +/** + * Interface for motor communication port + * Compatible with NodeSerialPortWrapper + */ +export interface MotorCommunicationPort { + write(data: Uint8Array): Promise; + read(timeout?: number): Promise; +} + +/** + * Read single motor position using OLD WORKING approach + */ +export async function readMotorPosition( + port: MotorCommunicationPort, + motorId: number +): Promise { + try { + // Create Read Position packet (exactly like old working version) + const packet = new Uint8Array([ + 0xff, + 0xff, // Header + motorId, // Servo ID + 0x04, // Length + 0x02, // Instruction: READ_DATA + STS3215_PROTOCOL.PRESENT_POSITION_ADDRESS, // Present_Position register address + 0x02, // Data length (2 bytes) + 0x00, // Checksum placeholder + ]); + + const checksum = + ~( + motorId + + 0x04 + + 0x02 + + STS3215_PROTOCOL.PRESENT_POSITION_ADDRESS + + 0x02 + ) & 0xff; + packet[7] = checksum; + + // Retry communication using OLD WORKING approach + for ( + let attempts = 1; + attempts <= STS3215_PROTOCOL.MAX_RETRIES; + attempts++ + ) { + try { + // Write packet + await port.write(packet); + + // Use OLD WORKING approach: direct port.once('data') event with proper cleanup + const timeout = 100 * attempts; // Progressive timeout like old approach + const response = await new Promise((resolve, reject) => { + // Access underlying SerialPort for event-based reading + const underlyingPort = + (port as any).underlyingPort || (port as any).port; + + if (!underlyingPort || !underlyingPort.once) { + reject(new Error("Cannot access underlying port for old approach")); + return; + } + + let isResolved = false; + + const dataHandler = (data: Buffer) => { + if (!isResolved) { + isResolved = true; + clearTimeout(timer); + resolve(new Uint8Array(data)); + } + }; + + const timer = setTimeout(() => { + if (!isResolved) { + isResolved = true; + // CRITICAL: Remove the event listener to prevent memory leak + underlyingPort.removeListener("data", dataHandler); + reject(new Error("Read timeout")); + } + }, timeout); + + // Attach event listener + underlyingPort.once("data", dataHandler); + }); + + if (response.length >= 7) { + const id = response[2]; + const error = response[4]; + + if (id === motorId && error === 0) { + const position = response[5] | (response[6] << 8); + return position; + } + } + } catch (readError) { + if (attempts < STS3215_PROTOCOL.MAX_RETRIES) { + // Wait between retry attempts (like old approach) + const retryDelay = STS3215_PROTOCOL.RETRY_DELAY * attempts; + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + } + } + } + + // If all attempts failed, return null + return null; + } catch (error) { + return null; + } +} + +/** + * Read all motor positions + */ +export async function readAllMotorPositions( + port: MotorCommunicationPort, + motorIds: number[] +): Promise { + const motorPositions: number[] = []; + + for (let i = 0; i < motorIds.length; i++) { + const motorId = motorIds[i]; + + const position = await readMotorPosition(port, motorId); + + if (position !== null) { + motorPositions.push(position); + } else { + // Use fallback value for failed reads + const fallback = Math.floor((STS3215_PROTOCOL.RESOLUTION - 1) / 2); + motorPositions.push(fallback); + } + + // Delay between motor reads + await new Promise((resolve) => + setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY) + ); + } + + return motorPositions; +} + +/** + * Write motor goal position + */ +export async function writeMotorPosition( + port: MotorCommunicationPort, + motorId: number, + position: number +): Promise { + // STS3215 Write Goal_Position packet + const packet = new Uint8Array([ + 0xff, + 0xff, // Header + motorId, // Servo ID + 0x05, // Length + 0x03, // Instruction: WRITE_DATA + STS3215_PROTOCOL.GOAL_POSITION_ADDRESS, // Goal_Position register address + position & 0xff, // Position low byte + (position >> 8) & 0xff, // Position high byte + 0x00, // Checksum placeholder + ]); + + // Calculate checksum + const checksum = + ~( + motorId + + 0x05 + + 0x03 + + STS3215_PROTOCOL.GOAL_POSITION_ADDRESS + + (position & 0xff) + + ((position >> 8) & 0xff) + ) & 0xff; + packet[8] = checksum; + + await port.write(packet); +} + +/** + * Generic function to write a 2-byte value to a motor register + */ +export async function writeMotorRegister( + port: MotorCommunicationPort, + motorId: number, + registerAddress: number, + value: number +): Promise { + // Create Write Register packet + const packet = new Uint8Array([ + 0xff, + 0xff, // Header + motorId, // Servo ID + 0x05, // Length + 0x03, // Instruction: WRITE_DATA + registerAddress, // Register address + value & 0xff, // Data_L (low byte) + (value >> 8) & 0xff, // Data_H (high byte) + 0x00, // Checksum placeholder + ]); + + // Calculate checksum + const checksum = + ~( + motorId + + 0x05 + + 0x03 + + registerAddress + + (value & 0xff) + + ((value >> 8) & 0xff) + ) & 0xff; + packet[8] = checksum; + + // Write register value + await port.write(packet); + + // Wait for response (silent unless error) + try { + await port.read(200); + } catch (error) { + // Silent - response not required for successful operation + } +} + +/** + * Lock a motor (motor will hold its position and resist movement) + */ +export async function lockMotor( + port: MotorCommunicationPort, + motorId: number +): Promise { + await writeMotorRegister( + port, + motorId, + STS3215_PROTOCOL.TORQUE_ENABLE_ADDRESS, + 1 + ); + // Small delay for command processing + await new Promise((resolve) => + setTimeout(resolve, STS3215_PROTOCOL.WRITE_TO_READ_DELAY) + ); +} + +/** + * Release a motor (motor can be moved freely by hand) + */ +export async function releaseMotor( + port: MotorCommunicationPort, + motorId: number +): Promise { + await writeMotorRegister( + port, + motorId, + STS3215_PROTOCOL.TORQUE_ENABLE_ADDRESS, + 0 + ); + // Small delay for command processing + await new Promise((resolve) => + setTimeout(resolve, STS3215_PROTOCOL.WRITE_TO_READ_DELAY) + ); +} + +/** + * Lock motors (motors will hold their positions - perfect after calibration) + */ +export async function lockMotors( + port: MotorCommunicationPort, + motorIds: number[] +): Promise { + for (const motorId of motorIds) { + await lockMotor(port, motorId); + // Small delay between motors + await new Promise((resolve) => + setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY) + ); + } +} + +/** + * Release motors (motors can be moved freely - perfect for calibration) + */ +export async function releaseMotors( + port: MotorCommunicationPort, + motorIds: number[] +): Promise { + for (const motorId of motorIds) { + await releaseMotor(port, motorId); + // Small delay between motors + await new Promise((resolve) => + setTimeout(resolve, STS3215_PROTOCOL.INTER_MOTOR_DELAY) + ); + } +} diff --git a/packages/node/src/utils/package-structure.test.ts b/packages/node/src/utils/package-structure.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a9edca3b7012226b147fa5c09e1a84ab8762949b --- /dev/null +++ b/packages/node/src/utils/package-structure.test.ts @@ -0,0 +1,51 @@ +/** + * Node.js package structure tests + * Basic tests to ensure imports and core functionality work + */ + +import { describe, it, expect } from "vitest"; +import { STS3215_PROTOCOL } from "./sts3215-protocol.js"; +import { encodeSignMagnitude, decodeSignMagnitude } from "./sign-magnitude.js"; +import { + createSO100Config, + SO100_KEYBOARD_CONTROLS, +} from "../robots/so100_config.js"; + +describe("Node.js Package Structure", () => { + it("should export STS3215 protocol constants", () => { + expect(STS3215_PROTOCOL.RESOLUTION).toBe(4096); + expect(STS3215_PROTOCOL.PRESENT_POSITION_ADDRESS).toBe(56); + expect(STS3215_PROTOCOL.HOMING_OFFSET_ADDRESS).toBe(31); + }); + + it("should handle sign-magnitude encoding/decoding", () => { + // Test positive values + expect(encodeSignMagnitude(100)).toBe(100); + expect(decodeSignMagnitude(100)).toBe(100); + + // Test negative values + const encoded = encodeSignMagnitude(-100); + expect(decodeSignMagnitude(encoded)).toBe(-100); + + // Test zero + expect(encodeSignMagnitude(0)).toBe(0); + expect(decodeSignMagnitude(0)).toBe(0); + }); + + it("should create SO-100 robot configuration", () => { + const config = createSO100Config("so100_follower"); + + expect(config.deviceType).toBe("so100_follower"); + expect(config.motorNames).toHaveLength(6); + expect(config.motorIds).toEqual([1, 2, 3, 4, 5, 6]); + expect(config.driveModes).toEqual([0, 0, 0, 0, 0, 0]); + expect(config.protocol.resolution).toBe(4096); + }); + + it("should have keyboard controls for SO-100", () => { + expect(SO100_KEYBOARD_CONTROLS.w.motor).toBe("elbow_flex"); + expect(SO100_KEYBOARD_CONTROLS.ArrowUp.motor).toBe("shoulder_lift"); + expect(SO100_KEYBOARD_CONTROLS.q.motor).toBe("wrist_roll"); + expect(SO100_KEYBOARD_CONTROLS.o.motor).toBe("gripper"); + }); +}); diff --git a/packages/node/src/utils/serial-port-wrapper.ts b/packages/node/src/utils/serial-port-wrapper.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8f95dc31e9ae0983b8f7ca9e79ed930414dc89e --- /dev/null +++ b/packages/node/src/utils/serial-port-wrapper.ts @@ -0,0 +1,119 @@ +/** + * Node.js Serial Port Wrapper + * Standardized serialport API interface with write/read operations + */ + +import { SerialPort } from "serialport"; + +/** + * Node.js Serial Port wrapper - provides write/read interface similar to web version + */ +export class NodeSerialPortWrapper { + private port: SerialPort; + private isConnected: boolean = false; + public readonly path: string; // Expose path for releaseMotors compatibility + + // Expose underlying port for OLD WORKING approach compatibility + public get underlyingPort(): SerialPort { + return this.port; + } + + constructor(path: string, options: any = {}) { + this.path = path; + this.port = new SerialPort({ + path, + baudRate: options.baudRate || 1000000, + dataBits: options.dataBits || 8, + parity: options.parity || "none", + stopBits: options.stopBits || 1, + autoOpen: false, + }); + } + + get isOpen(): boolean { + return this.isConnected && this.port.isOpen; + } + + async initialize(): Promise { + return new Promise((resolve, reject) => { + this.port.open((err) => { + if (err) { + reject(new Error(`Failed to open port: ${err.message}`)); + } else { + this.isConnected = true; + resolve(); + } + }); + }); + } + + // Add open method to match SerialPort interface + async open(): Promise { + return this.initialize(); + } + + async write(data: Uint8Array): Promise { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + reject(new Error("Port not open for writing")); + return; + } + + this.port.write(Buffer.from(data), (err) => { + if (err) { + reject(new Error(`Write failed: ${err.message}`)); + } else { + resolve(); + } + }); + }); + } + + async read(timeout: number = 1000): Promise { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + reject(new Error("Port not open for reading")); + return; + } + + const timeoutId = setTimeout(() => { + reject(new Error("Read timeout")); + }, timeout); + + const onData = (data: Buffer) => { + clearTimeout(timeoutId); + this.port.removeListener("data", onData); + resolve(data); // Return Buffer directly to match interface + }; + + this.port.once("data", onData); + }); + } + + async writeAndRead( + data: Uint8Array, + timeout: number = 1000 + ): Promise { + await this.write(data); + + // Wait for motor response (motors need time to process command) + await new Promise((resolve) => setTimeout(resolve, 10)); + + const response = await this.read(timeout); + return response ? new Uint8Array(response) : new Uint8Array(); + } + + async close(): Promise { + return new Promise((resolve) => { + if (this.isConnected && this.port.isOpen) { + this.port.close(() => { + this.isConnected = false; + resolve(); + }); + } else { + this.isConnected = false; + resolve(); + } + }); + } +} diff --git a/packages/node/src/utils/sign-magnitude.ts b/packages/node/src/utils/sign-magnitude.ts new file mode 100644 index 0000000000000000000000000000000000000000..e15eca5bdd690c2c946aa0cc4f09719126a06d2d --- /dev/null +++ b/packages/node/src/utils/sign-magnitude.ts @@ -0,0 +1,41 @@ +/** + * Sign-Magnitude Encoding Utilities + * For STS3215 motor homing offset encoding/decoding + */ + +import { STS3215_PROTOCOL } from "./sts3215-protocol.js"; + +/** + * Encode a signed integer using sign-magnitude format + * Bit at sign_bit_index represents sign (0=positive, 1=negative) + * Lower bits represent magnitude + */ +export function encodeSignMagnitude( + value: number, + signBitIndex: number = STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT +): number { + const maxMagnitude = (1 << signBitIndex) - 1; + const magnitude = Math.abs(value); + + if (magnitude > maxMagnitude) { + throw new Error( + `Magnitude ${magnitude} exceeds ${maxMagnitude} (max for signBitIndex=${signBitIndex})` + ); + } + + const directionBit = value < 0 ? 1 : 0; + return (directionBit << signBitIndex) | magnitude; +} + +/** + * Decode a sign-magnitude encoded value back to signed integer + * Extracts sign bit and magnitude, then applies sign + */ +export function decodeSignMagnitude( + encodedValue: number, + signBitIndex: number = STS3215_PROTOCOL.SIGN_MAGNITUDE_BIT +): number { + const signBit = (encodedValue >> signBitIndex) & 1; + const magnitude = encodedValue & ((1 << signBitIndex) - 1); + return signBit ? -magnitude : magnitude; +} \ No newline at end of file diff --git a/packages/node/src/utils/sts3215-protocol.ts b/packages/node/src/utils/sts3215-protocol.ts new file mode 100644 index 0000000000000000000000000000000000000000..e37da6cecc64d6e840a4b6b0b45b411ef58239f3 --- /dev/null +++ b/packages/node/src/utils/sts3215-protocol.ts @@ -0,0 +1,34 @@ +/** + * STS3215 Protocol Constants and Configuration + * Single source of truth for all STS3215 motor communication + */ + +/** + * STS3215 Protocol Configuration + * Register addresses, timing, and communication constants + */ +export const STS3215_PROTOCOL = { + // Register addresses + PRESENT_POSITION_ADDRESS: 56, + GOAL_POSITION_ADDRESS: 42, + HOMING_OFFSET_ADDRESS: 31, + MIN_POSITION_LIMIT_ADDRESS: 9, + MAX_POSITION_LIMIT_ADDRESS: 11, + TORQUE_ENABLE_ADDRESS: 40, // Torque Enable register (0=disable, 1=enable) + + // Protocol constants + RESOLUTION: 4096, // 12-bit resolution (0-4095) + SIGN_MAGNITUDE_BIT: 11, // Bit 11 is sign bit for Homing_Offset encoding + + // Data lengths + HOMING_OFFSET_LENGTH: 2, + PRESENT_POSITION_LENGTH: 2, + MIN_POSITION_LIMIT_LENGTH: 2, + MAX_POSITION_LIMIT_LENGTH: 2, + + // Communication timing (OLD WORKING VALUES) + WRITE_TO_READ_DELAY: 0, // No delay - immediate read (like old approach) + RETRY_DELAY: 50, // Base retry delay (multiplied by attempt number) + INTER_MOTOR_DELAY: 10, // Small delay between motors (like old approach) + MAX_RETRIES: 3, +} as const; diff --git a/packages/node/tsconfig.build.json b/packages/node/tsconfig.build.json new file mode 100644 index 0000000000000000000000000000000000000000..14e55f6015e9903657930098e767632dfcf9fe34 --- /dev/null +++ b/packages/node/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": false + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "dist", "node_modules"] +} diff --git a/packages/node/vite.config.ts b/packages/node/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..5790c2f5a5b1c13dd0eb80f16b18f861d38a6eff --- /dev/null +++ b/packages/node/vite.config.ts @@ -0,0 +1,40 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + target: "node18", + lib: { + entry: { + index: "src/index.ts", + calibrate: "src/calibrate.ts", + teleoperate: "src/teleoperate.ts", + find_port: "src/find_port.ts", + release_motors: "src/release_motors.ts", + }, + formats: ["es"], + }, + rollupOptions: { + external: [ + // Node.js built-ins + "fs", + "path", + "os", + "readline", + "util", + "events", + "stream", + "fs/promises", + // External dependencies + "serialport", + ], + }, + minify: false, + sourcemap: true, + }, + test: { + globals: true, + environment: "node", + include: ["src/**/*.test.ts"], + exclude: ["dist", "node_modules"], + }, +}); diff --git a/packages/web/.eslintrc.json b/packages/web/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..22ea29335d9b96d1b5e3de33f6f545ad7318742d --- /dev/null +++ b/packages/web/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": ["eslint:recommended", "@typescript-eslint/recommended"], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "env": { + "browser": true, + "es2022": true + }, + "rules": { + "@typescript-eslint/no-unused-vars": [ + "error", + { "argsIgnorePattern": "^_" } + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-assertion": "warn" + }, + "ignorePatterns": ["dist/", "node_modules/"] +} diff --git a/packages/web/README.md b/packages/web/README.md index 206e83d9554c183343a51addfde4386ae60678d2..a6780475547d59ca323cf26d7b86585dc4e121e7 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -22,27 +22,44 @@ yarn add @lerobot/web ```typescript import { findPort, releaseMotors, calibrate, teleoperate } from "@lerobot/web"; -// 1. find and connect to hardware like a robot arm +// 1. find available robot ports (shows browser port dialog) +console.log("🔍 finding available robot ports..."); const findProcess = await findPort(); const robots = await findProcess.result; -const robot = robots[0]; -// 2. release the motors and put them into the homing position +if (robots.length === 0) { + console.log("❌ no robots found. check connections and try again."); + return; +} + +// 2. connect to the first robot found +console.log(`✅ found ${robots.length} robot(s). using first one...`); +const robot = robots[0]; // already connected from findPort + +// 3. release motors for manual positioning +console.log("🔓 releasing motors for manual positioning..."); await releaseMotors(robot); -// 3. calibrate the motors by moving each motor through its full range of motion +// 4. calibrate motors by moving through full range +console.log("⚙️ starting calibration..."); const calibrationProcess = await calibrate({ robot, onProgress: (message) => console.log(message), - onLiveUpdate: (data) => console.log("Live positions:", data), + onLiveUpdate: (data) => console.log("live positions:", data), }); -// when done, stop calibration and get the min/max ranges for each motor -// which we need to control the robot in its defined ranges -calibrationProcess.stop(); +// move robot through its range, then stop calibration +console.log("👋 move robot through full range, then stop calibration..."); +// in a real app, you'd have a button to stop calibration +setTimeout(() => { + calibrationProcess.stop(); +}, 10000); // stop after 10 seconds for demo + const calibrationData = await calibrationProcess.result; +console.log("✅ calibration complete!"); -// 4. start controlling the robot arm with your keyboard +// 5. control robot with keyboard +console.log("🎮 starting keyboard control..."); const teleop = await teleoperate({ robot, calibrationData, @@ -50,10 +67,30 @@ const teleop = await teleoperate({ }); teleop.start(); -// stop any control -teleop.stop(); +// stop control after 30 seconds +setTimeout(() => { + teleop.stop(); + console.log("🛑 control stopped"); +}, 30000); +``` + +## How It Works + +### **`findPort()` - Discovery + Connection in One Step** + +In the browser, `findPort()` handles both discovery AND connection testing. It returns ready-to-use robot connections: + +```typescript +// ✅ browser workflow: find and connect in one step +const findProcess = await findPort(); +const robots = await findProcess.result; +const robot = robots[0]; // ready to use - already connected and tested! ``` +**Why no separate `connectPort()`?** The browser's WebSerial API requires user interaction for port access, so `findPort()` handles everything in one user-friendly flow. + +**Need direct port connection?** Use `@lerobot/node` which provides `connectPort()` for server-side applications where you know the exact port path (e.g., `"/dev/ttyUSB0"`). + ## Core API ### `findPort(config?): Promise` @@ -185,21 +222,57 @@ const calibrationData = await calibrationProcess.result; #### Returns: `CalibrationProcess` -- `result: Promise` - Calibration data (Python-compatible format) +- `result: Promise` - Calibration data (Python-compatible format) - `stop(): void` - Stop calibration process #### Calibration Data Format -```typescript +**Python Compatible**: This format is identical to Python lerobot calibration files. + +```json { "shoulder_pan": { "id": 1, "drive_mode": 0, - "homing_offset": 47, - "range_min": 985, - "range_max": 3085 + "homing_offset": 14, + "range_min": 1015, + "range_max": 3128 + }, + "shoulder_lift": { + "id": 2, + "drive_mode": 0, + "homing_offset": 989, + "range_min": 965, + "range_max": 3265 + }, + "elbow_flex": { + "id": 3, + "drive_mode": 0, + "homing_offset": -879, + "range_min": 820, + "range_max": 3051 + }, + "wrist_flex": { + "id": 4, + "drive_mode": 0, + "homing_offset": 31, + "range_min": 758, + "range_max": 3277 }, - // ... other motors + "wrist_roll": { + "id": 5, + "drive_mode": 0, + "homing_offset": -37, + "range_min": 2046, + "range_max": 3171 + }, + "gripper": { + "id": 6, + "drive_mode": 0, + "homing_offset": -1173, + "range_min": 2038, + "range_max": 3528 + } } ``` diff --git a/packages/web/package.json b/packages/web/package.json index 5a9383521b127d03228e82b121589ea628d7e3c7..88e7ece1f11654b98f8f363a82cee20bd88d0bc6 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -37,6 +37,8 @@ "scripts": { "build": "tsc --project tsconfig.build.json", "watch": "tsc --project tsconfig.build.json --watch", + "lint": "eslint src --ext .ts,.tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint src --ext .ts,.tsx --fix", "prepublishOnly": "npm run build", "dev": "vitest --watch", "test": "vitest run", @@ -44,8 +46,12 @@ "test:coverage": "vitest run --coverage" }, "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "@vitest/ui": "^2.1.9", + "eslint": "^8.0.0", "jsdom": "^24.1.3", + "typescript": "^5.3.0", "vite": "^6.3.5", "vitest": "^2.1.9" }, diff --git a/packages/web/src/calibrate.ts b/packages/web/src/calibrate.ts index 6e7b40870eb76d3b869557b350becad57bc6b4c7..c2de11bf94bd826e488f408a08076d4bb67c25e6 100644 --- a/packages/web/src/calibrate.ts +++ b/packages/web/src/calibrate.ts @@ -18,14 +18,14 @@ import { createSO100Config } from "./robots/so100_config.js"; import type { RobotHardwareConfig } from "./types/robot-config.js"; import type { CalibrateConfig, - WebCalibrationResults, + CalibrationResults, LiveCalibrationData, CalibrationProcess, } from "./types/calibration.js"; // Re-export types for external use export type { - WebCalibrationResults, + CalibrationResults, LiveCalibrationData, CalibrationProcess, } from "./types/calibration.js"; @@ -154,7 +154,7 @@ export async function calibrate( const stopFunction = () => shouldStop; // Start calibration process - const resultPromise = (async (): Promise => { + const resultPromise = (async (): Promise => { // Step 1: Set homing offsets (automatic) onProgress?.("⚙️ Setting motor homing offsets"); const homingOffsets = await setHomingOffsets( @@ -190,7 +190,7 @@ export async function calibrate( ); // Step 5: Compile results - const results: WebCalibrationResults = {}; + const results: CalibrationResults = {}; for (let i = 0; i < robotConfig.motorNames.length; i++) { const motorName = robotConfig.motorNames[i]; diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 23e6d80ff67a45f2bf8104cbc64191464048247b..e39c1f032005efcf1bcf09b7dc04515d9f1f41b4 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -32,7 +32,7 @@ export type { export type { CalibrateConfig, - WebCalibrationResults, + CalibrationResults, LiveCalibrationData, CalibrationProcess, } from "./types/calibration.js"; @@ -53,6 +53,10 @@ export type { // Utilities (advanced users) export { WebSerialPortWrapper } from "./utils/serial-port-wrapper.js"; +export { + readAllMotorPositions, + readMotorPosition, +} from "./utils/motor-communication.js"; export { createSO100Config, SO100_KEYBOARD_CONTROLS, diff --git a/packages/web/src/types/calibration.ts b/packages/web/src/types/calibration.ts index 99a1b7c2da6e581a334fac8a15ead3c68b24021a..cb942ab5d5f20b16ab419838923c6dbb194aa105 100644 --- a/packages/web/src/types/calibration.ts +++ b/packages/web/src/types/calibration.ts @@ -26,9 +26,9 @@ export interface CalibrateConfig { } /** - * Calibration results structure + * Calibration results structure - Python lerobot compatible format */ -export interface WebCalibrationResults { +export interface CalibrationResults { [motorName: string]: { id: number; drive_mode: number; @@ -43,5 +43,5 @@ export interface WebCalibrationResults { */ export interface CalibrationProcess { stop(): void; - result: Promise; + result: Promise; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba7d27f102e0813a10d006e63ca0a3e0378b4258..eead5ad230e50a9c010af52b34f043a4f451a0dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,54 +7,70 @@ settings: importers: .: - dependencies: - '@aws-sdk/client-s3': - specifier: ^3.856.0 - version: 3.856.0 - '@aws-sdk/lib-storage': - specifier: ^3.856.0 - version: 3.856.0(@aws-sdk/client-s3@3.856.0) - '@huggingface/hub': - specifier: ^2.4.0 - version: 2.4.0 - '@react-three/cannon': - specifier: ^6.6.0 - version: 6.6.0(@react-three/fiber@9.2.0(@types/react@19.1.8)(react@19.1.0)(three@0.178.0))(react@19.1.0)(three@0.178.0)(typescript@5.8.3) - apache-arrow: - specifier: ^21.0.0 - version: 21.0.0 - jszip: - specifier: ^3.10.1 - version: 3.10.1 - log-update: - specifier: ^6.1.0 - version: 6.1.0 - parquet-wasm: - specifier: ^0.6.1 - version: 0.6.1 - serialport: - specifier: ^12.0.0 - version: 12.0.0 - three: - specifier: ^0.178.0 - version: 0.178.0 devDependencies: '@changesets/cli': specifier: ^2.29.5 version: 2.29.5 '@types/node': - specifier: ^22.17.0 - version: 22.17.0 - '@types/three': - specifier: ^0.178.1 - version: 0.178.1 - tsx: - specifier: ^4.20.3 - version: 4.20.3 + specifier: ^22.10.5 + version: 22.15.31 typescript: specifier: ~5.8.3 version: 5.8.3 + packages/cli: + dependencies: + '@lerobot/node': + specifier: file:../node + version: link:../node + chalk: + specifier: ^5.3.0 + version: 5.6.0 + commander: + specifier: ^11.0.0 + version: 11.1.0 + serialport: + specifier: ^12.0.0 + version: 12.0.0 + devDependencies: + '@types/node': + specifier: ^18.0.0 + version: 18.19.123 + typescript: + specifier: ^5.3.0 + version: 5.8.3 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@18.19.123)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.0) + vite-node: + specifier: ^2.0.0 + version: 2.1.9(@types/node@18.19.123) + + packages/node: + dependencies: + serialport: + specifier: ^12.0.0 + version: 12.0.0 + devDependencies: + '@types/node': + specifier: ^18.0.0 + version: 18.19.123 + '@vitest/ui': + specifier: ^2.0.0 + version: 2.1.9(vitest@2.1.9) + typescript: + specifier: ^5.3.0 + version: 5.8.3 + vite: + specifier: ^6.3.5 + version: 6.3.5(@types/node@18.19.123)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.0) + vite-node: + specifier: ^2.0.0 + version: 2.1.9(@types/node@18.19.123) + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@18.19.123)(@vitest/ui@2.1.9)(jsdom@24.1.3) + packages/web: dependencies: typescript: @@ -79,165 +95,8 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@aws-crypto/crc32@5.2.0': - resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/crc32c@5.2.0': - resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} - - '@aws-crypto/sha1-browser@5.2.0': - resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} - - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-s3@3.856.0': - resolution: {integrity: sha512-bU6eUA3/g5Ehe1cHOBWgPqBamxDTuHpnUOT10Q8c7wVKdJN1+zx+cDQnoRwjOeGc5Rfiv0pMgy7iVFPfJA9aOQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/client-sso@3.856.0': - resolution: {integrity: sha512-MrLxzTrsipNxp+L0rOJeSUBUJEamnvOzAGzl4lQfl+1mtufKeKskwKuUu1NizLxZGLQQ77T8HFb8z1e1fAgcIg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/core@3.856.0': - resolution: {integrity: sha512-8E5qLsjJ/AwOCOwKxKdfaLEWiRZBrtFJaWlDkC8vTT0/nCzCLMxW8lEKMzkvsxRfje0YZ3V1+rcNycvlz0jVTw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-env@3.856.0': - resolution: {integrity: sha512-CQ8dVEonRlNHjinKUp3Dr+ihArpXMIjm0/S3N3UoujDaj40HS8Z3yc3S4TfSj5fhEEYxvWi2YDg2gEuKLf5eVw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-http@3.856.0': - resolution: {integrity: sha512-tqOmuPNaWJNVw69RmayCCaJ1ZslETvbOD3rUQPdy0OQcZ7MKcXmchPGA4Uu26CRbFxXFDvGtdEAoZfHJOt9IwA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-ini@3.856.0': - resolution: {integrity: sha512-OM41VYwleo/WFY/osHMetI/6ypeaeC597pCsTUX9u7OCt9jFldW0xC8YvWa8q1R195hfoPWjN0QMjkp0iGppCA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-node@3.856.0': - resolution: {integrity: sha512-PklXMo3ReBcXVMsigacQHdfrwHWx2SFctQCBGHRcY6NLoaHnVe4g+oW+BOOCj7c9JGn7c2mMNMzhuxgsfuDXRw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-process@3.856.0': - resolution: {integrity: sha512-mR+3uVhlHBrqRh7rFs8CRJ30go9xuB8uWHf2FL63ZliuYzYCrFknj+y+PLvYl+Aa4Ok57SW9BVrKkj6OpiEFKA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-sso@3.856.0': - resolution: {integrity: sha512-5/WY5zI8iF+HvxeNBiP7kOnn60jr76/MRGU8qQmbXd2/7GZM2sAHSTY2Qot6D9HwutAsU924y8Kxa/m7VZT4GQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.856.0': - resolution: {integrity: sha512-QPoHotFD7aiI+l1WF/QTlNVMtR7VY31y1uYaCXBTkmuELNlNFAVptNlct51/OcAqlWLp6wWfz75Sy9vdbNxuXw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/lib-storage@3.856.0': - resolution: {integrity: sha512-mM5L1ZUfulemREmXHSFIR3YGE5mBaZxIF1H/22zwgd3Ty8WbfsI9I+0lYADurCbc8/lHnFbVP3dZReL3scSS9A==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@aws-sdk/client-s3': ^3.856.0 - - '@aws-sdk/middleware-bucket-endpoint@3.840.0': - resolution: {integrity: sha512-+gkQNtPwcSMmlwBHFd4saVVS11In6ID1HczNzpM3MXKXRBfSlbZJbCt6wN//AZ8HMklZEik4tcEOG0qa9UY8SQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-expect-continue@3.840.0': - resolution: {integrity: sha512-iJg2r6FKsKKvdiU4oCOuCf7Ro/YE0Q2BT/QyEZN3/Rt8Nr4SAZiQOlcBXOCpGvuIKOEAhvDOUnW3aDHL01PdVw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-flexible-checksums@3.856.0': - resolution: {integrity: sha512-uM6a6VZAyM4h2fzD6qK6vQ0jwD1Vi8OzV+1xfb6Mjx3DsF0Dn+CJSpzVdX/EvGbWDEHdo2uhGw652lI08RJv8Q==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-host-header@3.840.0': - resolution: {integrity: sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-location-constraint@3.840.0': - resolution: {integrity: sha512-KVLD0u0YMF3aQkVF8bdyHAGWSUY6N1Du89htTLgqCcIhSxxAJ9qifrosVZ9jkAzqRW99hcufyt2LylcVU2yoKQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-logger@3.840.0': - resolution: {integrity: sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-recursion-detection@3.840.0': - resolution: {integrity: sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-sdk-s3@3.856.0': - resolution: {integrity: sha512-awpykPkEPZUTbVVWQrGiJmaAvLiqCRxJ8/JwGTcidZJZslOWUyBReJvU6XbxJoAhU/ZSyORQ6mITpAkIMS7PWw==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-ssec@3.840.0': - resolution: {integrity: sha512-CBZP9t1QbjDFGOrtnUEHL1oAvmnCUUm7p0aPNbIdSzNtH42TNKjPRN3TuEIJDGjkrqpL3MXyDSmNayDcw/XW7Q==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/middleware-user-agent@3.856.0': - resolution: {integrity: sha512-yObY8fwTtW6uG0jYfSOiFi8Fpi0ZdKl4kU1XSIJPNiE/wn9JsoAYZ1hHHtRgJHMfmgnsWMWGeOnPt6LzREtXsQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/nested-clients@3.856.0': - resolution: {integrity: sha512-ZDpWSlOXChlzNKdbKcW77iRQZKwuN4q9kDFvs0tD2TqhHMx9JMqegHaqLz8GwVVe/nPZRdx8cuguYCIEb4MSUg==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/region-config-resolver@3.840.0': - resolution: {integrity: sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.856.0': - resolution: {integrity: sha512-fMMjYWgzGMwAU2dD3BS/nzHQyNfb2F9ZiwUtUZ8265Ep9KhaBRS8uI8tnLGT6UDI6BTwQRS6qbFlRI/sY4KpZA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/token-providers@3.856.0': - resolution: {integrity: sha512-VTvUxY7hTPfsi4iehKAat3zaJj303f6KkXpA+p4LmijOkXdNoS8ziHlb5A/0PNFX5eobGJrBo391E+4bJPtpZA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/types@3.840.0': - resolution: {integrity: sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-arn-parser@3.804.0': - resolution: {integrity: sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-endpoints@3.848.0': - resolution: {integrity: sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-locate-window@3.804.0': - resolution: {integrity: sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==} - engines: {node: '>=18.0.0'} - - '@aws-sdk/util-user-agent-browser@3.840.0': - resolution: {integrity: sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==} - - '@aws-sdk/util-user-agent-node@3.856.0': - resolution: {integrity: sha512-BasNKsYoB18hUgBxAhtaU5xtyqe0A4CQ6VBtXzRu5+xYcAXsuP+5l0Wnr5BN9PNrOEvFvxTHZqVPejLNxbeM0Q==} - engines: {node: '>=18.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.821.0': - resolution: {integrity: sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==} - engines: {node: '>=18.0.0'} - - '@babel/runtime@7.28.2': - resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==} + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} '@changesets/apply-release-plan@7.0.12': @@ -323,9 +182,6 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@dimforge/rapier3d-compat@0.12.0': - resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -338,12 +194,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.25.8': - resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -356,12 +206,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.25.8': - resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -374,12 +218,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.25.8': - resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -392,12 +230,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.25.8': - resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -410,12 +242,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.25.8': - resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -428,12 +254,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.25.8': - resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -446,12 +266,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.25.8': - resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -464,12 +278,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.8': - resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -482,12 +290,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.25.8': - resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -500,12 +302,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.25.8': - resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -518,12 +314,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.25.8': - resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -536,12 +326,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.25.8': - resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -554,12 +338,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.25.8': - resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -572,12 +350,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.25.8': - resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -590,12 +362,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.25.8': - resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -608,12 +374,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.25.8': - resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -626,24 +386,12 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.25.8': - resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/netbsd-arm64@0.25.5': resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.25.8': - resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -656,24 +404,12 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.8': - resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/openbsd-arm64@0.25.5': resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.25.8': - resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -686,18 +422,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.8': - resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.8': - resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -710,12 +434,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.25.8': - resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -728,12 +446,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.25.8': - resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -746,12 +458,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.25.8': - resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -764,20 +470,6 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.25.8': - resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@huggingface/hub@2.4.0': - resolution: {integrity: sha512-8H9uLOIBJNPw6lUV1g/t+7pAVNOhBW5g7CNAOJ0JVkA1QObEb7u0O7vMVkjmu3311SUU39aSx8l1Fgsw586g4g==} - engines: {node: '>=18'} - hasBin: true - - '@huggingface/tasks@0.19.34': - resolution: {integrity: sha512-dIl3jyeddCEFJeogJOcbhfIq1tlo3N9K4EAxG/MfkGL0l7hI2kfs91Ut+1h6i09TQM8A9XM91NV7Jz6PgfWE7Q==} - '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -799,46 +491,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@pmndrs/cannon-worker-api@2.4.0': - resolution: {integrity: sha512-oJA1Bboc+WObksRaGDKJG0Wna9Q75xi1MdXVAZ9qXzBOyPsadmAnrmiKOEF0R8v/4zsuJElvscNZmyo3msbZjA==} - peerDependencies: - three: '>=0.139' - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@react-three/cannon@6.6.0': - resolution: {integrity: sha512-lP9rJoVHQi0w+dYF8FJAm2xr5eLfNEckb04j72kjqndUkuOPr26N4rSBhQbHl5b5N3tEnhQaIMungAvHkcY8/A==} - peerDependencies: - '@react-three/fiber': '>=8' - react: '>=18' - three: '>=0.139' - - '@react-three/fiber@9.2.0': - resolution: {integrity: sha512-esZe+E9T/aYEM4HlBkirr/yRE8qWTp9WUsLISyHHMCHKlJv85uc5N4wwKw+Ay0QeTSITw6T9Q3Svpu383Q+CSQ==} - peerDependencies: - expo: '>=43.0' - expo-asset: '>=8.4' - expo-file-system: '>=11.0' - expo-gl: '>=11.0' - react: ^19.0.0 - react-dom: ^19.0.0 - react-native: '>=0.78' - three: '>=0.156' - peerDependenciesMeta: - expo: - optional: true - expo-asset: - optional: true - expo-file-system: - optional: true - expo-gl: - optional: true - react-dom: - optional: true - react-native: - optional: true - '@rollup/rollup-android-arm-eabi@4.43.0': resolution: {integrity: sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==} cpu: [arm] @@ -1003,278 +658,31 @@ packages: resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} engines: {node: '>=12.0.0'} - '@smithy/abort-controller@4.0.4': - resolution: {integrity: sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==} - engines: {node: '>=18.0.0'} - - '@smithy/chunked-blob-reader-native@4.0.0': - resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==} - engines: {node: '>=18.0.0'} + '@types/estree@1.0.7': + resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - '@smithy/chunked-blob-reader@5.0.0': - resolution: {integrity: sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==} - engines: {node: '>=18.0.0'} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@smithy/config-resolver@4.1.4': - resolution: {integrity: sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==} - engines: {node: '>=18.0.0'} + '@types/node@18.19.123': + resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==} - '@smithy/core@3.7.2': - resolution: {integrity: sha512-JoLw59sT5Bm8SAjFCYZyuCGxK8y3vovmoVbZWLDPTH5XpPEIwpFd9m90jjVMwoypDuB/SdVgje5Y4T7w50lJaw==} - engines: {node: '>=18.0.0'} + '@types/node@22.15.31': + resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==} - '@smithy/credential-provider-imds@4.0.6': - resolution: {integrity: sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==} - engines: {node: '>=18.0.0'} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@smithy/eventstream-codec@4.0.4': - resolution: {integrity: sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-browser@4.0.4': - resolution: {integrity: sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-config-resolver@4.1.2': - resolution: {integrity: sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-node@4.0.4': - resolution: {integrity: sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-universal@4.0.4': - resolution: {integrity: sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.1.0': - resolution: {integrity: sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-blob-browser@4.0.4': - resolution: {integrity: sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.0.4': - resolution: {integrity: sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-stream-node@4.0.4': - resolution: {integrity: sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.0.4': - resolution: {integrity: sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==} - engines: {node: '>=18.0.0'} - - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/is-array-buffer@4.0.0': - resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} - engines: {node: '>=18.0.0'} - - '@smithy/md5-js@4.0.4': - resolution: {integrity: sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-content-length@4.0.4': - resolution: {integrity: sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.1.17': - resolution: {integrity: sha512-S3hSGLKmHG1m35p/MObQCBCdRsrpbPU8B129BVzRqRfDvQqPMQ14iO4LyRw+7LNizYc605COYAcjqgawqi+6jA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.1.18': - resolution: {integrity: sha512-bYLZ4DkoxSsPxpdmeapvAKy7rM5+25gR7PGxq2iMiecmbrRGBHj9s75N74Ylg+aBiw9i5jIowC/cLU2NR0qH8w==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.0.8': - resolution: {integrity: sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.0.4': - resolution: {integrity: sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.1.3': - resolution: {integrity: sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.1.0': - resolution: {integrity: sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==} - engines: {node: '>=18.0.0'} - - '@smithy/property-provider@4.0.4': - resolution: {integrity: sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@5.1.2': - resolution: {integrity: sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.0.4': - resolution: {integrity: sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-parser@4.0.4': - resolution: {integrity: sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==} - engines: {node: '>=18.0.0'} - - '@smithy/service-error-classification@4.0.6': - resolution: {integrity: sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.0.4': - resolution: {integrity: sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.1.2': - resolution: {integrity: sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.4.9': - resolution: {integrity: sha512-mbMg8mIUAWwMmb74LoYiArP04zWElPzDoA1jVOp3or0cjlDMgoS6WTC3QXK0Vxoc9I4zdrX0tq6qsOmaIoTWEQ==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.3.1': - resolution: {integrity: sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==} - engines: {node: '>=18.0.0'} - - '@smithy/url-parser@4.0.4': - resolution: {integrity: sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-base64@4.0.0': - resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.0.0': - resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.0.0': - resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-buffer-from@4.0.0': - resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.0.0': - resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.0.25': - resolution: {integrity: sha512-pxEWsxIsOPLfKNXvpgFHBGFC3pKYKUFhrud1kyooO9CJai6aaKDHfT10Mi5iiipPXN/JhKAu3qX9o75+X85OdQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.0.25': - resolution: {integrity: sha512-+w4n4hKFayeCyELZLfsSQG5mCC3TwSkmRHv4+el5CzFU8ToQpYGhpV7mrRzqlwKkntlPilT1HJy1TVeEvEjWOQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-endpoints@3.0.6': - resolution: {integrity: sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-hex-encoding@4.0.0': - resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.0.4': - resolution: {integrity: sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.0.6': - resolution: {integrity: sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-stream@4.2.3': - resolution: {integrity: sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.0.0': - resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@4.0.0': - resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} - engines: {node: '>=18.0.0'} - - '@smithy/util-waiter@4.0.6': - resolution: {integrity: sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==} - engines: {node: '>=18.0.0'} - - '@swc/helpers@0.5.17': - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - - '@tweenjs/tween.js@23.1.3': - resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} - - '@types/command-line-args@5.2.3': - resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} - - '@types/command-line-usage@5.0.4': - resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} - - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - - '@types/node@22.15.31': - resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==} - - '@types/node@22.17.0': - resolution: {integrity: sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==} - - '@types/node@24.1.0': - resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} - - '@types/react-reconciler@0.28.9': - resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} - peerDependencies: - '@types/react': '*' - - '@types/react@19.1.8': - resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} - - '@types/stats.js@0.17.4': - resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} - - '@types/three@0.178.1': - resolution: {integrity: sha512-WSabew1mgWgRx2RfLfKY+9h4wyg6U94JfLbZEGU245j/WY2kXqU0MUfghS+3AYMV5ET1VlILAgpy77cB6a3Itw==} - - '@types/uuid@9.0.8': - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - - '@types/webxr@0.5.22': - resolution: {integrity: sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==} - - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} @@ -1296,9 +704,6 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} - '@webgpu/types@0.1.64': - resolution: {integrity: sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -1307,37 +712,13 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@7.0.0: - resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} - engines: {node: '>=18'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - apache-arrow@21.0.0: - resolution: {integrity: sha512-UueXr0y7S6SB6ToIEON0ZIwRln1EY05NIMXKfPu8fumASypkXXHEb6LRTZGh7vnYoQ9TgqNMNN1937wyY9lyFQ==} - hasBin: true - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - array-back@6.2.2: - resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} - engines: {node: '>=12.17'} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -1349,26 +730,14 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} - bowser@2.11.0: - resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - buffer@5.6.0: - resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==} - - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1377,30 +746,13 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - cannon-es-debugger@1.0.0: - resolution: {integrity: sha512-sE9lDOBAYFKlh+0w+cvWKwUhJef8HYnUSVPWPL0jD15MAuVRQKno4QYZSGxgOoJkMR3mQqxL4bxys2b3RSWH8g==} - peerDependencies: - cannon-es: 0.x - three: 0.x - typescript: '>=3.8' - peerDependenciesMeta: - typescript: - optional: true - - cannon-es@0.20.0: - resolution: {integrity: sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==} - chai@5.2.1: resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} engines: {node: '>=18'} - chalk-template@0.4.0: - resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} - engines: {node: '>=12'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + chalk@5.6.0: + resolution: {integrity: sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} @@ -1413,36 +765,13 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} - cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - command-line-args@6.0.1: - resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==} - engines: {node: '>=12.20'} - peerDependencies: - '@75lb/nature': latest - peerDependenciesMeta: - '@75lb/nature': - optional: true - - command-line-usage@7.0.3: - resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} - engines: {node: '>=12.20.0'} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -1452,9 +781,6 @@ packages: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -1500,9 +826,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - emoji-regex@10.4.0: - resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} - enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -1511,10 +834,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - environment@1.1.0: - resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} - engines: {node: '>=18'} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1544,11 +863,6 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.25.8: - resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} - engines: {node: '>=18'} - hasBin: true - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -1557,10 +871,6 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -1576,10 +886,6 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-xml-parser@5.2.5: - resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} - hasBin: true - fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1598,22 +904,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-replace@5.0.2: - resolution: {integrity: sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==} - engines: {node: '>=14'} - peerDependencies: - '@75lb/nature': latest - peerDependenciesMeta: - '@75lb/nature': - optional: true - find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - flatbuffers@25.2.10: - resolution: {integrity: sha512-7JlN9ZvLDG1McO3kbX0k4v+SUAg48L1rIwEvN6ZQl/eCtgJz9UylTMzE9wrmYrcorgxm3CX/3T/w5VAub99UUw==} - flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} @@ -1637,10 +931,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-east-asian-width@1.3.0: - resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} - engines: {node: '>=18'} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1667,10 +957,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1707,27 +993,14 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@5.0.0: - resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} - engines: {node: '>=18'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1747,17 +1020,9 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - its-fine@2.0.0: - resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} - peerDependencies: - react: ^19.0.0 - jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -1775,33 +1040,16 @@ packages: canvas: optional: true - json-bignum@0.0.3: - resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} - engines: {node: '>=0.8'} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - jszip@3.10.1: - resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - - lie@3.3.0: - resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - log-update@6.1.0: - resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} - engines: {node: '>=18'} - loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} @@ -1819,9 +1067,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - meshoptimizer@0.18.1: - resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1834,10 +1079,6 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1867,10 +1108,6 @@ packages: nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} - os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} @@ -1901,12 +1138,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - - parquet-wasm@0.6.1: - resolution: {integrity: sha512-wTM/9Y4EHny8i0qgcOlL9UHsTXftowwCqDsAD8axaZbHp0Opp3ue8oxexbzTVNhqBjFhyhLiU3MT0rnEYnYU0Q==} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -1953,9 +1184,6 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -1972,36 +1200,10 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-reconciler@0.31.0: - resolution: {integrity: sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^19.0.0 - - react-use-measure@2.1.7: - resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} - peerDependencies: - react: '>=16.13' - react-dom: '>=16.13' - peerDependenciesMeta: - react-dom: - optional: true - - react@19.1.0: - resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} - engines: {node: '>=0.10.0'} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -2012,10 +1214,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2034,12 +1232,6 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2047,9 +1239,6 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.25.0: - resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} - semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -2059,9 +1248,6 @@ packages: resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} engines: {node: '>=16.0.0'} - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2085,10 +1271,6 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - slice-ansi@7.1.0: - resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} - engines: {node: '>=18'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2105,57 +1287,21 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} - stream-browserify@3.0.0: - resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} - - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strnum@2.1.1: - resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - suspend-react@0.1.3: - resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} - peerDependencies: - react: '>=17.0' - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - table-layout@4.1.1: - resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} - engines: {node: '>=12.17'} - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - three@0.178.0: - resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2198,9 +1344,6 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.20.3: resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} engines: {node: '>=18.0.0'} @@ -2211,16 +1354,12 @@ packages: engines: {node: '>=14.17'} hasBin: true - typical@7.3.0: - resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} - engines: {node: '>=12.17'} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.8.0: - resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} - universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -2232,18 +1371,6 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2375,14 +1502,6 @@ packages: engines: {node: '>=8'} hasBin: true - wordwrapjs@5.1.0: - resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} - engines: {node: '>=12.17'} - - wrap-ansi@9.0.0: - resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} - engines: {node: '>=18'} - ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -2407,24 +1526,6 @@ packages: engines: {node: '>= 14.6'} hasBin: true - zustand@5.0.6: - resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - snapshots: '@asamuzakjp/css-color@3.2.0': @@ -2435,484 +1536,7 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@aws-crypto/crc32@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.840.0 - tslib: 2.8.1 - - '@aws-crypto/crc32c@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.840.0 - tslib: 2.8.1 - - '@aws-crypto/sha1-browser@5.2.0': - dependencies: - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-locate-window': 3.804.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-locate-window': 3.804.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.840.0 - tslib: 2.8.1 - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-s3@3.856.0': - dependencies: - '@aws-crypto/sha1-browser': 5.2.0 - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.856.0 - '@aws-sdk/credential-provider-node': 3.856.0 - '@aws-sdk/middleware-bucket-endpoint': 3.840.0 - '@aws-sdk/middleware-expect-continue': 3.840.0 - '@aws-sdk/middleware-flexible-checksums': 3.856.0 - '@aws-sdk/middleware-host-header': 3.840.0 - '@aws-sdk/middleware-location-constraint': 3.840.0 - '@aws-sdk/middleware-logger': 3.840.0 - '@aws-sdk/middleware-recursion-detection': 3.840.0 - '@aws-sdk/middleware-sdk-s3': 3.856.0 - '@aws-sdk/middleware-ssec': 3.840.0 - '@aws-sdk/middleware-user-agent': 3.856.0 - '@aws-sdk/region-config-resolver': 3.840.0 - '@aws-sdk/signature-v4-multi-region': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-endpoints': 3.848.0 - '@aws-sdk/util-user-agent-browser': 3.840.0 - '@aws-sdk/util-user-agent-node': 3.856.0 - '@aws-sdk/xml-builder': 3.821.0 - '@smithy/config-resolver': 4.1.4 - '@smithy/core': 3.7.2 - '@smithy/eventstream-serde-browser': 4.0.4 - '@smithy/eventstream-serde-config-resolver': 4.1.2 - '@smithy/eventstream-serde-node': 4.0.4 - '@smithy/fetch-http-handler': 5.1.0 - '@smithy/hash-blob-browser': 4.0.4 - '@smithy/hash-node': 4.0.4 - '@smithy/hash-stream-node': 4.0.4 - '@smithy/invalid-dependency': 4.0.4 - '@smithy/md5-js': 4.0.4 - '@smithy/middleware-content-length': 4.0.4 - '@smithy/middleware-endpoint': 4.1.17 - '@smithy/middleware-retry': 4.1.18 - '@smithy/middleware-serde': 4.0.8 - '@smithy/middleware-stack': 4.0.4 - '@smithy/node-config-provider': 4.1.3 - '@smithy/node-http-handler': 4.1.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - '@smithy/url-parser': 4.0.4 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.25 - '@smithy/util-defaults-mode-node': 4.0.25 - '@smithy/util-endpoints': 3.0.6 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-retry': 4.0.6 - '@smithy/util-stream': 4.2.3 - '@smithy/util-utf8': 4.0.0 - '@smithy/util-waiter': 4.0.6 - '@types/uuid': 9.0.8 - tslib: 2.8.1 - uuid: 9.0.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-sso@3.856.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.856.0 - '@aws-sdk/middleware-host-header': 3.840.0 - '@aws-sdk/middleware-logger': 3.840.0 - '@aws-sdk/middleware-recursion-detection': 3.840.0 - '@aws-sdk/middleware-user-agent': 3.856.0 - '@aws-sdk/region-config-resolver': 3.840.0 - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-endpoints': 3.848.0 - '@aws-sdk/util-user-agent-browser': 3.840.0 - '@aws-sdk/util-user-agent-node': 3.856.0 - '@smithy/config-resolver': 4.1.4 - '@smithy/core': 3.7.2 - '@smithy/fetch-http-handler': 5.1.0 - '@smithy/hash-node': 4.0.4 - '@smithy/invalid-dependency': 4.0.4 - '@smithy/middleware-content-length': 4.0.4 - '@smithy/middleware-endpoint': 4.1.17 - '@smithy/middleware-retry': 4.1.18 - '@smithy/middleware-serde': 4.0.8 - '@smithy/middleware-stack': 4.0.4 - '@smithy/node-config-provider': 4.1.3 - '@smithy/node-http-handler': 4.1.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - '@smithy/url-parser': 4.0.4 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.25 - '@smithy/util-defaults-mode-node': 4.0.25 - '@smithy/util-endpoints': 3.0.6 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-retry': 4.0.6 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.856.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@aws-sdk/xml-builder': 3.821.0 - '@smithy/core': 3.7.2 - '@smithy/node-config-provider': 4.1.3 - '@smithy/property-provider': 4.0.4 - '@smithy/protocol-http': 5.1.2 - '@smithy/signature-v4': 5.1.2 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-utf8': 4.0.0 - fast-xml-parser: 5.2.5 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/property-provider': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/fetch-http-handler': 5.1.0 - '@smithy/node-http-handler': 4.1.0 - '@smithy/property-provider': 4.0.4 - '@smithy/protocol-http': 5.1.2 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - '@smithy/util-stream': 4.2.3 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/credential-provider-env': 3.856.0 - '@aws-sdk/credential-provider-http': 3.856.0 - '@aws-sdk/credential-provider-process': 3.856.0 - '@aws-sdk/credential-provider-sso': 3.856.0 - '@aws-sdk/credential-provider-web-identity': 3.856.0 - '@aws-sdk/nested-clients': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/credential-provider-imds': 4.0.6 - '@smithy/property-provider': 4.0.4 - '@smithy/shared-ini-file-loader': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.856.0': - dependencies: - '@aws-sdk/credential-provider-env': 3.856.0 - '@aws-sdk/credential-provider-http': 3.856.0 - '@aws-sdk/credential-provider-ini': 3.856.0 - '@aws-sdk/credential-provider-process': 3.856.0 - '@aws-sdk/credential-provider-sso': 3.856.0 - '@aws-sdk/credential-provider-web-identity': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/credential-provider-imds': 4.0.6 - '@smithy/property-provider': 4.0.4 - '@smithy/shared-ini-file-loader': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-process@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/property-provider': 4.0.4 - '@smithy/shared-ini-file-loader': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.856.0': - dependencies: - '@aws-sdk/client-sso': 3.856.0 - '@aws-sdk/core': 3.856.0 - '@aws-sdk/token-providers': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/property-provider': 4.0.4 - '@smithy/shared-ini-file-loader': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-web-identity@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/nested-clients': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/property-provider': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/lib-storage@3.856.0(@aws-sdk/client-s3@3.856.0)': - dependencies: - '@aws-sdk/client-s3': 3.856.0 - '@smithy/abort-controller': 4.0.4 - '@smithy/middleware-endpoint': 4.1.17 - '@smithy/smithy-client': 4.4.9 - buffer: 5.6.0 - events: 3.3.0 - stream-browserify: 3.0.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-bucket-endpoint@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-arn-parser': 3.804.0 - '@smithy/node-config-provider': 4.1.3 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - '@smithy/util-config-provider': 4.0.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-expect-continue@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-flexible-checksums@3.856.0': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@aws-crypto/crc32c': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/is-array-buffer': 4.0.0 - '@smithy/node-config-provider': 4.1.3 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-stream': 4.2.3 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-host-header@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-location-constraint@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-logger@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-recursion-detection@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-sdk-s3@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-arn-parser': 3.804.0 - '@smithy/core': 3.7.2 - '@smithy/node-config-provider': 4.1.3 - '@smithy/protocol-http': 5.1.2 - '@smithy/signature-v4': 5.1.2 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - '@smithy/util-config-provider': 4.0.0 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-stream': 4.2.3 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-ssec@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-user-agent@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-endpoints': 3.848.0 - '@smithy/core': 3.7.2 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.856.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.856.0 - '@aws-sdk/middleware-host-header': 3.840.0 - '@aws-sdk/middleware-logger': 3.840.0 - '@aws-sdk/middleware-recursion-detection': 3.840.0 - '@aws-sdk/middleware-user-agent': 3.856.0 - '@aws-sdk/region-config-resolver': 3.840.0 - '@aws-sdk/types': 3.840.0 - '@aws-sdk/util-endpoints': 3.848.0 - '@aws-sdk/util-user-agent-browser': 3.840.0 - '@aws-sdk/util-user-agent-node': 3.856.0 - '@smithy/config-resolver': 4.1.4 - '@smithy/core': 3.7.2 - '@smithy/fetch-http-handler': 5.1.0 - '@smithy/hash-node': 4.0.4 - '@smithy/invalid-dependency': 4.0.4 - '@smithy/middleware-content-length': 4.0.4 - '@smithy/middleware-endpoint': 4.1.17 - '@smithy/middleware-retry': 4.1.18 - '@smithy/middleware-serde': 4.0.8 - '@smithy/middleware-stack': 4.0.4 - '@smithy/node-config-provider': 4.1.3 - '@smithy/node-http-handler': 4.1.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - '@smithy/url-parser': 4.0.4 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-body-length-node': 4.0.0 - '@smithy/util-defaults-mode-browser': 4.0.25 - '@smithy/util-defaults-mode-node': 4.0.25 - '@smithy/util-endpoints': 3.0.6 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-retry': 4.0.6 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/region-config-resolver@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/node-config-provider': 4.1.3 - '@smithy/types': 4.3.1 - '@smithy/util-config-provider': 4.0.0 - '@smithy/util-middleware': 4.0.4 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.856.0': - dependencies: - '@aws-sdk/middleware-sdk-s3': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/signature-v4': 5.1.2 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.856.0': - dependencies: - '@aws-sdk/core': 3.856.0 - '@aws-sdk/nested-clients': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/property-provider': 4.0.4 - '@smithy/shared-ini-file-loader': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.840.0': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/util-arn-parser@3.804.0': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.848.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/types': 4.3.1 - '@smithy/url-parser': 4.0.4 - '@smithy/util-endpoints': 3.0.6 - tslib: 2.8.1 - - '@aws-sdk/util-locate-window@3.804.0': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-browser@3.840.0': - dependencies: - '@aws-sdk/types': 3.840.0 - '@smithy/types': 4.3.1 - bowser: 2.11.0 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.856.0': - dependencies: - '@aws-sdk/middleware-user-agent': 3.856.0 - '@aws-sdk/types': 3.840.0 - '@smithy/node-config-provider': 4.1.3 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.821.0': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@babel/runtime@7.28.2': {} + '@babel/runtime@7.27.6': {} '@changesets/apply-release-plan@7.0.12': dependencies: @@ -3076,248 +1700,162 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@dimforge/rapier3d-compat@0.12.0': {} - '@esbuild/aix-ppc64@0.21.5': optional: true '@esbuild/aix-ppc64@0.25.5': optional: true - '@esbuild/aix-ppc64@0.25.8': - optional: true - '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.25.5': optional: true - '@esbuild/android-arm64@0.25.8': - optional: true - '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.25.5': optional: true - '@esbuild/android-arm@0.25.8': - optional: true - '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.25.5': optional: true - '@esbuild/android-x64@0.25.8': - optional: true - '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.25.5': optional: true - '@esbuild/darwin-arm64@0.25.8': - optional: true - '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.25.5': optional: true - '@esbuild/darwin-x64@0.25.8': - optional: true - '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.25.5': optional: true - '@esbuild/freebsd-arm64@0.25.8': - optional: true - '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.25.5': optional: true - '@esbuild/freebsd-x64@0.25.8': - optional: true - '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.25.5': optional: true - '@esbuild/linux-arm64@0.25.8': - optional: true - '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.25.5': optional: true - '@esbuild/linux-arm@0.25.8': - optional: true - '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.25.5': optional: true - '@esbuild/linux-ia32@0.25.8': - optional: true - '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.25.5': optional: true - '@esbuild/linux-loong64@0.25.8': - optional: true - '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.25.5': optional: true - '@esbuild/linux-mips64el@0.25.8': - optional: true - '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.25.5': optional: true - '@esbuild/linux-ppc64@0.25.8': - optional: true - '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.25.5': optional: true - '@esbuild/linux-riscv64@0.25.8': - optional: true - '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.25.5': optional: true - '@esbuild/linux-s390x@0.25.8': - optional: true - '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.25.5': optional: true - '@esbuild/linux-x64@0.25.8': - optional: true - '@esbuild/netbsd-arm64@0.25.5': optional: true - '@esbuild/netbsd-arm64@0.25.8': - optional: true - '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.25.5': optional: true - '@esbuild/netbsd-x64@0.25.8': - optional: true - '@esbuild/openbsd-arm64@0.25.5': optional: true - '@esbuild/openbsd-arm64@0.25.8': - optional: true - '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.25.5': optional: true - '@esbuild/openbsd-x64@0.25.8': - optional: true - - '@esbuild/openharmony-arm64@0.25.8': - optional: true - '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.25.5': optional: true - '@esbuild/sunos-x64@0.25.8': - optional: true - '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.25.5': optional: true - '@esbuild/win32-arm64@0.25.8': - optional: true - '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.25.5': optional: true - '@esbuild/win32-ia32@0.25.8': - optional: true - '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.25.5': optional: true - '@esbuild/win32-x64@0.25.8': - optional: true - - '@huggingface/hub@2.4.0': - dependencies: - '@huggingface/tasks': 0.19.34 - - '@huggingface/tasks@0.19.34': {} - '@jridgewell/sourcemap-codec@1.5.0': {} '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.27.6 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.28.2 + '@babel/runtime': 7.27.6 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -3336,43 +1874,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@pmndrs/cannon-worker-api@2.4.0(three@0.178.0)': - dependencies: - three: 0.178.0 - '@polka/url@1.0.0-next.29': {} - '@react-three/cannon@6.6.0(@react-three/fiber@9.2.0(@types/react@19.1.8)(react@19.1.0)(three@0.178.0))(react@19.1.0)(three@0.178.0)(typescript@5.8.3)': - dependencies: - '@pmndrs/cannon-worker-api': 2.4.0(three@0.178.0) - '@react-three/fiber': 9.2.0(@types/react@19.1.8)(react@19.1.0)(three@0.178.0) - cannon-es: 0.20.0 - cannon-es-debugger: 1.0.0(cannon-es@0.20.0)(three@0.178.0)(typescript@5.8.3) - react: 19.1.0 - three: 0.178.0 - transitivePeerDependencies: - - typescript - - '@react-three/fiber@9.2.0(@types/react@19.1.8)(react@19.1.0)(three@0.178.0)': - dependencies: - '@babel/runtime': 7.28.2 - '@types/react-reconciler': 0.28.9(@types/react@19.1.8) - '@types/webxr': 0.5.22 - base64-js: 1.5.1 - buffer: 6.0.3 - its-fine: 2.0.0(@types/react@19.1.8)(react@19.1.0) - react: 19.1.0 - react-reconciler: 0.31.0(react@19.1.0) - react-use-measure: 2.1.7(react@19.1.0) - scheduler: 0.25.0 - suspend-react: 0.1.3(react@19.1.0) - three: 0.178.0 - use-sync-external-store: 1.5.0(react@19.1.0) - zustand: 5.0.6(@types/react@19.1.8)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) - transitivePeerDependencies: - - '@types/react' - - immer - '@rollup/rollup-android-arm-eabi@4.43.0': optional: true @@ -3427,450 +1930,78 @@ snapshots: '@rollup/rollup-win32-arm64-msvc@4.43.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.43.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.43.0': - optional: true - - '@serialport/binding-mock@10.2.2': - dependencies: - '@serialport/bindings-interface': 1.2.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - '@serialport/bindings-cpp@12.0.1': - dependencies: - '@serialport/bindings-interface': 1.2.2 - '@serialport/parser-readline': 11.0.0 - debug: 4.3.4 - node-addon-api: 7.0.0 - node-gyp-build: 4.6.0 - transitivePeerDependencies: - - supports-color - - '@serialport/bindings-interface@1.2.2': {} - - '@serialport/parser-byte-length@12.0.0': {} - - '@serialport/parser-cctalk@12.0.0': {} - - '@serialport/parser-delimiter@11.0.0': {} - - '@serialport/parser-delimiter@12.0.0': {} - - '@serialport/parser-inter-byte-timeout@12.0.0': {} - - '@serialport/parser-packet-length@12.0.0': {} - - '@serialport/parser-readline@11.0.0': - dependencies: - '@serialport/parser-delimiter': 11.0.0 - - '@serialport/parser-readline@12.0.0': - dependencies: - '@serialport/parser-delimiter': 12.0.0 - - '@serialport/parser-ready@12.0.0': {} - - '@serialport/parser-regex@12.0.0': {} - - '@serialport/parser-slip-encoder@12.0.0': {} - - '@serialport/parser-spacepacket@12.0.0': {} - - '@serialport/stream@12.0.0': - dependencies: - '@serialport/bindings-interface': 1.2.2 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - - '@smithy/abort-controller@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/chunked-blob-reader-native@4.0.0': - dependencies: - '@smithy/util-base64': 4.0.0 - tslib: 2.8.1 - - '@smithy/chunked-blob-reader@5.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/config-resolver@4.1.4': - dependencies: - '@smithy/node-config-provider': 4.1.3 - '@smithy/types': 4.3.1 - '@smithy/util-config-provider': 4.0.0 - '@smithy/util-middleware': 4.0.4 - tslib: 2.8.1 - - '@smithy/core@3.7.2': - dependencies: - '@smithy/middleware-serde': 4.0.8 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - '@smithy/util-base64': 4.0.0 - '@smithy/util-body-length-browser': 4.0.0 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-stream': 4.2.3 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.0.6': - dependencies: - '@smithy/node-config-provider': 4.1.3 - '@smithy/property-provider': 4.0.4 - '@smithy/types': 4.3.1 - '@smithy/url-parser': 4.0.4 - tslib: 2.8.1 - - '@smithy/eventstream-codec@4.0.4': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.3.1 - '@smithy/util-hex-encoding': 4.0.0 - tslib: 2.8.1 - - '@smithy/eventstream-serde-browser@4.0.4': - dependencies: - '@smithy/eventstream-serde-universal': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-config-resolver@4.1.2': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-node@4.0.4': - dependencies: - '@smithy/eventstream-serde-universal': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-universal@4.0.4': - dependencies: - '@smithy/eventstream-codec': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.1.0': - dependencies: - '@smithy/protocol-http': 5.1.2 - '@smithy/querystring-builder': 4.0.4 - '@smithy/types': 4.3.1 - '@smithy/util-base64': 4.0.0 - tslib: 2.8.1 - - '@smithy/hash-blob-browser@4.0.4': - dependencies: - '@smithy/chunked-blob-reader': 5.0.0 - '@smithy/chunked-blob-reader-native': 4.0.0 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/hash-node@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - '@smithy/util-buffer-from': 4.0.0 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@smithy/hash-stream-node@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/is-array-buffer@4.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/md5-js@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.0.4': - dependencies: - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@4.1.17': - dependencies: - '@smithy/core': 3.7.2 - '@smithy/middleware-serde': 4.0.8 - '@smithy/node-config-provider': 4.1.3 - '@smithy/shared-ini-file-loader': 4.0.4 - '@smithy/types': 4.3.1 - '@smithy/url-parser': 4.0.4 - '@smithy/util-middleware': 4.0.4 - tslib: 2.8.1 - - '@smithy/middleware-retry@4.1.18': - dependencies: - '@smithy/node-config-provider': 4.1.3 - '@smithy/protocol-http': 5.1.2 - '@smithy/service-error-classification': 4.0.6 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-retry': 4.0.6 - tslib: 2.8.1 - uuid: 9.0.1 - - '@smithy/middleware-serde@4.0.8': - dependencies: - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/middleware-stack@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/node-config-provider@4.1.3': - dependencies: - '@smithy/property-provider': 4.0.4 - '@smithy/shared-ini-file-loader': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.1.0': - dependencies: - '@smithy/abort-controller': 4.0.4 - '@smithy/protocol-http': 5.1.2 - '@smithy/querystring-builder': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/property-provider@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/protocol-http@5.1.2': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/querystring-builder@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - '@smithy/util-uri-escape': 4.0.0 - tslib: 2.8.1 - - '@smithy/querystring-parser@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/service-error-classification@4.0.6': - dependencies: - '@smithy/types': 4.3.1 - - '@smithy/shared-ini-file-loader@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/signature-v4@5.1.2': - dependencies: - '@smithy/is-array-buffer': 4.0.0 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - '@smithy/util-hex-encoding': 4.0.0 - '@smithy/util-middleware': 4.0.4 - '@smithy/util-uri-escape': 4.0.0 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@smithy/smithy-client@4.4.9': - dependencies: - '@smithy/core': 3.7.2 - '@smithy/middleware-endpoint': 4.1.17 - '@smithy/middleware-stack': 4.0.4 - '@smithy/protocol-http': 5.1.2 - '@smithy/types': 4.3.1 - '@smithy/util-stream': 4.2.3 - tslib: 2.8.1 - - '@smithy/types@4.3.1': - dependencies: - tslib: 2.8.1 - - '@smithy/url-parser@4.0.4': - dependencies: - '@smithy/querystring-parser': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/util-base64@4.0.0': - dependencies: - '@smithy/util-buffer-from': 4.0.0 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@smithy/util-body-length-browser@4.0.0': - dependencies: - tslib: 2.8.1 + '@rollup/rollup-win32-ia32-msvc@4.43.0': + optional: true - '@smithy/util-body-length-node@4.0.0': - dependencies: - tslib: 2.8.1 + '@rollup/rollup-win32-x64-msvc@4.43.0': + optional: true - '@smithy/util-buffer-from@2.2.0': + '@serialport/binding-mock@10.2.2': dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color - '@smithy/util-buffer-from@4.0.0': + '@serialport/bindings-cpp@12.0.1': dependencies: - '@smithy/is-array-buffer': 4.0.0 - tslib: 2.8.1 + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color - '@smithy/util-config-provider@4.0.0': - dependencies: - tslib: 2.8.1 + '@serialport/bindings-interface@1.2.2': {} - '@smithy/util-defaults-mode-browser@4.0.25': - dependencies: - '@smithy/property-provider': 4.0.4 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - bowser: 2.11.0 - tslib: 2.8.1 + '@serialport/parser-byte-length@12.0.0': {} - '@smithy/util-defaults-mode-node@4.0.25': - dependencies: - '@smithy/config-resolver': 4.1.4 - '@smithy/credential-provider-imds': 4.0.6 - '@smithy/node-config-provider': 4.1.3 - '@smithy/property-provider': 4.0.4 - '@smithy/smithy-client': 4.4.9 - '@smithy/types': 4.3.1 - tslib: 2.8.1 - - '@smithy/util-endpoints@3.0.6': - dependencies: - '@smithy/node-config-provider': 4.1.3 - '@smithy/types': 4.3.1 - tslib: 2.8.1 + '@serialport/parser-cctalk@12.0.0': {} - '@smithy/util-hex-encoding@4.0.0': - dependencies: - tslib: 2.8.1 + '@serialport/parser-delimiter@11.0.0': {} - '@smithy/util-middleware@4.0.4': - dependencies: - '@smithy/types': 4.3.1 - tslib: 2.8.1 + '@serialport/parser-delimiter@12.0.0': {} - '@smithy/util-retry@4.0.6': - dependencies: - '@smithy/service-error-classification': 4.0.6 - '@smithy/types': 4.3.1 - tslib: 2.8.1 + '@serialport/parser-inter-byte-timeout@12.0.0': {} - '@smithy/util-stream@4.2.3': - dependencies: - '@smithy/fetch-http-handler': 5.1.0 - '@smithy/node-http-handler': 4.1.0 - '@smithy/types': 4.3.1 - '@smithy/util-base64': 4.0.0 - '@smithy/util-buffer-from': 4.0.0 - '@smithy/util-hex-encoding': 4.0.0 - '@smithy/util-utf8': 4.0.0 - tslib: 2.8.1 - - '@smithy/util-uri-escape@4.0.0': - dependencies: - tslib: 2.8.1 + '@serialport/parser-packet-length@12.0.0': {} - '@smithy/util-utf8@2.3.0': + '@serialport/parser-readline@11.0.0': dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 + '@serialport/parser-delimiter': 11.0.0 - '@smithy/util-utf8@4.0.0': + '@serialport/parser-readline@12.0.0': dependencies: - '@smithy/util-buffer-from': 4.0.0 - tslib: 2.8.1 + '@serialport/parser-delimiter': 12.0.0 - '@smithy/util-waiter@4.0.6': - dependencies: - '@smithy/abort-controller': 4.0.4 - '@smithy/types': 4.3.1 - tslib: 2.8.1 + '@serialport/parser-ready@12.0.0': {} - '@swc/helpers@0.5.17': - dependencies: - tslib: 2.8.1 + '@serialport/parser-regex@12.0.0': {} - '@tweenjs/tween.js@23.1.3': {} + '@serialport/parser-slip-encoder@12.0.0': {} - '@types/command-line-args@5.2.3': {} + '@serialport/parser-spacepacket@12.0.0': {} - '@types/command-line-usage@5.0.4': {} + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color '@types/estree@1.0.7': {} '@types/node@12.20.55': {} - '@types/node@22.15.31': + '@types/node@18.19.123': dependencies: - undici-types: 6.21.0 - optional: true + undici-types: 5.26.5 - '@types/node@22.17.0': + '@types/node@22.15.31': dependencies: undici-types: 6.21.0 - '@types/node@24.1.0': - dependencies: - undici-types: 7.8.0 - - '@types/react-reconciler@0.28.9(@types/react@19.1.8)': - dependencies: - '@types/react': 19.1.8 - - '@types/react@19.1.8': - dependencies: - csstype: 3.1.3 - - '@types/stats.js@0.17.4': {} - - '@types/three@0.178.1': - dependencies: - '@dimforge/rapier3d-compat': 0.12.0 - '@tweenjs/tween.js': 23.1.3 - '@types/stats.js': 0.17.4 - '@types/webxr': 0.5.22 - '@webgpu/types': 0.1.64 - fflate: 0.8.2 - meshoptimizer: 0.18.1 - - '@types/uuid@9.0.8': {} - - '@types/webxr@0.5.22': {} - '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -3878,6 +2009,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 1.2.0 + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@18.19.123))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@18.19.123) + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.15.31))': dependencies: '@vitest/spy': 2.1.9 @@ -3914,7 +2053,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.15.31)(@vitest/ui@2.1.9)(jsdom@24.1.3) + vitest: 2.1.9(@types/node@18.19.123)(@vitest/ui@2.1.9)(jsdom@24.1.3) '@vitest/utils@2.1.9': dependencies: @@ -3922,74 +2061,30 @@ snapshots: loupe: 3.2.0 tinyrainbow: 1.2.0 - '@webgpu/types@0.1.64': {} - agent-base@7.1.4: {} ansi-colors@4.1.3: {} - ansi-escapes@7.0.0: - dependencies: - environment: 1.1.0 - ansi-regex@5.0.1: {} - ansi-regex@6.1.0: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - - apache-arrow@21.0.0: - dependencies: - '@swc/helpers': 0.5.17 - '@types/command-line-args': 5.2.3 - '@types/command-line-usage': 5.0.4 - '@types/node': 24.1.0 - command-line-args: 6.0.1 - command-line-usage: 7.0.3 - flatbuffers: 25.2.10 - json-bignum: 0.0.3 - tslib: 2.8.1 - transitivePeerDependencies: - - '@75lb/nature' - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 - array-back@6.2.2: {} - array-union@2.1.0: {} assertion-error@2.0.1: {} asynckit@0.4.0: {} - base64-js@1.5.1: {} - better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 - bowser@2.11.0: {} - braces@3.0.3: dependencies: fill-range: 7.1.1 - buffer@5.6.0: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -3997,15 +2092,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - cannon-es-debugger@1.0.0(cannon-es@0.20.0)(three@0.178.0)(typescript@5.8.3): - dependencies: - cannon-es: 0.20.0 - three: 0.178.0 - optionalDependencies: - typescript: 5.8.3 - - cannon-es@0.20.0: {} - chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -4014,14 +2100,7 @@ snapshots: loupe: 3.2.0 pathval: 2.0.1 - chalk-template@0.4.0: - dependencies: - chalk: 4.1.2 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 + chalk@5.6.0: {} chardet@0.7.0: {} @@ -4029,35 +2108,11 @@ snapshots: ci-info@3.9.0: {} - cli-cursor@5.0.0: - dependencies: - restore-cursor: 5.1.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - command-line-args@6.0.1: - dependencies: - array-back: 6.2.2 - find-replace: 5.0.2 - lodash.camelcase: 4.3.0 - typical: 7.3.0 - - command-line-usage@7.0.3: - dependencies: - array-back: 6.2.2 - chalk-template: 0.4.0 - table-layout: 4.1.1 - typical: 7.3.0 - - core-util-is@1.0.3: {} + commander@11.1.0: {} cross-spawn@7.0.6: dependencies: @@ -4070,8 +2125,6 @@ snapshots: '@asamuzakjp/css-color': 3.2.0 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} - data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -4103,8 +2156,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - emoji-regex@10.4.0: {} - enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -4112,8 +2163,6 @@ snapshots: entities@6.0.1: {} - environment@1.1.0: {} - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -4185,43 +2234,12 @@ snapshots: '@esbuild/win32-ia32': 0.25.5 '@esbuild/win32-x64': 0.25.5 - esbuild@0.25.8: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.8 - '@esbuild/android-arm': 0.25.8 - '@esbuild/android-arm64': 0.25.8 - '@esbuild/android-x64': 0.25.8 - '@esbuild/darwin-arm64': 0.25.8 - '@esbuild/darwin-x64': 0.25.8 - '@esbuild/freebsd-arm64': 0.25.8 - '@esbuild/freebsd-x64': 0.25.8 - '@esbuild/linux-arm': 0.25.8 - '@esbuild/linux-arm64': 0.25.8 - '@esbuild/linux-ia32': 0.25.8 - '@esbuild/linux-loong64': 0.25.8 - '@esbuild/linux-mips64el': 0.25.8 - '@esbuild/linux-ppc64': 0.25.8 - '@esbuild/linux-riscv64': 0.25.8 - '@esbuild/linux-s390x': 0.25.8 - '@esbuild/linux-x64': 0.25.8 - '@esbuild/netbsd-arm64': 0.25.8 - '@esbuild/netbsd-x64': 0.25.8 - '@esbuild/openbsd-arm64': 0.25.8 - '@esbuild/openbsd-x64': 0.25.8 - '@esbuild/openharmony-arm64': 0.25.8 - '@esbuild/sunos-x64': 0.25.8 - '@esbuild/win32-arm64': 0.25.8 - '@esbuild/win32-ia32': 0.25.8 - '@esbuild/win32-x64': 0.25.8 - esprima@4.0.1: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.7 - events@3.3.0: {} - expect-type@1.2.2: {} extendable-error@0.1.7: {} @@ -4240,10 +2258,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-xml-parser@5.2.5: - dependencies: - strnum: 2.1.1 - fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -4258,15 +2272,11 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-replace@5.0.2: {} - find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - flatbuffers@25.2.10: {} - flatted@3.3.3: {} form-data@4.0.4: @@ -4294,8 +2304,6 @@ snapshots: function-bind@1.1.2: {} - get-east-asian-width@1.3.0: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4317,6 +2325,7 @@ snapshots: get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 + optional: true glob-parent@5.1.2: dependencies: @@ -4335,8 +2344,6 @@ snapshots: graceful-fs@4.2.11: {} - has-flag@4.0.0: {} - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -4375,20 +2382,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} - ignore@5.3.2: {} - immediate@3.0.6: {} - - inherits@2.0.4: {} - is-extglob@2.1.1: {} - is-fullwidth-code-point@5.0.0: - dependencies: - get-east-asian-width: 1.3.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4403,17 +2400,8 @@ snapshots: is-windows@1.0.2: {} - isarray@1.0.0: {} - isexe@2.0.0: {} - its-fine@2.0.0(@types/react@19.1.8)(react@19.1.0): - dependencies: - '@types/react-reconciler': 0.28.9(@types/react@19.1.8) - react: 19.1.0 - transitivePeerDependencies: - - '@types/react' - jiti@1.21.7: optional: true @@ -4450,39 +2438,16 @@ snapshots: - supports-color - utf-8-validate - json-bignum@0.0.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - jszip@3.10.1: - dependencies: - lie: 3.3.0 - pako: 1.0.11 - readable-stream: 2.3.8 - setimmediate: 1.0.5 - - lie@3.3.0: - dependencies: - immediate: 3.0.6 - locate-path@5.0.0: dependencies: p-locate: 4.1.0 - lodash.camelcase@4.3.0: {} - lodash.startcase@4.4.0: {} - log-update@6.1.0: - dependencies: - ansi-escapes: 7.0.0 - cli-cursor: 5.0.0 - slice-ansi: 7.1.0 - strip-ansi: 7.1.0 - wrap-ansi: 9.0.0 - loupe@3.2.0: {} lru-cache@10.4.3: {} @@ -4495,8 +2460,6 @@ snapshots: merge2@1.4.1: {} - meshoptimizer@0.18.1: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -4508,8 +2471,6 @@ snapshots: dependencies: mime-db: 1.52.0 - mimic-function@5.0.1: {} - mri@1.2.0: {} mrmime@2.0.1: {} @@ -4526,10 +2487,6 @@ snapshots: nwsapi@2.2.20: {} - onetime@7.0.0: - dependencies: - mimic-function: 5.0.1 - os-tmpdir@1.0.2: {} outdent@0.5.0: {} @@ -4554,10 +2511,6 @@ snapshots: dependencies: quansync: 0.2.10 - pako@1.0.11: {} - - parquet-wasm@0.6.1: {} - parse5@7.3.0: dependencies: entities: 6.0.1 @@ -4588,8 +2541,6 @@ snapshots: prettier@2.8.8: {} - process-nextick-args@2.0.1: {} - psl@1.15.0: dependencies: punycode: 2.3.1 @@ -4602,17 +2553,6 @@ snapshots: queue-microtask@1.2.3: {} - react-reconciler@0.31.0(react@19.1.0): - dependencies: - react: 19.1.0 - scheduler: 0.25.0 - - react-use-measure@2.1.7(react@19.1.0): - dependencies: - react: 19.1.0 - - react@19.1.0: {} - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -4620,32 +2560,12 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - requires-port@1.0.0: {} resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - - restore-cursor@5.1.0: - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 + resolve-pkg-maps@1.0.0: + optional: true reusify@1.1.0: {} @@ -4683,18 +2603,12 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - safer-buffer@2.1.2: {} saxes@6.0.0: dependencies: xmlchars: 2.2.0 - scheduler@0.25.0: {} - semver@7.7.2: {} serialport@12.0.0: @@ -4716,8 +2630,6 @@ snapshots: transitivePeerDependencies: - supports-color - setimmediate@1.0.5: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -4736,11 +2648,6 @@ snapshots: slash@3.0.0: {} - slice-ansi@7.1.0: - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 5.0.0 - source-map-js@1.2.1: {} spawndamnit@3.0.1: @@ -4754,56 +2661,16 @@ snapshots: std-env@3.9.0: {} - stream-browserify@3.0.0: - dependencies: - inherits: 2.0.4 - readable-stream: 3.6.2 - - string-width@7.2.0: - dependencies: - emoji-regex: 10.4.0 - get-east-asian-width: 1.3.0 - strip-ansi: 7.1.0 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.1.0 - strip-bom@3.0.0: {} - strnum@2.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - suspend-react@0.1.3(react@19.1.0): - dependencies: - react: 19.1.0 - symbol-tree@3.2.4: {} - table-layout@4.1.1: - dependencies: - array-back: 6.2.2 - wordwrapjs: 5.1.0 - term-size@2.2.1: {} - three@0.178.0: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -4840,23 +2707,20 @@ snapshots: dependencies: punycode: 2.3.1 - tslib@2.8.1: {} - tsx@4.20.3: dependencies: - esbuild: 0.25.8 + esbuild: 0.25.5 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 + optional: true typescript@5.8.3: {} - typical@7.3.0: {} + undici-types@5.26.5: {} undici-types@6.21.0: {} - undici-types@7.8.0: {} - universalify@0.1.2: {} universalify@0.2.0: {} @@ -4866,13 +2730,23 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - use-sync-external-store@1.5.0(react@19.1.0): + vite-node@2.1.9(@types/node@18.19.123): dependencies: - react: 19.1.0 - - util-deprecate@1.0.2: {} - - uuid@9.0.1: {} + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@18.19.123) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser vite-node@2.1.9(@types/node@22.15.31): dependencies: @@ -4892,6 +2766,15 @@ snapshots: - supports-color - terser + vite@5.4.19(@types/node@18.19.123): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.5 + rollup: 4.43.0 + optionalDependencies: + '@types/node': 18.19.123 + fsevents: 2.3.3 + vite@5.4.19(@types/node@22.15.31): dependencies: esbuild: 0.21.5 @@ -4901,6 +2784,21 @@ snapshots: '@types/node': 22.15.31 fsevents: 2.3.3 + vite@6.3.5(@types/node@18.19.123)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.5 + rollup: 4.43.0 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 18.19.123 + fsevents: 2.3.3 + jiti: 1.21.7 + tsx: 4.20.3 + yaml: 2.8.0 + vite@6.3.5(@types/node@22.15.31)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 @@ -4916,6 +2814,43 @@ snapshots: tsx: 4.20.3 yaml: 2.8.0 + vitest@2.1.9(@types/node@18.19.123)(@vitest/ui@2.1.9)(jsdom@24.1.3): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@18.19.123)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@18.19.123) + vite-node: 2.1.9(@types/node@18.19.123) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 18.19.123 + '@vitest/ui': 2.1.9(vitest@2.1.9) + jsdom: 24.1.3 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@2.1.9(@types/node@22.15.31)(@vitest/ui@2.1.9)(jsdom@24.1.3): dependencies: '@vitest/expect': 2.1.9 @@ -4979,14 +2914,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wordwrapjs@5.1.0: {} - - wrap-ansi@9.0.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 7.2.0 - strip-ansi: 7.1.0 - ws@8.18.3: {} xml-name-validator@5.0.0: {} @@ -4995,9 +2922,3 @@ snapshots: yaml@2.8.0: optional: true - - zustand@5.0.6(@types/react@19.1.8)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): - optionalDependencies: - '@types/react': 19.1.8 - react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100644 index 2b95aa2ddb806030b1f4d01a486aede4f40aa63d..0000000000000000000000000000000000000000 --- a/src/cli/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env node - -/** - * lerobot.js CLI - * - * Provides command-line interface for lerobot functionality - * Maintains compatibility with Python lerobot command structure - */ - -import { findPort } from "../lerobot/node/find_port.js"; -import { main as calibrateMain } from "../lerobot/node/calibrate.js"; -import { main as teleoperateMain } from "../lerobot/node/teleoperate.js"; - -/** - * Show usage information - */ -function showUsage() { - console.log("Usage: lerobot "); - console.log(""); - console.log("Commands:"); - console.log( - " find-port Find the USB port associated with your MotorsBus" - ); - console.log(" calibrate Recalibrate your device (robot or teleoperator)"); - console.log(" teleoperate Control a robot using keyboard input"); - console.log(""); - console.log("Examples:"); - console.log(" lerobot find-port"); - console.log( - " lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm" - ); - console.log( - " lerobot teleoperate --robot.type=so100_follower --robot.port=COM4 --teleop.type=keyboard" - ); - console.log(""); -} - -/** - * Main CLI function - */ -async function main() { - const args = process.argv.slice(2); - - if (args.length === 0) { - showUsage(); - process.exit(1); - } - - const command = args[0]; - - try { - switch (command) { - case "find-port": - await findPort(); - break; - - case "calibrate": - // Pass remaining arguments to calibrate command - const calibrateArgs = args.slice(1); - await calibrateMain(calibrateArgs); - break; - - case "teleoperate": - // Pass remaining arguments to teleoperate command - const teleoperateArgs = args.slice(1); - await teleoperateMain(teleoperateArgs); - break; - - case "help": - case "--help": - case "-h": - showUsage(); - break; - - default: - console.error(`Unknown command: ${command}`); - showUsage(); - process.exit(1); - } - } catch (error) { - console.error("Error:", error instanceof Error ? error.message : error); - process.exit(1); - } -} - -// Run the CLI -main(); diff --git a/src/lerobot/node/calibrate.ts b/src/lerobot/node/calibrate.ts deleted file mode 100644 index e718a3b1577aa3ead5d08610a9c161c892cbcf5d..0000000000000000000000000000000000000000 --- a/src/lerobot/node/calibrate.ts +++ /dev/null @@ -1,246 +0,0 @@ -/** - * Helper to recalibrate your device (robot or teleoperator). - * - * Example: - * ``` - * npx lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm - * ``` - */ - -import { createSO100Follower } from "./robots/so100_follower.js"; -import { createSO100Leader } from "./teleoperators/so100_leader.js"; -import { - initializeDeviceCommunication, - readMotorPositions, - performInteractiveCalibration, - setMotorLimits, - verifyCalibration, -} from "./common/calibration.js"; -import type { CalibrateConfig } from "./types/robot-config.js"; -import type { CalibrationResults } from "./types/calibration.js"; -import { getSO100Config } from "./common/so100_config.js"; - -/** - * Main calibrate function - * Mirrors Python lerobot calibrate.py calibrate() function - * Uses shared calibration procedures instead of device-specific implementations - */ -export async function calibrate(config: CalibrateConfig): Promise { - // Validate configuration - exactly one device must be specified - if (Boolean(config.robot) === Boolean(config.teleop)) { - throw new Error("Choose either a robot or a teleop."); - } - - const deviceConfig = config.robot || config.teleop!; - - let device; - let calibrationResults: CalibrationResults; - - try { - // Create device for connection management only - if (config.robot) { - switch (config.robot.type) { - case "so100_follower": - device = createSO100Follower(config.robot); - break; - default: - throw new Error(`Unsupported robot type: ${config.robot.type}`); - } - } else if (config.teleop) { - switch (config.teleop.type) { - case "so100_leader": - device = createSO100Leader(config.teleop); - break; - default: - throw new Error( - `Unsupported teleoperator type: ${config.teleop.type}` - ); - } - } - - if (!device) { - throw new Error("Failed to create device"); - } - - // Connect to device (silent unless error) - await device.connect(false); // calibrate=False like Python - - // Get SO-100 calibration configuration - const so100Config = getSO100Config( - deviceConfig.type as "so100_follower" | "so100_leader", - (device as any).port - ); - - // Perform shared calibration procedures (silent unless error) - await initializeDeviceCommunication(so100Config); - await setMotorLimits(so100Config); - - // Interactive calibration with live updates - THE MAIN PART - calibrationResults = await performInteractiveCalibration(so100Config); - - // Save and cleanup (silent unless error) - await verifyCalibration(so100Config); - await (device as any).saveCalibration(calibrationResults); - await device.disconnect(); - } catch (error) { - // Ensure we disconnect even if there's an error - if (device) { - try { - await device.disconnect(); - } catch (disconnectError) { - console.warn("Warning: Failed to disconnect properly"); - } - } - throw error; - } -} - -/** - * Parse command line arguments in Python argparse style - * Handles --robot.type=so100_follower --robot.port=COM4 format - */ -export function parseArgs(args: string[]): CalibrateConfig { - const config: CalibrateConfig = {}; - - for (const arg of args) { - if (arg.startsWith("--robot.")) { - if (!config.robot) { - config.robot = { type: "so100_follower", port: "" }; - } - - const [key, value] = arg.substring(8).split("="); - switch (key) { - case "type": - if (value !== "so100_follower") { - throw new Error(`Unsupported robot type: ${value}`); - } - config.robot.type = value as "so100_follower"; - break; - case "port": - config.robot.port = value; - break; - case "id": - config.robot.id = value; - break; - case "disable_torque_on_disconnect": - config.robot.disable_torque_on_disconnect = value === "true"; - break; - case "max_relative_target": - config.robot.max_relative_target = value ? parseInt(value) : null; - break; - case "use_degrees": - config.robot.use_degrees = value === "true"; - break; - default: - throw new Error(`Unknown robot parameter: ${key}`); - } - } else if (arg.startsWith("--teleop.")) { - if (!config.teleop) { - config.teleop = { type: "so100_leader", port: "" }; - } - - const [key, value] = arg.substring(9).split("="); - switch (key) { - case "type": - if (value !== "so100_leader") { - throw new Error(`Unsupported teleoperator type: ${value}`); - } - config.teleop.type = value as "so100_leader"; - break; - case "port": - config.teleop.port = value; - break; - case "id": - config.teleop.id = value; - break; - default: - throw new Error(`Unknown teleoperator parameter: ${key}`); - } - } else if (arg === "--help" || arg === "-h") { - showUsage(); - process.exit(0); - } else if (!arg.startsWith("--")) { - // Skip non-option arguments - continue; - } else { - throw new Error(`Unknown argument: ${arg}`); - } - } - - // Validate required fields - if (config.robot && !config.robot.port) { - throw new Error("Robot port is required (--robot.port=PORT)"); - } - if (config.teleop && !config.teleop.port) { - throw new Error("Teleoperator port is required (--teleop.port=PORT)"); - } - - return config; -} - -/** - * Show usage information matching Python argparse output - */ -function showUsage(): void { - console.log("Usage: lerobot calibrate [options]"); - console.log(""); - console.log("Recalibrate your device (robot or teleoperator)"); - console.log(""); - console.log("Options:"); - console.log(" --robot.type=TYPE Robot type (so100_follower)"); - console.log( - " --robot.port=PORT Robot serial port (e.g., COM4, /dev/ttyUSB0)" - ); - console.log(" --robot.id=ID Robot identifier"); - console.log(" --teleop.type=TYPE Teleoperator type (so100_leader)"); - console.log(" --teleop.port=PORT Teleoperator serial port"); - console.log(" --teleop.id=ID Teleoperator identifier"); - console.log(" -h, --help Show this help message"); - console.log(""); - console.log("Examples:"); - console.log( - " lerobot calibrate --robot.type=so100_follower --robot.port=COM4 --robot.id=my_follower_arm" - ); - console.log( - " lerobot calibrate --teleop.type=so100_leader --teleop.port=COM3 --teleop.id=my_leader_arm" - ); - console.log(""); - console.log("Use 'lerobot find-port' to discover available ports."); -} - -/** - * CLI entry point when called directly - * Mirrors Python's if __name__ == "__main__": pattern - */ -export async function main(args: string[]): Promise { - try { - if (args.length === 0 || args.includes("--help") || args.includes("-h")) { - showUsage(); - return; - } - - const config = parseArgs(args); - await calibrate(config); - } catch (error) { - if (error instanceof Error) { - console.error("Error:", error.message); - } else { - console.error("Error:", error); - } - - console.error(""); - console.error("Please verify:"); - console.error("1. The device is connected to the specified port"); - console.error("2. No other application is using the port"); - console.error("3. You have permission to access the port"); - console.error(""); - console.error("Use 'lerobot find-port' to discover available ports."); - - process.exit(1); - } -} - -if (import.meta.url === `file://${process.argv[1]}`) { - const args = process.argv.slice(2); - main(args); -} diff --git a/src/lerobot/node/common/calibration.ts b/src/lerobot/node/common/calibration.ts deleted file mode 100644 index 2cf542d4251da89e9ea45278d65ad629f65187e1..0000000000000000000000000000000000000000 --- a/src/lerobot/node/common/calibration.ts +++ /dev/null @@ -1,694 +0,0 @@ -/** - * Shared calibration procedures for SO-100 devices (both leader and follower) - * Mirrors Python lerobot calibrate.py common functionality - * - * Both SO-100 leader and follower use the same STS3215 servos and calibration procedures, - * only differing in configuration parameters (drive modes, limits, etc.) - */ - -import * as readline from "readline"; -import { SerialPort } from "serialport"; -import logUpdate from "log-update"; - -/** - * Sign-magnitude encoding functions for Feetech STS3215 motors - * Mirrors Python lerobot/common/utils/encoding_utils.py - */ - -/** - * Encode a signed integer using sign-magnitude format - * Bit at sign_bit_index represents sign (0=positive, 1=negative) - * Lower bits represent magnitude - */ -function encodeSignMagnitude(value: number, signBitIndex: number): number { - const maxMagnitude = (1 << signBitIndex) - 1; - const magnitude = Math.abs(value); - - if (magnitude > maxMagnitude) { - throw new Error( - `Magnitude ${magnitude} exceeds ${maxMagnitude} (max for signBitIndex=${signBitIndex})` - ); - } - - const directionBit = value < 0 ? 1 : 0; - return (directionBit << signBitIndex) | magnitude; -} - -/** - * Decode a sign-magnitude encoded value back to signed integer - * Extracts sign bit and magnitude, then applies sign - */ -function decodeSignMagnitude( - encodedValue: number, - signBitIndex: number -): number { - const directionBit = (encodedValue >> signBitIndex) & 1; - const magnitudeMask = (1 << signBitIndex) - 1; - const magnitude = encodedValue & magnitudeMask; - return directionBit ? -magnitude : magnitude; -} - -/** - * Device configuration for calibration - * Despite the "SO100" name, this interface is now device-agnostic and configurable - * for any robot using similar serial protocols (Feetech STS3215, etc.) - */ -import type { - SO100CalibrationConfig, - CalibrationResults, -} from "../types/calibration.js"; - -/** - * Initialize device communication - * Common for both SO-100 leader and follower (same hardware) - */ -export async function initializeDeviceCommunication( - config: SO100CalibrationConfig -): Promise { - try { - // Test ping to servo ID 1 (same protocol for all SO-100 devices) - const pingPacket = Buffer.from([0xff, 0xff, 0x01, 0x02, 0x01, 0xfb]); - - if (!config.port || !config.port.isOpen) { - throw new Error("Serial port not open"); - } - - await new Promise((resolve, reject) => { - config.port.write(pingPacket, (error) => { - if (error) { - reject(new Error(`Failed to send ping: ${error.message}`)); - } else { - resolve(); - } - }); - }); - - try { - await readData(config.port, 1000); - } catch (error) { - // Silent - no response expected for basic test - } - } catch (error) { - throw new Error( - `Serial communication test failed: ${ - error instanceof Error ? error.message : error - }` - ); - } -} - -/** - * Read current motor positions - * Uses device-specific protocol - configurable for different robot types - */ -export async function readMotorPositions( - config: SO100CalibrationConfig, - quiet: boolean = false -): Promise { - const motorPositions: number[] = []; - - for (let i = 0; i < config.motorIds.length; i++) { - const motorId = config.motorIds[i]; - const motorName = config.motorNames[i]; - - try { - // Create Read Position packet using configurable address - const packet = Buffer.from([ - 0xff, - 0xff, - motorId, - 0x04, - 0x02, - config.protocol.presentPositionAddress, // Configurable address instead of hardcoded 0x38 - 0x02, - 0x00, - ]); - const checksum = - ~( - motorId + - 0x04 + - 0x02 + - config.protocol.presentPositionAddress + - 0x02 - ) & 0xff; - packet[7] = checksum; - - if (!config.port || !config.port.isOpen) { - throw new Error("Serial port not open"); - } - - await new Promise((resolve, reject) => { - config.port.write(packet, (error) => { - if (error) { - reject(new Error(`Failed to send read packet: ${error.message}`)); - } else { - resolve(); - } - }); - }); - - try { - const response = await readData(config.port, 100); // Faster timeout for 30Hz performance - if (response.length >= 7) { - const id = response[2]; - const error = response[4]; - if (id === motorId && error === 0) { - const position = response[5] | (response[6] << 8); - motorPositions.push(position); - } else { - // Use half of max resolution as fallback instead of hardcoded 2047 - motorPositions.push( - Math.floor((config.protocol.resolution - 1) / 2) - ); - } - } else { - motorPositions.push(Math.floor((config.protocol.resolution - 1) / 2)); - } - } catch (readError) { - motorPositions.push(Math.floor((config.protocol.resolution - 1) / 2)); - } - } catch (error) { - motorPositions.push(Math.floor((config.protocol.resolution - 1) / 2)); - } - - // Minimal delay between servo reads for 30Hz performance - await new Promise((resolve) => setTimeout(resolve, 2)); - } - - return motorPositions; -} - -/** - * Interactive calibration procedure - * Same flow for both leader and follower, just different configurations - */ -export async function performInteractiveCalibration( - config: SO100CalibrationConfig -): Promise { - // Step 1: Set homing position - await promptUser( - `Move the SO-100 ${config.deviceType} to the MIDDLE of its range of motion and press ENTER...` - ); - - const homingOffsets = await setHomingOffsets(config); - - // Step 2: Record ranges of motion with live updates - const { rangeMins, rangeMaxes } = await recordRangesOfMotion(config); - - // Step 3: Set special range for wrist_roll (full turn motor) - rangeMins["wrist_roll"] = 0; - rangeMaxes["wrist_roll"] = 4095; - - // Step 4: Write hardware position limits to motors (matching Python behavior) - await writeHardwarePositionLimits(config, rangeMins, rangeMaxes); - - // Compile results in Python-compatible format - const results: CalibrationResults = {}; - - for (let i = 0; i < config.motorNames.length; i++) { - const motorName = config.motorNames[i]; - const motorId = config.motorIds[i]; - - results[motorName] = { - id: motorId, - drive_mode: config.driveModes[i], - homing_offset: homingOffsets[motorName], - range_min: rangeMins[motorName], - range_max: rangeMaxes[motorName], - }; - } - - return results; -} - -/** - * Set motor limits (device-specific) - */ -export async function setMotorLimits( - config: SO100CalibrationConfig -): Promise { - // Silent unless error - motor limits configured internally -} - -/** - * Verify calibration was successful - */ -export async function verifyCalibration( - config: SO100CalibrationConfig -): Promise { - // Silent unless error - calibration verification passed internally -} - -/** - * Reset homing offsets to 0 for all motors - * Mirrors Python reset_calibration() - critical step before calculating new offsets - * This ensures Present_Position reflects true physical position without existing offsets - */ -async function resetHomingOffsets( - config: SO100CalibrationConfig -): Promise { - for (let i = 0; i < config.motorIds.length; i++) { - const motorId = config.motorIds[i]; - const motorName = config.motorNames[i]; - - try { - // Write 0 to Homing_Offset register using configurable address - const homingOffsetValue = 0; - - // Create Write Homing_Offset packet using configurable address - const packet = Buffer.from([ - 0xff, - 0xff, // Header - motorId, // Servo ID - 0x05, // Length (Instruction + Address + Data + Checksum) - 0x03, // Instruction: WRITE_DATA - config.protocol.homingOffsetAddress, // Configurable address instead of hardcoded 0x1f - homingOffsetValue & 0xff, // Data_L (low byte) - (homingOffsetValue >> 8) & 0xff, // Data_H (high byte) - 0x00, // Checksum (will calculate) - ]); - - // Calculate checksum using configurable address - const checksum = - ~( - motorId + - 0x05 + - 0x03 + - config.protocol.homingOffsetAddress + - (homingOffsetValue & 0xff) + - ((homingOffsetValue >> 8) & 0xff) - ) & 0xff; - packet[8] = checksum; - - if (!config.port || !config.port.isOpen) { - throw new Error("Serial port not open"); - } - - // Send reset packet - await new Promise((resolve, reject) => { - config.port.write(packet, (error) => { - if (error) { - reject( - new Error( - `Failed to reset homing offset for ${motorName}: ${error.message}` - ) - ); - } else { - resolve(); - } - }); - }); - - // Wait for response (silent unless error) - try { - await readData(config.port, 200); - } catch (error) { - // Silent - response not required for successful operation - } - } catch (error) { - throw new Error( - `Failed to reset homing offset for ${motorName}: ${ - error instanceof Error ? error.message : error - }` - ); - } - - // Small delay between motor writes - await new Promise((resolve) => setTimeout(resolve, 20)); - } -} - -/** - * Record homing offsets (current positions as center) - * Mirrors Python bus.set_half_turn_homings() - * - * CRITICAL: Must reset existing homing offsets to 0 first (like Python does) - * CRITICAL: Must WRITE the new homing offsets to motors immediately (like Python does) - */ -async function setHomingOffsets( - config: SO100CalibrationConfig -): Promise<{ [motor: string]: number }> { - // CRITICAL: Reset existing homing offsets to 0 first (matching Python) - await resetHomingOffsets(config); - - // Wait a moment for reset to take effect - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Now read positions (which will be true physical positions) - const currentPositions = await readMotorPositions(config); - const homingOffsets: { [motor: string]: number } = {}; - - for (let i = 0; i < config.motorNames.length; i++) { - const motorName = config.motorNames[i]; - const position = currentPositions[i]; - - // Generic formula: pos - int((max_res - 1) / 2) using configurable resolution - const halfTurn = Math.floor((config.protocol.resolution - 1) / 2); - homingOffsets[motorName] = position - halfTurn; - } - - // CRITICAL: Write homing offsets to motors immediately (matching Python exactly) - // Python does: for motor, offset in homing_offsets.items(): self.write("Homing_Offset", motor, offset) - await writeHomingOffsetsToMotors(config, homingOffsets); - - return homingOffsets; -} - -/** - * Write homing offsets to motor registers immediately - * Mirrors Python's immediate writing in set_half_turn_homings() - */ -async function writeHomingOffsetsToMotors( - config: SO100CalibrationConfig, - homingOffsets: { [motor: string]: number } -): Promise { - for (let i = 0; i < config.motorIds.length; i++) { - const motorId = config.motorIds[i]; - const motorName = config.motorNames[i]; - const homingOffset = homingOffsets[motorName]; - - try { - // Encode using sign-magnitude format (like Python) - const encodedOffset = encodeSignMagnitude( - homingOffset, - config.protocol.signMagnitudeBit - ); - - // Create Write Homing_Offset packet - const packet = Buffer.from([ - 0xff, - 0xff, // Header - motorId, // Servo ID - 0x05, // Length - 0x03, // Instruction: WRITE_DATA - config.protocol.homingOffsetAddress, // Homing_Offset address - encodedOffset & 0xff, // Data_L (low byte) - (encodedOffset >> 8) & 0xff, // Data_H (high byte) - 0x00, // Checksum (will calculate) - ]); - - // Calculate checksum - const checksum = - ~( - motorId + - 0x05 + - 0x03 + - config.protocol.homingOffsetAddress + - (encodedOffset & 0xff) + - ((encodedOffset >> 8) & 0xff) - ) & 0xff; - packet[8] = checksum; - - if (!config.port || !config.port.isOpen) { - throw new Error("Serial port not open"); - } - - // Send packet - await new Promise((resolve, reject) => { - config.port.write(packet, (error) => { - if (error) { - reject( - new Error( - `Failed to write homing offset for ${motorName}: ${error.message}` - ) - ); - } else { - resolve(); - } - }); - }); - - // Wait for response (silent unless error) - try { - await readData(config.port, 200); - } catch (error) { - // Silent - response not required for successful operation - } - } catch (error) { - throw new Error( - `Failed to write homing offset for ${motorName}: ${ - error instanceof Error ? error.message : error - }` - ); - } - - // Small delay between motor writes - await new Promise((resolve) => setTimeout(resolve, 20)); - } -} - -/** - * Record ranges of motion with live updating table - * Mirrors Python bus.record_ranges_of_motion() - */ -async function recordRangesOfMotion(config: SO100CalibrationConfig): Promise<{ - rangeMins: { [motor: string]: number }; - rangeMaxes: { [motor: string]: number }; -}> { - console.log( - "Move all joints sequentially through their entire ranges of motion." - ); - console.log( - "Positions will be recorded continuously. Press ENTER to stop...\n" - ); - - const rangeMins: { [motor: string]: number } = {}; - const rangeMaxes: { [motor: string]: number } = {}; - - // Read actual current positions (matching Python exactly) - // Python does: start_positions = self.sync_read("Present_Position", motors, normalize=False) - // mins = start_positions.copy(); maxes = start_positions.copy() - const startPositions = await readMotorPositions(config); - - for (let i = 0; i < config.motorNames.length; i++) { - const motorName = config.motorNames[i]; - const startPosition = startPositions[i]; - rangeMins[motorName] = startPosition; // Use actual position, not hardcoded 2047 - rangeMaxes[motorName] = startPosition; // Use actual position, not hardcoded 2047 - } - - let recording = true; - let readCount = 0; - - // Set up readline to detect Enter key - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.on("line", () => { - recording = false; - rl.close(); - }); - - // Continuous recording loop with live updates - THE LIVE UPDATING TABLE! - while (recording) { - try { - const positions = await readMotorPositions(config); // Always quiet during live recording - readCount++; - - // Update min/max ranges - for (let i = 0; i < config.motorNames.length; i++) { - const motorName = config.motorNames[i]; - const position = positions[i]; - - if (position < rangeMins[motorName]) { - rangeMins[motorName] = position; - } - if (position > rangeMaxes[motorName]) { - rangeMaxes[motorName] = position; - } - } - - // Show real-time feedback every 3 reads for faster updates - LIVE TABLE UPDATE - if (readCount % 3 === 0) { - // Build the live table content - let liveTable = `Readings: ${readCount}\n\n`; - liveTable += "Motor Name Current Min Max Range\n"; - liveTable += "─".repeat(55) + "\n"; - - for (let i = 0; i < config.motorNames.length; i++) { - const motorName = config.motorNames[i]; - const current = positions[i]; - const min = rangeMins[motorName]; - const max = rangeMaxes[motorName]; - const range = max - min; - - liveTable += `${motorName.padEnd(15)} ${current - .toString() - .padStart(6)} ${min.toString().padStart(6)} ${max - .toString() - .padStart(6)} ${range.toString().padStart(8)}\n`; - } - liveTable += "\nMove joints through their full range..."; - - // Update the display in place (no new console lines!) - logUpdate(liveTable); - } - - // Minimal delay for 30Hz reading rate (~33ms cycle time) - await new Promise((resolve) => setTimeout(resolve, 10)); - } catch (error) { - console.warn( - `Read error: ${error instanceof Error ? error.message : error}` - ); - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } - - // Stop live updating and return to normal console - logUpdate.done(); - - return { rangeMins, rangeMaxes }; -} - -/** - * Prompt user for input (real implementation with readline) - */ -async function promptUser(message: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - rl.question(message, (answer) => { - rl.close(); - resolve(answer); - }); - }); -} - -/** - * Read data from serial port with timeout - */ -async function readData( - port: SerialPort, - timeout: number = 5000 -): Promise { - if (!port || !port.isOpen) { - throw new Error("Serial port not open"); - } - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error("Read timeout")); - }, timeout); - - port.once("data", (data: Buffer) => { - clearTimeout(timer); - resolve(data); - }); - }); -} - -/** - * Write hardware position limits to motors - * Mirrors Python lerobot write_calibration() behavior where it writes: - * - Min_Position_Limit register with calibration.range_min - * - Max_Position_Limit register with calibration.range_max - * This physically constrains the motors to the calibrated ranges - */ -async function writeHardwarePositionLimits( - config: SO100CalibrationConfig, - rangeMins: { [motor: string]: number }, - rangeMaxes: { [motor: string]: number } -): Promise { - for (let i = 0; i < config.motorIds.length; i++) { - const motorId = config.motorIds[i]; - const motorName = config.motorNames[i]; - const minLimit = rangeMins[motorName]; - const maxLimit = rangeMaxes[motorName]; - - try { - // Write Min_Position_Limit register - await writeMotorRegister( - config, - motorId, - config.protocol.minPositionLimitAddress, - minLimit, - `Min_Position_Limit for ${motorName}` - ); - - // Small delay between writes - await new Promise((resolve) => setTimeout(resolve, 20)); - - // Write Max_Position_Limit register - await writeMotorRegister( - config, - motorId, - config.protocol.maxPositionLimitAddress, - maxLimit, - `Max_Position_Limit for ${motorName}` - ); - - // Small delay between motors - await new Promise((resolve) => setTimeout(resolve, 20)); - } catch (error) { - throw new Error( - `Failed to write position limits for ${motorName}: ${ - error instanceof Error ? error.message : error - }` - ); - } - } -} - -/** - * Generic function to write a 2-byte value to a motor register - * Used for both Min_Position_Limit and Max_Position_Limit - */ -async function writeMotorRegister( - config: SO100CalibrationConfig, - motorId: number, - registerAddress: number, - value: number, - description: string -): Promise { - // Create Write Register packet - const packet = Buffer.from([ - 0xff, - 0xff, // Header - motorId, // Servo ID - 0x05, // Length (Instruction + Address + Data + Checksum) - 0x03, // Instruction: WRITE_DATA - registerAddress, // Register address - value & 0xff, // Data_L (low byte) - (value >> 8) & 0xff, // Data_H (high byte) - 0x00, // Checksum (will calculate) - ]); - - // Calculate checksum - const checksum = - ~( - motorId + - 0x05 + - 0x03 + - registerAddress + - (value & 0xff) + - ((value >> 8) & 0xff) - ) & 0xff; - packet[8] = checksum; - - if (!config.port || !config.port.isOpen) { - throw new Error("Serial port not open"); - } - - // Send packet - await new Promise((resolve, reject) => { - config.port.write(packet, (error) => { - if (error) { - reject(new Error(`Failed to write ${description}: ${error.message}`)); - } else { - resolve(); - } - }); - }); - - // Wait for response (silent unless error) - try { - await readData(config.port, 200); - } catch (error) { - // Silent - response not required for successful operation - } -} diff --git a/src/lerobot/node/common/so100_config.ts b/src/lerobot/node/common/so100_config.ts deleted file mode 100644 index 62fc5dffa72b187e5cc423824019c2bde8a525c1..0000000000000000000000000000000000000000 --- a/src/lerobot/node/common/so100_config.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * SO-100 device configurations - * Defines the differences between leader and follower devices - * Mirrors Python lerobot device configuration approach - */ - -import type { SO100CalibrationConfig } from "../types/calibration.js"; -import { SerialPort } from "serialport"; - -/** - * Common motor names for all SO-100 devices - */ -const SO100_MOTOR_NAMES = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", -]; - -/** - * Common motor IDs for all SO-100 devices (STS3215 servos) - */ -const SO100_MOTOR_IDS = [1, 2, 3, 4, 5, 6]; - -/** - * Protocol configuration for STS3215 motors used in SO-100 devices - */ -interface STS3215Protocol { - resolution: number; - homingOffsetAddress: number; - homingOffsetLength: number; - presentPositionAddress: number; - presentPositionLength: number; - minPositionLimitAddress: number; - minPositionLimitLength: number; - maxPositionLimitAddress: number; - maxPositionLimitLength: number; - signMagnitudeBit: number; // Bit 11 is sign bit for Homing_Offset encoding -} - -/** - * STS3215 Protocol Configuration - * These addresses and settings are specific to the STS3215 servo motors - */ -export const STS3215_PROTOCOL: STS3215Protocol = { - resolution: 4096, // 12-bit resolution (0-4095) - homingOffsetAddress: 31, // Address for Homing_Offset register - homingOffsetLength: 2, // 2 bytes for Homing_Offset - presentPositionAddress: 56, // Address for Present_Position register - presentPositionLength: 2, // 2 bytes for Present_Position - minPositionLimitAddress: 9, // Address for Min_Position_Limit register - minPositionLimitLength: 2, // 2 bytes for Min_Position_Limit - maxPositionLimitAddress: 11, // Address for Max_Position_Limit register - maxPositionLimitLength: 2, // 2 bytes for Max_Position_Limit - signMagnitudeBit: 11, // Bit 11 is sign bit for Homing_Offset encoding -} as const; - -/** - * SO-100 Follower Configuration - * Robot arm that performs tasks autonomously - * Drive modes match Python lerobot exactly: all motors use drive_mode=0 - */ -export function createSO100FollowerConfig( - port: SerialPort -): SO100CalibrationConfig { - return { - deviceType: "so100_follower", - port, - motorNames: SO100_MOTOR_NAMES, - motorIds: SO100_MOTOR_IDS, - protocol: STS3215_PROTOCOL, - - // Python lerobot uses drive_mode=0 for all motors (current format) - driveModes: [0, 0, 0, 0, 0, 0], - - // Calibration modes (not used in current implementation, but kept for compatibility) - calibModes: ["DEGREE", "DEGREE", "DEGREE", "DEGREE", "DEGREE", "LINEAR"], - - // Follower limits - these are not used in calibration file format - limits: { - position_min: [-180, -90, -90, -90, -90, -90], - position_max: [180, 90, 90, 90, 90, 90], - velocity_max: [100, 100, 100, 100, 100, 100], - torque_max: [50, 50, 50, 50, 25, 25], - }, - }; -} - -/** - * SO-100 Leader Configuration - * Teleoperator arm that humans use to control the follower - * Drive modes match Python lerobot exactly: all motors use drive_mode=0 - */ -export function createSO100LeaderConfig( - port: SerialPort -): SO100CalibrationConfig { - return { - deviceType: "so100_leader", - port, - motorNames: SO100_MOTOR_NAMES, - motorIds: SO100_MOTOR_IDS, - protocol: STS3215_PROTOCOL, - - // Python lerobot uses drive_mode=0 for all motors (current format) - driveModes: [0, 0, 0, 0, 0, 0], - - // Same calibration modes as follower - calibModes: ["DEGREE", "DEGREE", "DEGREE", "DEGREE", "DEGREE", "LINEAR"], - - // Leader limits - these are not used in calibration file format - limits: { - position_min: [-120, -60, -60, -60, -180, -45], - position_max: [120, 60, 60, 60, 180, 45], - velocity_max: [80, 80, 80, 80, 120, 60], - torque_max: [30, 30, 30, 30, 20, 15], - }, - }; -} - -/** - * Get configuration for any SO-100 device type - */ -export function getSO100Config( - deviceType: "so100_follower" | "so100_leader", - port: SerialPort -): SO100CalibrationConfig { - switch (deviceType) { - case "so100_follower": - return createSO100FollowerConfig(port); - case "so100_leader": - return createSO100LeaderConfig(port); - default: - throw new Error(`Unknown SO-100 device type: ${deviceType}`); - } -} diff --git a/src/lerobot/node/find_port.ts b/src/lerobot/node/find_port.ts deleted file mode 100644 index e3dce6d6134693dea5c015b65443660fbf62dbce..0000000000000000000000000000000000000000 --- a/src/lerobot/node/find_port.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Helper to find the USB port associated with your MotorsBus. - * - * Example: - * ``` - * npx lerobot find-port - * ``` - */ - -import { SerialPort } from "serialport"; -import { createInterface } from "readline"; -import { platform } from "os"; -import { readdir } from "fs/promises"; -import { join } from "path"; - -/** - * Find all available serial ports on the system - * Mirrors Python's find_available_ports() function - */ -async function findAvailablePorts(): Promise { - if (platform() === "win32") { - // List COM ports using serialport library (equivalent to pyserial) - const ports = await SerialPort.list(); - return ports.map((port) => port.path); - } else { - // List /dev/tty* ports for Unix-based systems (Linux/macOS) - try { - const devFiles = await readdir("/dev"); - const ttyPorts = devFiles - .filter((file) => file.startsWith("tty")) - .map((file) => join("/dev", file)); - return ttyPorts; - } catch (error) { - // Fallback to serialport library if /dev reading fails - const ports = await SerialPort.list(); - return ports.map((port) => port.path); - } - } -} - -/** - * Create readline interface for user input - * Equivalent to Python's input() function - */ -function createReadlineInterface() { - return createInterface({ - input: process.stdin, - output: process.stdout, - }); -} - -/** - * Prompt user for input and wait for response - * Equivalent to Python's input() function - */ -function waitForInput(prompt: string = ""): Promise { - const rl = createReadlineInterface(); - return new Promise((resolve) => { - if (prompt) { - process.stdout.write(prompt); - } - rl.on("line", (answer) => { - rl.close(); - resolve(answer); - }); - }); -} - -/** - * Sleep for specified milliseconds - * Equivalent to Python's time.sleep() - */ -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Main find port function - direct port of Python find_port() - * Maintains identical UX and messaging - */ -export async function findPort(): Promise { - console.log("Finding all available ports for the MotorsBus."); - - const portsBefore = await findAvailablePorts(); - console.log("Ports before disconnecting:", portsBefore); - - console.log( - "Remove the USB cable from your MotorsBus and press Enter when done." - ); - await waitForInput(); - - // Allow some time for port to be released (equivalent to Python's time.sleep(0.5)) - await sleep(500); - - const portsAfter = await findAvailablePorts(); - const portsDiff = portsBefore.filter((port) => !portsAfter.includes(port)); - - if (portsDiff.length === 1) { - const port = portsDiff[0]; - console.log(`The port of this MotorsBus is '${port}'`); - console.log("Reconnect the USB cable."); - } else if (portsDiff.length === 0) { - throw new Error( - `Could not detect the port. No difference was found (${JSON.stringify( - portsDiff - )}).` - ); - } else { - throw new Error( - `Could not detect the port. More than one port was found (${JSON.stringify( - portsDiff - )}).` - ); - } -} - -/** - * CLI entry point when called directly - */ -if (import.meta.url === `file://${process.argv[1]}`) { - findPort().catch((error) => { - console.error(error.message); - process.exit(1); - }); -} diff --git a/src/lerobot/node/robots/robot.ts b/src/lerobot/node/robots/robot.ts deleted file mode 100644 index 0ab57bb3c9fbbe4757ab8778fc60ff62b9b032d8..0000000000000000000000000000000000000000 --- a/src/lerobot/node/robots/robot.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Base Robot class for Node.js platform - * Uses serialport package for serial communication - * Mirrors Python lerobot/common/robots/robot.py - */ - -import { SerialPort } from "serialport"; -import { mkdir, writeFile } from "fs/promises"; -import { existsSync, readFileSync, mkdirSync } from "fs"; -import { join } from "path"; -import type { RobotConfig } from "../types/robot-config.js"; -import { getCalibrationDir, ROBOTS } from "../utils/constants.js"; - -export abstract class Robot { - protected port: SerialPort | null = null; - protected config: RobotConfig; - protected calibrationDir: string; - protected calibrationPath: string; - protected name: string; - protected calibration: any = {}; // Loaded calibration data - protected isCalibrated: boolean = false; - - constructor(config: RobotConfig) { - this.config = config; - this.name = config.type; - - // Determine calibration directory - // Mirrors Python: config.calibration_dir if config.calibration_dir else HF_LEROBOT_CALIBRATION / ROBOTS / self.name - this.calibrationDir = - config.calibration_dir || join(getCalibrationDir(), ROBOTS, this.name); - - // Use robot ID or type as filename - const robotId = config.id || this.name; - this.calibrationPath = join(this.calibrationDir, `${robotId}.json`); - - // Auto-load calibration if it exists (like Python version) - this.loadCalibration(); - } - - /** - * Connect to the robot - * Mirrors Python robot.connect() - */ - async connect(_calibrate: boolean = false): Promise { - try { - this.port = new SerialPort({ - path: this.config.port, - baudRate: 1000000, // Default baud rate for Feetech motors (SO-100) - matches Python lerobot - dataBits: 8, // 8 data bits - matches Python serial.EIGHTBITS - stopBits: 1, // 1 stop bit - matches Python default - parity: "none", // No parity - matches Python default - autoOpen: false, - }); - - // Open the port - await new Promise((resolve, reject) => { - this.port!.open((error) => { - if (error) { - reject( - new Error( - `Failed to open port ${this.config.port}: ${error.message}` - ) - ); - } else { - resolve(); - } - }); - }); - } catch (error) { - throw new Error(`Could not connect to robot on port ${this.config.port}`); - } - } - - /** - * Calibrate the robot - * Must be implemented by subclasses - */ - abstract calibrate(): Promise; - - /** - * Disconnect from the robot - * Mirrors Python robot.disconnect() - */ - async disconnect(): Promise { - if (this.port && this.port.isOpen) { - // Handle torque disable if configured - if (this.config.disable_torque_on_disconnect) { - await this.disableTorque(); - } - - await new Promise((resolve) => { - this.port!.close(() => { - resolve(); - }); - }); - - this.port = null; - } - } - - /** - * Save calibration data to JSON file - * Mirrors Python's configuration saving - */ - protected async saveCalibration(calibrationData: any): Promise { - // Ensure calibration directory exists - try { - mkdirSync(this.calibrationDir, { recursive: true }); - } catch (error) { - // Directory might already exist, that's fine - } - - // Save calibration data as JSON - await writeFile( - this.calibrationPath, - JSON.stringify(calibrationData, null, 2) - ); - - console.log(`Configuration saved to: ${this.calibrationPath}`); - } - - /** - * Load calibration data from JSON file - * Mirrors Python's _load_calibration() - */ - protected loadCalibration(): void { - try { - if (existsSync(this.calibrationPath)) { - const calibrationData = readFileSync(this.calibrationPath, "utf8"); - this.calibration = JSON.parse(calibrationData); - this.isCalibrated = true; - console.log(`✅ Loaded calibration from: ${this.calibrationPath}`); - } else { - console.log( - `⚠️ No calibration file found at: ${this.calibrationPath}` - ); - this.isCalibrated = false; - } - } catch (error) { - console.warn( - `Failed to load calibration: ${ - error instanceof Error ? error.message : error - }` - ); - this.calibration = {}; - this.isCalibrated = false; - } - } - - /** - * Send command to robot via serial port - */ - protected async sendCommand(command: string): Promise { - if (!this.port || !this.port.isOpen) { - throw new Error("Robot not connected"); - } - - return new Promise((resolve, reject) => { - this.port!.write(command, (error) => { - if (error) { - reject(new Error(`Failed to send command: ${error.message}`)); - } else { - resolve(); - } - }); - }); - } - - /** - * Read data from robot - */ - protected async readData(timeout: number = 5000): Promise { - if (!this.port || !this.port.isOpen) { - throw new Error("Robot not connected"); - } - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error("Read timeout")); - }, timeout); - - this.port!.once("data", (data: Buffer) => { - clearTimeout(timer); - resolve(data); - }); - }); - } - - /** - * Disable torque on disconnect (SO-100 specific) - */ - protected async disableTorque(): Promise { - try { - await this.sendCommand("TORQUE_DISABLE\r\n"); - } catch (error) { - console.warn("Warning: Could not disable torque on disconnect"); - } - } -} diff --git a/src/lerobot/node/robots/so100_follower.ts b/src/lerobot/node/robots/so100_follower.ts deleted file mode 100644 index 82b17a8dd639defc24d3f55a9fc0c76957f65f2d..0000000000000000000000000000000000000000 --- a/src/lerobot/node/robots/so100_follower.ts +++ /dev/null @@ -1,614 +0,0 @@ -/** - * SO-100 Follower Robot implementation for Node.js - * Mirrors Python lerobot/common/robots/so100_follower/so100_follower.py - */ - -import { Robot } from "./robot.js"; -import type { RobotConfig } from "../types/robot-config.js"; -import * as readline from "readline"; - -export class SO100Follower extends Robot { - constructor(config: RobotConfig) { - super(config); - - // Validate that this is an SO-100 follower config - if (config.type !== "so100_follower") { - throw new Error( - `Invalid robot type: ${config.type}. Expected: so100_follower` - ); - } - } - - /** - * Calibrate the SO-100 follower robot - * NOTE: Calibration logic has been moved to shared/common/calibration.ts - * This method is kept for backward compatibility but delegates to the main calibrate.ts - */ - async calibrate(): Promise { - throw new Error( - "Direct device calibration is deprecated. Use the main calibrate.ts orchestrator instead." - ); - } - - /** - * Initialize robot communication - * For now, just test basic serial connectivity - */ - private async initializeRobot(): Promise { - console.log("Initializing robot communication..."); - - try { - // For SO-100, we need to implement Feetech servo protocol - // For now, just test that we can send/receive data - console.log("Testing serial port connectivity..."); - - // Try to ping servo ID 1 (shoulder_pan motor) - // This is a very basic test - real implementation needs proper Feetech protocol - const pingPacket = Buffer.from([0xff, 0xff, 0x01, 0x02, 0x01, 0xfb]); // Basic ping packet - - if (!this.port || !this.port.isOpen) { - throw new Error("Serial port not open"); - } - - // Send ping packet - await new Promise((resolve, reject) => { - this.port!.write(pingPacket, (error) => { - if (error) { - reject(new Error(`Failed to send ping: ${error.message}`)); - } else { - resolve(); - } - }); - }); - - console.log("Ping packet sent successfully"); - - // Try to read response with shorter timeout - try { - const response = await this.readData(1000); // 1 second timeout - console.log(`Response received: ${response.length} bytes`); - } catch (error) { - console.log("No response received (expected for basic test)"); - } - } catch (error) { - throw new Error( - `Serial communication test failed: ${ - error instanceof Error ? error.message : error - }` - ); - } - - console.log("Robot communication test completed."); - } - - /** - * Read current motor positions as a record with motor names - * For teleoperation use - */ - async getMotorPositions(): Promise> { - const positions = await this.readMotorPositions(); - const motorNames = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", - ]; - - const result: Record = {}; - for (let i = 0; i < motorNames.length; i++) { - result[motorNames[i]] = positions[i]; - } - return result; - } - - /** - * Get calibration data for teleoperation - * Returns position limits and offsets from calibration file - */ - getCalibrationLimits(): Record { - if (!this.isCalibrated || !this.calibration) { - console.warn("No calibration data available, using default limits"); - // Default STS3215 limits as fallback - return { - shoulder_pan: { min: 985, max: 3085 }, - shoulder_lift: { min: 1200, max: 2800 }, - elbow_flex: { min: 1000, max: 3000 }, - wrist_flex: { min: 1100, max: 2900 }, - wrist_roll: { min: 0, max: 4095 }, // Full rotation motor - gripper: { min: 1800, max: 2300 }, - }; - } - - // Extract limits from calibration data (matches Python format) - const limits: Record = {}; - for (const [motorName, calibData] of Object.entries(this.calibration)) { - if ( - calibData && - typeof calibData === "object" && - "range_min" in calibData && - "range_max" in calibData - ) { - limits[motorName] = { - min: Number(calibData.range_min), - max: Number(calibData.range_max), - }; - } - } - - return limits; - } - - /** - * Set motor positions from a record with motor names - * For teleoperation use - */ - async setMotorPositions(positions: Record): Promise { - const motorNames = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", - ]; - const motorIds = [1, 2, 3, 4, 5, 6]; // SO-100 has servo IDs 1-6 - - for (let i = 0; i < motorNames.length; i++) { - const motorName = motorNames[i]; - const motorId = motorIds[i]; - const position = positions[motorName]; - - if (position !== undefined) { - await this.writeMotorPosition(motorId, position); - } - } - } - - /** - * Write position to a single motor - * Implements STS3215 WRITE_DATA command for position control - */ - private async writeMotorPosition( - motorId: number, - position: number - ): Promise { - if (!this.port || !this.port.isOpen) { - throw new Error("Serial port not open"); - } - - // Clamp position to valid range - const clampedPosition = Math.max(0, Math.min(4095, Math.round(position))); - - // Create STS3215 Write Position packet - // Format: [0xFF, 0xFF, ID, Length, Instruction, Address, Data_L, Data_H, Checksum] - // Goal_Position address for STS3215 is 42 (0x2A), length 2 bytes - const packet = Buffer.from([ - 0xff, - 0xff, // Header - motorId, // Servo ID - 0x05, // Length (Instruction + Address + Data_L + Data_H + Checksum) - 0x03, // Instruction: WRITE_DATA - 0x2a, // Address: Goal_Position (42) - clampedPosition & 0xff, // Data_L (low byte) - (clampedPosition >> 8) & 0xff, // Data_H (high byte) - 0x00, // Checksum (will calculate) - ]); - - // Calculate checksum: ~(ID + Length + Instruction + Address + Data_L + Data_H) & 0xFF - const checksum = - ~( - motorId + - 0x05 + - 0x03 + - 0x2a + - (clampedPosition & 0xff) + - ((clampedPosition >> 8) & 0xff) - ) & 0xff; - packet[8] = checksum; - - // Send write position packet - await new Promise((resolve, reject) => { - this.port!.write(packet, (error) => { - if (error) { - reject(new Error(`Failed to send write packet: ${error.message}`)); - } else { - resolve(); - } - }); - }); - - // Small delay to allow servo to process command - await new Promise((resolve) => setTimeout(resolve, 1)); - } - - /** - * Read current motor positions - * Implements basic STS3215 servo protocol to read actual positions - */ - private async readMotorPositions(): Promise { - console.log("Reading motor positions..."); - - const motorPositions: number[] = []; - const motorIds = [1, 2, 3, 4, 5, 6]; // SO-100 has servo IDs 1-6 - const motorNames = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", - ]; - - // Try to read position from each servo using STS3215 protocol - for (let i = 0; i < motorIds.length; i++) { - const motorId = motorIds[i]; - const motorName = motorNames[i]; - - try { - console.log(` Reading ${motorName} (ID ${motorId})...`); - - // Create STS3215 Read Position packet - // Format: [0xFF, 0xFF, ID, Length, Instruction, Address, DataLength, Checksum] - // Present_Position address for STS3215 is 56 (0x38), length 2 bytes - const packet = Buffer.from([ - 0xff, - 0xff, // Header - motorId, // Servo ID - 0x04, // Length (Instruction + Address + DataLength + Checksum) - 0x02, // Instruction: READ_DATA - 0x38, // Address: Present_Position (56) - 0x02, // Data Length: 2 bytes - 0x00, // Checksum (will calculate) - ]); - - // Calculate checksum: ~(ID + Length + Instruction + Address + DataLength) & 0xFF - const checksum = ~(motorId + 0x04 + 0x02 + 0x38 + 0x02) & 0xff; - packet[7] = checksum; - - if (!this.port || !this.port.isOpen) { - throw new Error("Serial port not open"); - } - - // Send read position packet - await new Promise((resolve, reject) => { - this.port!.write(packet, (error) => { - if (error) { - reject(new Error(`Failed to send read packet: ${error.message}`)); - } else { - resolve(); - } - }); - }); - - // Try to read response (timeout after 500ms) - try { - const response = await this.readData(500); - - if (response.length >= 7) { - // Parse response: [0xFF, 0xFF, ID, Length, Error, Data_L, Data_H, Checksum] - const id = response[2]; - const error = response[4]; - - if (id === motorId && error === 0) { - // Extract 16-bit position from Data_L and Data_H - const position = response[5] | (response[6] << 8); - motorPositions.push(position); - - // Show calibrated range if available - const calibratedLimits = this.getCalibrationLimits(); - const limits = calibratedLimits[motorName]; - const rangeText = limits - ? `(${limits.min}-${limits.max} calibrated)` - : `(0-4095 raw)`; - console.log(` ${motorName}: ${position} ${rangeText}`); - } else { - console.warn( - ` ${motorName}: Error response (error code: ${error})` - ); - motorPositions.push(2047); // Use center position as fallback - } - } else { - console.warn(` ${motorName}: Invalid response length`); - motorPositions.push(2047); // Use center position as fallback - } - } catch (readError) { - console.warn( - ` ${motorName}: Read timeout - using fallback position` - ); - motorPositions.push(2047); // Use center position as fallback - } - } catch (error) { - console.warn( - ` ${motorName}: Communication error - ${ - error instanceof Error ? error.message : error - }` - ); - motorPositions.push(2047); // Use center position as fallback - } - - // Small delay between servo reads - await new Promise((resolve) => setTimeout(resolve, 10)); - } - - console.log(`Motor positions: [${motorPositions.join(", ")}]`); - return motorPositions; - } - - /** - * Set motor limits and safety parameters - * TODO: Implement proper Feetech servo protocol - */ - private async setMotorLimits(): Promise { - console.log("Setting motor limits..."); - - // Set default limits for SO-100 (based on Python implementation) - const limits = { - position_min: [-180, -90, -90, -90, -90, -90], - position_max: [180, 90, 90, 90, 90, 90], - velocity_max: [100, 100, 100, 100, 100, 100], - torque_max: [50, 50, 50, 50, 25, 25], - }; - - // For now, just return the limits without sending to robot - // Real implementation needs Feetech servo protocol to set limits - console.log("Motor limits configured (mock)."); - return limits; - } - - /** - * Interactive calibration process - matches Python lerobot calibration flow - * Implements real calibration with user interaction - */ - private async calibrateMotors(): Promise { - console.log("\n=== INTERACTIVE CALIBRATION ==="); - console.log("Starting SO-100 follower arm calibration..."); - - // Step 1: Move to middle position and record homing offsets - console.log("\n📍 STEP 1: Set Homing Position"); - await this.promptUser( - "Move the SO-100 to the MIDDLE of its range of motion and press ENTER..." - ); - - const homingOffsets = await this.setHomingOffsets(); - - // Step 2: Record ranges of motion - console.log("\n📏 STEP 2: Record Joint Ranges"); - const { rangeMins, rangeMaxes } = await this.recordRangesOfMotion(); - - // Step 3: Set special range for wrist_roll (full turn motor) - console.log("\n🔄 STEP 3: Configure Full-Turn Motor"); - console.log("Setting wrist_roll as full-turn motor (0-4095 range)"); - rangeMins["wrist_roll"] = 0; - rangeMaxes["wrist_roll"] = 4095; - - // Step 4: Compile calibration results - const motorNames = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", - ]; - const results = []; - - for (let i = 0; i < motorNames.length; i++) { - const motorId = i + 1; // Servo IDs are 1-6 - const motorName = motorNames[i]; - - results.push({ - motor: motorId, - name: motorName, - status: "success", - homing_offset: homingOffsets[motorName], - range_min: rangeMins[motorName], - range_max: rangeMaxes[motorName], - range_size: rangeMaxes[motorName] - rangeMins[motorName], - }); - - console.log( - `✅ ${motorName} calibrated: range ${rangeMins[motorName]} to ${rangeMaxes[motorName]} (offset: ${homingOffsets[motorName]})` - ); - } - - console.log("\n🎉 Interactive calibration completed!"); - return results; - } - - /** - * Verify calibration was successful - * TODO: Implement proper verification with Feetech servo protocol - */ - private async verifyCalibration(): Promise { - console.log("Verifying calibration..."); - - // For now, just mock successful verification - // Real implementation should check: - // 1. All motors respond to ping - // 2. Position limits are set correctly - // 3. Homing offsets are applied - // 4. Motors can move to test positions - - console.log("Calibration verification passed (mock)."); - } - - /** - * Prompt user for input (like Python's input() function) - */ - private async promptUser(message: string): Promise { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise((resolve) => { - rl.question(message, (answer) => { - rl.close(); - resolve(answer); - }); - }); - } - - /** - * Record homing offsets (current positions as center) - * Mirrors Python bus.set_half_turn_homings() - */ - private async setHomingOffsets(): Promise<{ [motor: string]: number }> { - console.log("Recording current positions as homing offsets..."); - - const currentPositions = await this.readMotorPositions(); - const motorNames = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", - ]; - const homingOffsets: { [motor: string]: number } = {}; - - for (let i = 0; i < motorNames.length; i++) { - const motorName = motorNames[i]; - const position = currentPositions[i]; - // Calculate homing offset (half turn offset from current position) - const maxRes = 4095; // STS3215 resolution - homingOffsets[motorName] = position - Math.floor(maxRes / 2); - console.log( - ` ${motorName}: offset ${homingOffsets[motorName]} (current pos: ${position})` - ); - } - - return homingOffsets; - } - - /** - * Record ranges of motion by continuously reading positions - * Mirrors Python bus.record_ranges_of_motion() - */ - private async recordRangesOfMotion(): Promise<{ - rangeMins: { [motor: string]: number }; - rangeMaxes: { [motor: string]: number }; - }> { - console.log("\n=== RECORDING RANGES OF MOTION ==="); - console.log( - "Move all joints sequentially through their entire ranges of motion." - ); - console.log( - "Positions will be recorded continuously. Press ENTER to stop...\n" - ); - - const motorNames = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", - ]; - const rangeMins: { [motor: string]: number } = {}; - const rangeMaxes: { [motor: string]: number } = {}; - - // Initialize with current positions - const initialPositions = await this.readMotorPositions(); - for (let i = 0; i < motorNames.length; i++) { - const motorName = motorNames[i]; - const position = initialPositions[i]; - rangeMins[motorName] = position; - rangeMaxes[motorName] = position; - } - - let recording = true; - let readCount = 0; - - // Set up readline to detect Enter key - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.on("line", () => { - recording = false; - rl.close(); - }); - - console.log("Recording started... (move the robot joints now)"); - - // Continuous recording loop - while (recording) { - try { - const positions = await this.readMotorPositions(); - readCount++; - - // Update min/max ranges - for (let i = 0; i < motorNames.length; i++) { - const motorName = motorNames[i]; - const position = positions[i]; - - if (position < rangeMins[motorName]) { - rangeMins[motorName] = position; - } - if (position > rangeMaxes[motorName]) { - rangeMaxes[motorName] = position; - } - } - - // Show real-time feedback every 10 reads - if (readCount % 10 === 0) { - console.clear(); // Clear screen for live update - console.log("=== LIVE POSITION RECORDING ==="); - console.log(`Readings: ${readCount} | Press ENTER to stop\n`); - - console.log("Motor Name Current Min Max Range"); - console.log("─".repeat(55)); - - for (let i = 0; i < motorNames.length; i++) { - const motorName = motorNames[i]; - const current = positions[i]; - const min = rangeMins[motorName]; - const max = rangeMaxes[motorName]; - const range = max - min; - - console.log( - `${motorName.padEnd(15)} ${current.toString().padStart(6)} ${min - .toString() - .padStart(6)} ${max.toString().padStart(6)} ${range - .toString() - .padStart(8)}` - ); - } - console.log("\nMove joints through their full range..."); - } - - // Small delay to avoid overwhelming the serial port - await new Promise((resolve) => setTimeout(resolve, 50)); - } catch (error) { - console.warn( - `Read error: ${error instanceof Error ? error.message : error}` - ); - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } - - console.log(`\nRecording stopped after ${readCount} readings.`); - console.log("\nFinal ranges recorded:"); - - for (const motorName of motorNames) { - const min = rangeMins[motorName]; - const max = rangeMaxes[motorName]; - const range = max - min; - console.log(` ${motorName}: ${min} to ${max} (range: ${range})`); - } - - return { rangeMins, rangeMaxes }; - } -} - -/** - * Factory function to create SO-100 follower robot - * Mirrors Python's make_robot_from_config pattern - */ -export function createSO100Follower(config: RobotConfig): SO100Follower { - return new SO100Follower(config); -} diff --git a/src/lerobot/node/teleoperate.ts b/src/lerobot/node/teleoperate.ts deleted file mode 100644 index 1dd83eb503f25cfa1b59ad515776302a61e7de48..0000000000000000000000000000000000000000 --- a/src/lerobot/node/teleoperate.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * Robot teleoperation using keyboard control - * - * Example: - * ``` - * npx lerobot teleoperate --robot.type=so100_follower --robot.port=COM4 --teleop.type=keyboard - * ``` - */ - -import { createSO100Follower } from "./robots/so100_follower.js"; -import { KeyboardController } from "./utils/keyboard-teleop.js"; -import type { TeleoperateConfig } from "./types/teleoperation.js"; - -/** - * Main teleoperate function - * Mirrors Python lerobot teleoperate.py structure - */ -export async function teleoperate(config: TeleoperateConfig): Promise { - // Validate configuration - if (!config.robot) { - throw new Error("Robot configuration is required"); - } - - if (!config.teleop || config.teleop.type !== "keyboard") { - throw new Error("Only keyboard teleoperation is currently supported"); - } - - const stepSize = config.step_size || 25; - const duration = config.duration_s; - - let robot; - let keyboardController; - - try { - // Create robot - switch (config.robot.type) { - case "so100_follower": - robot = createSO100Follower(config.robot); - break; - default: - throw new Error(`Unsupported robot type: ${config.robot.type}`); - } - - console.log( - `Connecting to robot: ${config.robot.type} on ${config.robot.port}` - ); - if (config.robot.id) { - console.log(`Robot ID: ${config.robot.id}`); - } - - await robot.connect(false); // calibrate=false - console.log("Robot connected successfully."); - - // Show calibration status - const isCalibrated = (robot as any).isCalibrated; - if (isCalibrated) { - console.log( - `✅ Loaded calibration for: ${config.robot.id || config.robot.type}` - ); - } else { - console.log( - `⚠️ No calibration found for: ${ - config.robot.id || config.robot.type - } (using defaults)` - ); - console.log( - " Run 'npx lerobot calibrate' first for optimal performance!" - ); - } - - // Create keyboard controller - keyboardController = new KeyboardController(robot, stepSize); - - console.log(""); - console.log("Starting keyboard teleoperation..."); - console.log("Controls:"); - console.log(" ↑↓ Arrow Keys: Shoulder Lift"); - console.log(" ←→ Arrow Keys: Shoulder Pan"); - console.log(" W/S: Elbow Flex"); - console.log(" A/D: Wrist Flex"); - console.log(" Q/E: Wrist Roll"); - console.log(" Space: Gripper Toggle"); - console.log(" ESC: Emergency Stop"); - console.log(" Ctrl+C: Exit"); - console.log(""); - console.log("Press any control key to begin..."); - console.log(""); - - // Start teleoperation control loop - await teleoperationLoop(keyboardController, robot, duration || null); - } catch (error) { - // Ensure we disconnect even if there's an error - if (keyboardController) { - try { - await keyboardController.stop(); - } catch (stopError) { - console.warn("Warning: Failed to stop keyboard controller properly"); - } - } - if (robot) { - try { - await robot.disconnect(); - } catch (disconnectError) { - console.warn("Warning: Failed to disconnect robot properly"); - } - } - throw error; - } -} - -/** - * Main teleoperation control loop - */ -async function teleoperationLoop( - keyboardController: KeyboardController, - robot: any, - duration: number | null -): Promise { - console.log("Initializing teleoperation..."); - - // Start keyboard controller - await keyboardController.start(); - - const startTime = performance.now(); - - // Set up graceful shutdown - let running = true; - process.on("SIGINT", () => { - console.log("\nShutting down gracefully..."); - running = false; - }); - - try { - // Just wait for the keyboard controller to handle everything - while (running) { - // Check duration limit - if (duration && performance.now() - startTime >= duration * 1000) { - console.log(`\nDuration limit reached (${duration}s). Stopping...`); - break; - } - - // Small delay to prevent busy waiting - await new Promise((resolve) => setTimeout(resolve, 100)); - } - } finally { - console.log("\nStopping teleoperation..."); - await keyboardController.stop(); - await robot.disconnect(); - console.log("Teleoperation stopped."); - } -} - -/** - * Parse command line arguments in Python argparse style - * Handles --robot.type=so100_follower --teleop.type=keyboard format - */ -export function parseArgs(args: string[]): TeleoperateConfig { - const config: Partial = {}; - - for (const arg of args) { - if (arg.startsWith("--robot.")) { - if (!config.robot) { - config.robot = { type: "so100_follower", port: "" }; - } - - const [key, value] = arg.substring(8).split("="); - switch (key) { - case "type": - if (value !== "so100_follower") { - throw new Error(`Unsupported robot type: ${value}`); - } - config.robot.type = value as "so100_follower"; - break; - case "port": - config.robot.port = value; - break; - case "id": - config.robot.id = value; - break; - default: - throw new Error(`Unknown robot parameter: ${key}`); - } - } else if (arg.startsWith("--teleop.")) { - if (!config.teleop) { - config.teleop = { type: "keyboard" }; - } - - const [key, value] = arg.substring(9).split("="); - switch (key) { - case "type": - if (value !== "keyboard") { - throw new Error(`Unsupported teleoperator type: ${value}`); - } - config.teleop.type = value as "keyboard"; - break; - default: - throw new Error(`Unknown teleoperator parameter: ${key}`); - } - } else if (arg.startsWith("--fps=")) { - config.fps = parseInt(arg.substring(6)); - if (isNaN(config.fps) || config.fps <= 0) { - throw new Error("FPS must be a positive number"); - } - } else if (arg.startsWith("--step_size=")) { - config.step_size = parseInt(arg.substring(12)); - if (isNaN(config.step_size) || config.step_size <= 0) { - throw new Error("Step size must be a positive number"); - } - } else if (arg.startsWith("--duration_s=")) { - config.duration_s = parseInt(arg.substring(13)); - if (isNaN(config.duration_s) || config.duration_s <= 0) { - throw new Error("Duration must be a positive number"); - } - } else if (arg === "--help" || arg === "-h") { - showUsage(); - process.exit(0); - } else if (!arg.startsWith("--")) { - // Skip non-option arguments - continue; - } else { - throw new Error(`Unknown argument: ${arg}`); - } - } - - // Validate required fields - if (!config.robot?.port) { - throw new Error("Robot port is required (--robot.port=PORT)"); - } - if (!config.teleop?.type) { - throw new Error("Teleoperator type is required (--teleop.type=keyboard)"); - } - - return config as TeleoperateConfig; -} - -/** - * Show usage information matching Python argparse output - */ -function showUsage(): void { - console.log("Usage: lerobot teleoperate [options]"); - console.log(""); - console.log("Control a robot using keyboard input"); - console.log(""); - console.log("Options:"); - console.log(" --robot.type=TYPE Robot type (so100_follower)"); - console.log( - " --robot.port=PORT Robot serial port (e.g., COM4, /dev/ttyUSB0)" - ); - console.log(" --robot.id=ID Robot identifier"); - console.log(" --teleop.type=TYPE Teleoperator type (keyboard)"); - console.log( - " --fps=FPS Control loop frame rate (default: 60)" - ); - console.log( - " --step_size=SIZE Position step size per keypress (default: 10)" - ); - console.log(" --duration_s=SECONDS Teleoperation duration in seconds"); - console.log(" -h, --help Show this help message"); - console.log(""); - console.log("Keyboard Controls:"); - console.log(" ↑↓ Arrow Keys Shoulder Lift"); - console.log(" ←→ Arrow Keys Shoulder Pan"); - console.log(" W/S Elbow Flex"); - console.log(" A/D Wrist Flex"); - console.log(" Q/E Wrist Roll"); - console.log(" Space Gripper Toggle"); - console.log(" ESC Emergency Stop"); - console.log(" Ctrl+C Exit"); - console.log(""); - console.log("Examples:"); - console.log( - " lerobot teleoperate --robot.type=so100_follower --robot.port=COM4 --teleop.type=keyboard" - ); - console.log( - " lerobot teleoperate --robot.type=so100_follower --robot.port=COM4 --teleop.type=keyboard --fps=30 --step_size=50" - ); - console.log(""); - console.log("Use 'lerobot find-port' to discover available ports."); -} - -/** - * CLI entry point when called directly - * Mirrors Python's if __name__ == "__main__": pattern - */ -export async function main(args: string[]): Promise { - try { - if (args.length === 0 || args.includes("--help") || args.includes("-h")) { - showUsage(); - return; - } - - const config = parseArgs(args); - await teleoperate(config); - } catch (error) { - if (error instanceof Error) { - console.error("Error:", error.message); - } else { - console.error("Error:", error); - } - - console.error(""); - console.error("Please verify:"); - console.error("1. The robot is connected to the specified port"); - console.error("2. No other application is using the port"); - console.error("3. You have permission to access the port"); - console.error(""); - console.error("Use 'lerobot find-port' to discover available ports."); - - process.exit(1); - } -} - -if (import.meta.url === `file://${process.argv[1]}`) { - const args = process.argv.slice(2); - main(args); -} diff --git a/src/lerobot/node/teleoperators/so100_leader.ts b/src/lerobot/node/teleoperators/so100_leader.ts deleted file mode 100644 index 1a970378c720462c6943b98075d7d6147a06bb68..0000000000000000000000000000000000000000 --- a/src/lerobot/node/teleoperators/so100_leader.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * SO-100 Leader Teleoperator implementation for Node.js - * - * Minimal implementation - calibration logic moved to shared/common/calibration.ts - * This class only handles connection management and basic device operations - */ - -import { Teleoperator } from "./teleoperator.js"; -import type { TeleoperatorConfig } from "../types/teleoperator-config.js"; - -export class SO100Leader extends Teleoperator { - constructor(config: TeleoperatorConfig) { - super(config); - - // Validate that this is an SO-100 leader config - if (config.type !== "so100_leader") { - throw new Error( - `Invalid teleoperator type: ${config.type}. Expected: so100_leader` - ); - } - } - - /** - * Calibrate the SO-100 leader teleoperator - * NOTE: Calibration logic has been moved to shared/common/calibration.ts - * This method is kept for backward compatibility but delegates to the main calibrate.ts - */ - async calibrate(): Promise { - throw new Error( - "Direct device calibration is deprecated. Use the main calibrate.ts orchestrator instead." - ); - } -} - -/** - * Factory function to create SO-100 leader teleoperator - * Mirrors Python's make_teleoperator_from_config pattern - */ -export function createSO100Leader(config: TeleoperatorConfig): SO100Leader { - return new SO100Leader(config); -} diff --git a/src/lerobot/node/teleoperators/teleoperator.ts b/src/lerobot/node/teleoperators/teleoperator.ts deleted file mode 100644 index 6be7e1afaa0dcddf0c7737dc770b725f5a4d9163..0000000000000000000000000000000000000000 --- a/src/lerobot/node/teleoperators/teleoperator.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Base Teleoperator class for Node.js platform - * Uses serialport package for serial communication - * Mirrors Python lerobot/common/teleoperators/teleoperator.py - */ - -import { SerialPort } from "serialport"; -import { mkdir, writeFile } from "fs/promises"; -import { join } from "path"; -import type { TeleoperatorConfig } from "../types/teleoperator-config.js"; -import { getCalibrationDir, TELEOPERATORS } from "../utils/constants.js"; - -export abstract class Teleoperator { - protected port: SerialPort | null = null; - protected config: TeleoperatorConfig; - protected calibrationDir: string; - protected calibrationPath: string; - protected name: string; - - constructor(config: TeleoperatorConfig) { - this.config = config; - this.name = config.type; - - // Determine calibration directory - // Mirrors Python: config.calibration_dir if config.calibration_dir else HF_LEROBOT_CALIBRATION / TELEOPERATORS / self.name - this.calibrationDir = - config.calibration_dir || - join(getCalibrationDir(), TELEOPERATORS, this.name); - - // Use teleoperator ID or type as filename - const teleopId = config.id || this.name; - this.calibrationPath = join(this.calibrationDir, `${teleopId}.json`); - } - - /** - * Connect to the teleoperator - * Mirrors Python teleoperator.connect() - */ - async connect(_calibrate: boolean = false): Promise { - try { - this.port = new SerialPort({ - path: this.config.port, - baudRate: 1000000, // Correct baud rate for Feetech motors (SO-100) - matches Python lerobot - dataBits: 8, // 8 data bits - matches Python serial.EIGHTBITS - stopBits: 1, // 1 stop bit - matches Python default - parity: "none", // No parity - matches Python default - autoOpen: false, - }); - - // Open the port - await new Promise((resolve, reject) => { - this.port!.open((error) => { - if (error) { - reject( - new Error( - `Failed to open port ${this.config.port}: ${error.message}` - ) - ); - } else { - resolve(); - } - }); - }); - } catch (error) { - throw new Error( - `Could not connect to teleoperator on port ${this.config.port}` - ); - } - } - - /** - * Calibrate the teleoperator - * Must be implemented by subclasses - */ - abstract calibrate(): Promise; - - /** - * Disconnect from the teleoperator - * Mirrors Python teleoperator.disconnect() - */ - async disconnect(): Promise { - if (this.port && this.port.isOpen) { - await new Promise((resolve) => { - this.port!.close(() => { - resolve(); - }); - }); - - this.port = null; - } - } - - /** - * Save calibration data to JSON file - * Mirrors Python's configuration saving - */ - protected async saveCalibration(calibrationData: any): Promise { - // Ensure calibration directory exists - await mkdir(this.calibrationDir, { recursive: true }); - - // Save calibration data as JSON - await writeFile( - this.calibrationPath, - JSON.stringify(calibrationData, null, 2) - ); - - console.log(`Configuration saved to: ${this.calibrationPath}`); - } - - /** - * Send command to teleoperator via serial port - */ - protected async sendCommand(command: string): Promise { - if (!this.port || !this.port.isOpen) { - throw new Error("Teleoperator not connected"); - } - - return new Promise((resolve, reject) => { - this.port!.write(command, (error) => { - if (error) { - reject(new Error(`Failed to send command: ${error.message}`)); - } else { - resolve(); - } - }); - }); - } - - /** - * Read data from teleoperator - */ - protected async readData(timeout: number = 5000): Promise { - if (!this.port || !this.port.isOpen) { - throw new Error("Teleoperator not connected"); - } - - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error("Read timeout")); - }, timeout); - - this.port!.once("data", (data: Buffer) => { - clearTimeout(timer); - resolve(data); - }); - }); - } -} diff --git a/src/lerobot/node/types/calibration.ts b/src/lerobot/node/types/calibration.ts deleted file mode 100644 index 8cc11760ebac06c04de75fc15115360849168be0..0000000000000000000000000000000000000000 --- a/src/lerobot/node/types/calibration.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Calibration types for Node.js implementation - */ - -import type { SerialPort } from "serialport"; - -export interface SO100CalibrationConfig { - deviceType: "so100_follower" | "so100_leader"; - port: SerialPort; - motorNames: string[]; - motorIds: number[]; // Device-specific motor IDs (e.g., [1,2,3,4,5,6] for SO-100) - driveModes: number[]; - calibModes: string[]; - - // Protocol-specific configuration - protocol: { - resolution: number; // Motor resolution (e.g., 4096 for STS3215) - homingOffsetAddress: number; // Register address for homing offset (e.g., 31 for STS3215) - homingOffsetLength: number; // Length in bytes for homing offset register - presentPositionAddress: number; // Register address for present position (e.g., 56 for STS3215) - presentPositionLength: number; // Length in bytes for present position register - minPositionLimitAddress: number; // Register address for min position limit (e.g., 9 for STS3215) - minPositionLimitLength: number; // Length in bytes for min position limit register - maxPositionLimitAddress: number; // Register address for max position limit (e.g., 11 for STS3215) - maxPositionLimitLength: number; // Length in bytes for max position limit register - signMagnitudeBit: number; // Sign bit index for homing offset encoding (e.g., 11 for STS3215) - }; - - limits: { - position_min: number[]; - position_max: number[]; - velocity_max: number[]; - torque_max: number[]; - }; -} - -export interface CalibrationResults { - [motorName: string]: { - id: number; - drive_mode: number; - homing_offset: number; - range_min: number; - range_max: number; - }; -} diff --git a/src/lerobot/node/types/robot-config.ts b/src/lerobot/node/types/robot-config.ts deleted file mode 100644 index 7a5b2eb73c983351084301e78b12caf3c3d82999..0000000000000000000000000000000000000000 --- a/src/lerobot/node/types/robot-config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Robot configuration types for Node.js implementation - */ - -export interface RobotConfig { - type: "so100_follower"; - port: string; - id?: string; - calibration_dir?: string; - // SO-100 specific options - disable_torque_on_disconnect?: boolean; - max_relative_target?: number | null; - use_degrees?: boolean; -} - -export interface CalibrateConfig { - robot?: RobotConfig; - teleop?: TeleoperatorConfig; -} - -// Re-export from teleoperator-config for convenience -import type { TeleoperatorConfig } from "./teleoperator-config.js"; -export type { TeleoperatorConfig }; diff --git a/src/lerobot/node/types/teleoperation.ts b/src/lerobot/node/types/teleoperation.ts deleted file mode 100644 index 3387ddbea78766a069250e49e06e5419a1c0edcc..0000000000000000000000000000000000000000 --- a/src/lerobot/node/types/teleoperation.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Teleoperation types for Node.js implementation - */ - -import type { RobotConfig } from "./robot-config.js"; - -export interface TeleoperateConfig { - robot: RobotConfig; - teleop: KeyboardTeleoperationConfig; - fps?: number; // Default: 60 - step_size?: number; // Default: 10 (motor position units) - duration_s?: number | null; // Default: null (infinite) -} - -export interface KeyboardTeleoperationConfig { - type: "keyboard"; // Only keyboard for now, expandable later -} diff --git a/src/lerobot/node/types/teleoperator-config.ts b/src/lerobot/node/types/teleoperator-config.ts deleted file mode 100644 index 68bb7a0c5f4cc97e30f0f7fc489b7370748ff5dd..0000000000000000000000000000000000000000 --- a/src/lerobot/node/types/teleoperator-config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Teleoperator configuration types for Node.js implementation - */ - -export interface TeleoperatorConfig { - type: "so100_leader"; - port: string; - id?: string; - calibration_dir?: string; - // SO-100 leader specific options -} diff --git a/src/lerobot/node/utils/keyboard-teleop.ts b/src/lerobot/node/utils/keyboard-teleop.ts deleted file mode 100644 index 75ef6cb952b8a040ae8726ed4e565645c74f5e34..0000000000000000000000000000000000000000 --- a/src/lerobot/node/utils/keyboard-teleop.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * Keyboard teleoperation controller for Node.js terminal - * Handles raw keyboard input and robot position control using the keypress package. - */ - -import * as readline from "readline"; -import { SO100Follower } from "../robots/so100_follower.js"; - -/** - * Keyboard controller for robot teleoperation - * Handles terminal keyboard input and robot position updates - */ -export class KeyboardController { - private robot: SO100Follower; - private stepSize: number; - private currentPositions: Record = {}; - private motorNames = [ - "shoulder_pan", - "shoulder_lift", - "elbow_flex", - "wrist_flex", - "wrist_roll", - "gripper", - ]; - private running = false; - private gripperState = false; // Toggle state for gripper - - constructor(robot: SO100Follower, stepSize: number = 25) { - this.robot = robot; - this.stepSize = stepSize; - } - - /** - * Start keyboard teleoperation - * Sets up raw keyboard input and initializes robot positions - */ - async start(): Promise { - console.log("Initializing keyboard controller..."); - - // Initialize current positions from robot - try { - this.currentPositions = await this.readRobotPositions(); - } catch (error) { - console.warn( - "Could not read initial robot positions, using calibrated centers" - ); - // Initialize with calibrated center positions if available, otherwise use middle positions - const calibratedLimits = this.robot.getCalibrationLimits(); - this.motorNames.forEach((motor) => { - const limits = calibratedLimits[motor]; - const centerPosition = limits - ? Math.floor((limits.min + limits.max) / 2) - : 2047; - this.currentPositions[motor] = centerPosition; - }); - } - - // Set up raw keyboard input - this.setupKeyboardInput(); - this.running = true; - - console.log("Keyboard controller ready. Use controls to move robot."); - } - - /** - * Stop keyboard teleoperation - * Cleans up keyboard input handling - */ - async stop(): Promise { - this.running = false; - - // Reset terminal to normal mode - if (process.stdin.setRawMode) { - process.stdin.setRawMode(false); - } - process.stdin.removeAllListeners("keypress"); - - console.log("Keyboard controller stopped."); - } - - /** - * Get current robot positions - */ - async getCurrentPositions(): Promise> { - return { ...this.currentPositions }; - } - - /** - * Set up keyboard input handling - * Uses readline for cross-platform keyboard input - */ - private setupKeyboardInput(): void { - // Set up raw mode for immediate key response - if (process.stdin.setRawMode) { - process.stdin.setRawMode(true); - } - process.stdin.resume(); - process.stdin.setEncoding("utf8"); - - // Handle keyboard input - process.stdin.on("data", (key: string) => { - if (!this.running) return; - - this.handleKeyPress(key); - }); - } - - /** - * Handle individual key presses - * Maps keys to robot motor movements - */ - private async handleKeyPress(key: string): Promise { - let positionChanged = false; - const newPositions = { ...this.currentPositions }; - - // Handle arrow keys first (they start with ESC but are multi-byte sequences) - if (key.startsWith("\u001b[")) { - const arrowKey = key.slice(2); - switch (arrowKey) { - case "A": // Up arrow - newPositions.shoulder_lift += this.stepSize; - positionChanged = true; - break; - case "B": // Down arrow - newPositions.shoulder_lift -= this.stepSize; - positionChanged = true; - break; - case "C": // Right arrow - newPositions.shoulder_pan += this.stepSize; - positionChanged = true; - break; - case "D": // Left arrow - newPositions.shoulder_pan -= this.stepSize; - positionChanged = true; - break; - } - } else { - // Handle single character keys - const keyCode = key.charCodeAt(0); - - switch (keyCode) { - // Standalone ESC key (emergency stop) - case 27: - if (key.length === 1) { - console.log("\n🛑 EMERGENCY STOP!"); - await this.emergencyStop(); - return; - } - break; - - // Regular character keys - case 119: // 'w' - newPositions.elbow_flex += this.stepSize; - positionChanged = true; - break; - case 115: // 's' - newPositions.elbow_flex -= this.stepSize; - positionChanged = true; - break; - case 97: // 'a' - newPositions.wrist_flex -= this.stepSize; - positionChanged = true; - break; - case 100: // 'd' - newPositions.wrist_flex += this.stepSize; - positionChanged = true; - break; - case 113: // 'q' - newPositions.wrist_roll -= this.stepSize; - positionChanged = true; - break; - case 101: // 'e' - newPositions.wrist_roll += this.stepSize; - positionChanged = true; - break; - case 32: // Space - // Toggle gripper - this.gripperState = !this.gripperState; - newPositions.gripper = this.gripperState ? 2300 : 1800; - positionChanged = true; - break; - - // Ctrl+C - case 3: - console.log("\nExiting..."); - process.exit(0); - } - } - - if (positionChanged) { - // Apply position limits using calibration - this.enforcePositionLimits(newPositions); - - // Update robot positions - only send changed motors for better performance - try { - await this.writeRobotPositions(newPositions); - this.currentPositions = newPositions; - } catch (error) { - console.warn( - `Failed to update robot positions: ${ - error instanceof Error ? error.message : error - }` - ); - } - } - } - - /** - * Read current positions from robot - * Uses SO100Follower position reading methods - */ - private async readRobotPositions(): Promise> { - try { - return await this.robot.getMotorPositions(); - } catch (error) { - console.warn( - `Failed to read robot positions: ${ - error instanceof Error ? error.message : error - }` - ); - // Return default positions as fallback - const positions: Record = {}; - this.motorNames.forEach((motor, index) => { - positions[motor] = 2047; // STS3215 middle position - }); - return positions; - } - } - - /** - * Write positions to robot - optimized to only send changed motors - * This was the key to the smooth performance in the working version - */ - private async writeRobotPositions( - newPositions: Record - ): Promise { - // Only send commands for motors that actually changed - const changedPositions: Record = {}; - let hasChanges = false; - - for (const [motor, newPosition] of Object.entries(newPositions)) { - if (Math.abs(this.currentPositions[motor] - newPosition) > 0.5) { - changedPositions[motor] = newPosition; - hasChanges = true; - } - } - - if (hasChanges) { - await this.robot.setMotorPositions(changedPositions); - } - } - - /** - * Enforce position limits based on calibration data - * Uses actual calibrated limits instead of hardcoded defaults - */ - private enforcePositionLimits(positions: Record): void { - // Get calibrated limits from robot - const calibratedLimits = this.robot.getCalibrationLimits(); - - for (const [motor, position] of Object.entries(positions)) { - const limits = calibratedLimits[motor]; - if (limits) { - positions[motor] = Math.max(limits.min, Math.min(limits.max, position)); - } - } - } - - /** - * Emergency stop - halt all robot movement - */ - private async emergencyStop(): Promise { - try { - // Stop all robot movement - // TODO: Implement emergency stop in SO100Follower - console.log("Emergency stop executed."); - await this.stop(); - process.exit(0); - } catch (error) { - console.error("Emergency stop failed:", error); - process.exit(1); - } - } -} diff --git a/tsconfig.cli.json b/tsconfig.cli.json deleted file mode 100644 index a2c6e2a2479bb154bbbdbe236e99b4a5ecc48f81..0000000000000000000000000000000000000000 --- a/tsconfig.cli.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src/lerobot/**/*", "src/cli/**/*"] -}