blanchon commited on
Commit
d4d25f7
·
1 Parent(s): f62f94b
SPEC.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Robot USB Connection System - Summary
2
+
3
+ ## Core Components
4
+
5
+ ### USBProducer (Output)
6
+ **Purpose**: Sends software commands to control physical robot hardware
7
+ - Receives normalized joint commands from software
8
+ - Converts normalized values to raw servo positions
9
+ - Sends position commands to physical servos
10
+ - Locks servos for precise software control
11
+
12
+ ### USBConsumer (Input)
13
+ **Purpose**: Reads physical robot movements and translates to software commands
14
+ - Continuously monitors physical servo positions
15
+ - Converts raw servo values to normalized percentages
16
+ - Detects movement changes and broadcasts updates
17
+ - Keeps servos unlocked for manual manipulation
18
+
19
+ ## Key Features
20
+
21
+ ### Calibration System (`USBCalibrationPanel.svelte`)
22
+ - **Requirement**: All USB connections must be calibrated before use
23
+ - **Process**: Records min/max physical range for each servo by manual movement
24
+ - **Result**: Establishes mapping between raw servo values (0-4095) and normalized values
25
+
26
+ ### Normalized Communication Protocol
27
+ - **Standard Joints**: -100% to +100% range (bipolar)
28
+ - **Gripper/Jaw**: 0% to +100% range (unipolar)
29
+ - **Benefits**: Consistent software interface across different robots
30
+ - **Conversion**: Automatic normalization/denormalization based on calibration data
31
+
32
+ ### Multi-Port Support
33
+ - **Capability**: Multiple independent USB connections simultaneously
34
+ - **Independence**: Each connection has its own calibration and configuration
35
+ - **Use Case**: Control multiple robots or multiple connections to same robot
36
+
37
+ ### Batch Operations
38
+ - **Sync Read**: Read multiple servo positions simultaneously
39
+ - **Sync Write**: Send commands to multiple servos in batched operations
40
+ - **Performance**: Reduces USB communication overhead and latency
41
+
42
+ ## Key Constraints
43
+
44
+ ### Connection Exclusivity
45
+ - **Input**: Only one consumer (input source) active per robot at a time
46
+ - **Output**: Multiple producers (output destinations) can be active simultaneously
47
+ - **Rationale**: Prevents conflicting input commands while allowing broadcast to multiple destinations
48
+
49
+ ### Servo Locking Strategy
50
+ - **Consumer Mode**: Servos unlocked → manual movement possible + position reading
51
+ - **Producer Mode**: Servos locked → software control only, no manual movement
52
+ - **Safety**: Prevents mechanical conflicts between manual and software control
53
+
54
+ ### Calibration Dependency
55
+ - **Mandatory**: USB connections cannot establish without valid calibration
56
+ - **Per-Connection**: Each USB port requires independent calibration
57
+ - **Safety**: Prevents commanding impossible positions or damaging hardware
58
+
59
+ ## Additional System Requirements (from SPEC.md analysis)
60
+
61
+ ### Connection Management
62
+ - **Auto-Detection**: System should detect available USB ports automatically
63
+ - **Status Monitoring**: Real-time connection health and error reporting
64
+ - **Graceful Disconnection**: Safe servo unlocking on disconnect/error
65
+
66
+ ### Error Handling & Recovery
67
+ - **Port Conflicts**: Queue management to prevent "Port is busy" errors
68
+ - **Retry Logic**: Automatic retry with backoff for failed servo commands
69
+ - **Connection Recovery**: Automatic reconnection attempts after USB disconnect
70
+
71
+ ### User Interface Integration
72
+ - **Modal Management**: Unified connection setup through calibration panels
73
+ - **Status Display**: Visual indicators for connection state and calibration status
74
+ - **Manual Control**: Direct joint manipulation when no input consumer active
75
+
76
+ ### Performance Optimizations
77
+ - **Polling Rates**: Configurable update frequencies for different use cases
78
+ - **Change Detection**: Only broadcast updates when values actually change
79
+ - **Queueing**: Serial command processing to prevent USB port conflicts
80
+
81
+ ## System Architecture Concepts
82
+
83
+ ### Bidirectional Data Flow
84
+ ```
85
+ Physical Robot ←→ USB Hardware ←→ Calibration Layer ←→ Normalized Interface ←→ Software
86
+ ```
87
+
88
+ ### Connection Patterns
89
+ - **Teaching Mode**: USB Consumer only (read robot movements)
90
+ - **Control Mode**: USB Producer only (software controls robot)
91
+ - **Bidirectional**: Both consumer and producer (full interaction)
92
+ - **Broadcasting**: Multiple producers (send to hardware + remote systems)
93
+
94
+ ### Value Transformation Pipeline
95
+ ```
96
+ Raw Servo (0-4095) ←→ Calibrated Range ←→ Normalized (-100/+100) ←→ Software Commands
97
+ ```
98
+
99
+ This system provides a robust, safe, and flexible interface for robot hardware control while maintaining consistency across different robot configurations and use cases.
packages/feetech.js/.npmignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.pdf
2
+ *.xls
3
+ *.html
src/lib/elements/robot/drivers/USBServoDriver.ts ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ConnectionStatus, USBDriverConfig } from '../models.js';
2
+ import { CalibrationState } from '../calibration/CalibrationState.svelte.js';
3
+ import { ScsServoSDK } from 'feetech.js';
4
+ import { ROBOT_CONFIG } from '../config.js';
5
+
6
+ export abstract class USBServoDriver {
7
+ readonly id: string;
8
+ readonly name: string;
9
+ readonly config: USBDriverConfig;
10
+
11
+ protected _status: ConnectionStatus = { isConnected: false };
12
+ protected statusCallbacks: ((status: ConnectionStatus) => void)[] = [];
13
+
14
+ protected scsServoSDK: ScsServoSDK | null = null;
15
+
16
+ // Calibration state - directly embedded
17
+ readonly calibrationState: CalibrationState;
18
+
19
+ // Calibration polling
20
+ private calibrationPollingInterval: ReturnType<typeof setInterval> | null = null;
21
+
22
+ // Joint to servo ID mapping for SO-100 arm
23
+ protected readonly jointToServoMap = {
24
+ "Rotation": 1,
25
+ "Pitch": 2,
26
+ "Elbow": 3,
27
+ "Wrist_Pitch": 4,
28
+ "Wrist_Roll": 5,
29
+ "Jaw": 6
30
+ };
31
+
32
+ constructor(config: USBDriverConfig, driverType: string) {
33
+ this.config = config;
34
+ this.id = `usb-${driverType}-${Date.now()}`;
35
+ this.name = `USB ${driverType}`;
36
+ this.calibrationState = new CalibrationState();
37
+ }
38
+
39
+ get status(): ConnectionStatus {
40
+ return this._status;
41
+ }
42
+
43
+ get needsCalibration(): boolean {
44
+ return this.calibrationState.needsCalibration;
45
+ }
46
+
47
+ get isCalibrating(): boolean {
48
+ return this.calibrationState.isCalibrating;
49
+ }
50
+
51
+ get isCalibrated(): boolean {
52
+ return this.calibrationState.isCalibrated;
53
+ }
54
+
55
+ // Type guard to check if a driver is a USB driver
56
+ static isUSBDriver(driver: any): driver is USBServoDriver {
57
+ return driver && typeof driver.calibrationState === 'object' &&
58
+ typeof driver.needsCalibration === 'boolean' &&
59
+ typeof driver.isCalibrated === 'boolean' &&
60
+ typeof driver.startCalibration === 'function';
61
+ }
62
+
63
+ // Type-safe method to get calibration interface
64
+ getCalibrationInterface(): {
65
+ needsCalibration: boolean;
66
+ isCalibrating: boolean;
67
+ isCalibrated: boolean;
68
+ startCalibration: () => Promise<void>;
69
+ completeCalibration: () => Promise<Record<string, number>>;
70
+ skipCalibration: () => void;
71
+ cancelCalibration: () => void;
72
+ onCalibrationCompleteWithPositions: (callback: (positions: Record<string, number>) => void) => () => void;
73
+ } {
74
+ return {
75
+ needsCalibration: this.needsCalibration,
76
+ isCalibrating: this.isCalibrating,
77
+ isCalibrated: this.isCalibrated,
78
+ startCalibration: () => this.startCalibration(),
79
+ completeCalibration: () => this.completeCalibration(),
80
+ skipCalibration: () => this.skipCalibration(),
81
+ cancelCalibration: () => this.cancelCalibration(),
82
+ onCalibrationCompleteWithPositions: (callback) => this.onCalibrationCompleteWithPositions(callback)
83
+ };
84
+ }
85
+
86
+ // Abstract methods that subclasses must implement
87
+ abstract connect(): Promise<void>;
88
+ abstract disconnect(): Promise<void>;
89
+
90
+ // Common connection logic
91
+ protected async connectToUSB(): Promise<void> {
92
+ if (this._status.isConnected) {
93
+ console.log(`[${this.name}] Already connected`);
94
+ return;
95
+ }
96
+
97
+ try {
98
+ console.log(`[${this.name}] Connecting...`);
99
+
100
+ // Create a new SDK instance for this driver instead of using the singleton
101
+ // This allows multiple drivers to connect to different ports simultaneously
102
+ this.scsServoSDK = new ScsServoSDK();
103
+
104
+ await this.scsServoSDK.connect({
105
+ baudRate: this.config.baudRate || ROBOT_CONFIG.usb.baudRate,
106
+ protocolEnd: 0 // STS/SMS protocol
107
+ });
108
+
109
+ this._status = { isConnected: true, lastConnected: new Date() };
110
+ this.notifyStatusChange();
111
+
112
+ console.log(`[${this.name}] Connected successfully`);
113
+
114
+ // Debug: Log SDK instance methods to identify the issue
115
+ console.log(`[${this.name}] SDK instance methods:`, Object.getOwnPropertyNames(this.scsServoSDK));
116
+ console.log(`[${this.name}] SDK prototype methods:`, Object.getOwnPropertyNames(Object.getPrototypeOf(this.scsServoSDK)));
117
+ console.log(`[${this.name}] writeTorqueEnable available:`, typeof this.scsServoSDK.writeTorqueEnable);
118
+ console.log(`[${this.name}] syncReadPositions available:`, typeof this.scsServoSDK.syncReadPositions);
119
+
120
+ } catch (error) {
121
+ console.error(`[${this.name}] Connection failed:`, error);
122
+ this._status = { isConnected: false, error: `Connection failed: ${error}` };
123
+ this.notifyStatusChange();
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ protected async disconnectFromUSB(): Promise<void> {
129
+ if (this.scsServoSDK) {
130
+ try {
131
+ await this.unlockAllServos();
132
+ await this.scsServoSDK.disconnect();
133
+ } catch (error) {
134
+ console.warn(`[${this.name}] Error during disconnect:`, error);
135
+ }
136
+ this.scsServoSDK = null;
137
+ }
138
+
139
+ this._status = { isConnected: false };
140
+ this.notifyStatusChange();
141
+
142
+ console.log(`[${this.name}] Disconnected`);
143
+ }
144
+
145
+ // Calibration methods
146
+ async startCalibration(): Promise<void> {
147
+ if (!this._status.isConnected) {
148
+ await this.connectToUSB();
149
+ }
150
+
151
+ if (!this._status.isConnected) {
152
+ throw new Error('Cannot start calibration: not connected');
153
+ }
154
+
155
+ console.log(`[${this.name}] Starting calibration...`);
156
+ this.calibrationState.startCalibration();
157
+
158
+ // Unlock servos for manual movement during calibration
159
+ await this.unlockAllServos();
160
+
161
+ // Start polling positions during calibration
162
+ await this.startCalibrationPolling();
163
+ }
164
+
165
+ async completeCalibration(): Promise<Record<string, number>> {
166
+ if (!this.isCalibrating) {
167
+ throw new Error('Not currently calibrating');
168
+ }
169
+
170
+ // Stop polling
171
+ this.stopCalibrationPolling();
172
+
173
+ // Read final positions
174
+ const finalPositions = await this.readCurrentPositions();
175
+
176
+ // Complete calibration state
177
+ this.calibrationState.completeCalibration();
178
+
179
+ console.log(`[${this.name}] Calibration completed`);
180
+ return finalPositions;
181
+ }
182
+
183
+ skipCalibration(): void {
184
+ // Stop polling if active
185
+ this.stopCalibrationPolling();
186
+ this.calibrationState.skipCalibration();
187
+ }
188
+
189
+ async setPredefinedCalibration(): Promise<void> {
190
+ // Stop polling if active
191
+ this.stopCalibrationPolling();
192
+ this.skipCalibration();
193
+ }
194
+
195
+ // Cancel calibration
196
+ cancelCalibration(): void {
197
+ // Stop polling if active
198
+ this.stopCalibrationPolling();
199
+ this.calibrationState.cancelCalibration();
200
+ }
201
+
202
+ // Start polling servo positions during calibration
203
+ private async startCalibrationPolling(): Promise<void> {
204
+ if (this.calibrationPollingInterval !== null) {
205
+ return; // Already polling
206
+ }
207
+
208
+ console.log(`[${this.name}] Starting calibration position polling...`);
209
+
210
+ // Poll positions every 100ms during calibration
211
+ this.calibrationPollingInterval = setInterval(async () => {
212
+ if (!this.isCalibrating || !this._status.isConnected || !this.scsServoSDK) {
213
+ this.stopCalibrationPolling();
214
+ return;
215
+ }
216
+
217
+ try {
218
+ // Read positions for all servos
219
+ const servoIds = Object.values(this.jointToServoMap);
220
+ const positions = await this.scsServoSDK.syncReadPositions(servoIds);
221
+
222
+ // Update calibration state with current positions
223
+ Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => {
224
+ const position = positions.get(servoId);
225
+ if (position !== undefined) {
226
+ this.calibrationState.updateCurrentValue(jointName, position);
227
+ console.debug(`[${this.name}] ${jointName} (servo ${servoId}): ${position}`);
228
+ }
229
+ });
230
+
231
+ } catch (error) {
232
+ console.warn(`[${this.name}] Calibration polling error:`, error);
233
+ // Continue polling despite errors - user might be moving servos rapidly
234
+ }
235
+ }, 100); // Poll every 100ms
236
+ }
237
+
238
+ // Stop polling servo positions
239
+ private stopCalibrationPolling(): void {
240
+ if (this.calibrationPollingInterval !== null) {
241
+ clearInterval(this.calibrationPollingInterval);
242
+ this.calibrationPollingInterval = null;
243
+ console.log(`[${this.name}] Stopped calibration position polling`);
244
+ }
245
+ }
246
+
247
+ // Servo position reading (for calibration)
248
+ async readCurrentPositions(): Promise<Record<string, number>> {
249
+ if (!this.scsServoSDK || !this._status.isConnected) {
250
+ throw new Error('Cannot read positions: not connected');
251
+ }
252
+
253
+ const positions: Record<string, number> = {};
254
+
255
+ try {
256
+ const servoIds = Object.values(this.jointToServoMap);
257
+ const servoPositions = await this.scsServoSDK.syncReadPositions(servoIds);
258
+
259
+ Object.entries(this.jointToServoMap).forEach(([jointName, servoId]) => {
260
+ const position = servoPositions.get(servoId);
261
+ if (position !== undefined) {
262
+ positions[jointName] = position;
263
+ // Update calibration state with current position
264
+ this.calibrationState.updateCurrentValue(jointName, position);
265
+ }
266
+ });
267
+
268
+ } catch (error) {
269
+ console.error(`[${this.name}] Error reading positions:`, error);
270
+ throw error;
271
+ }
272
+
273
+ return positions;
274
+ }
275
+
276
+ // Value conversion methods
277
+ normalizeValue(rawValue: number, jointName: string): number {
278
+ if (!this.isCalibrated) {
279
+ throw new Error('Cannot normalize value: not calibrated');
280
+ }
281
+ return this.calibrationState.normalizeValue(rawValue, jointName);
282
+ }
283
+
284
+ denormalizeValue(normalizedValue: number, jointName: string): number {
285
+ if (!this.isCalibrated) {
286
+ throw new Error('Cannot denormalize value: not calibrated');
287
+ }
288
+ return this.calibrationState.denormalizeValue(normalizedValue, jointName);
289
+ }
290
+
291
+ // Servo control methods
292
+ protected async lockAllServos(): Promise<void> {
293
+ if (!this.scsServoSDK) return;
294
+
295
+ try {
296
+ console.log(`[${this.name}] Locking all servos...`);
297
+
298
+ const servoIds = Object.values(this.jointToServoMap);
299
+
300
+ for (const servoId of servoIds) {
301
+ try {
302
+ // Check if writeTorqueEnable method exists before calling
303
+ if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') {
304
+ console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`);
305
+ continue;
306
+ }
307
+
308
+ await this.scsServoSDK.writeTorqueEnable(servoId, true);
309
+ await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay));
310
+ } catch (error) {
311
+ console.warn(`[${this.name}] Failed to lock servo ${servoId}:`, error);
312
+ }
313
+ }
314
+
315
+ console.log(`[${this.name}] All servos locked`);
316
+
317
+ } catch (error) {
318
+ console.error(`[${this.name}] Error locking servos:`, error);
319
+ }
320
+ }
321
+
322
+ protected async unlockAllServos(): Promise<void> {
323
+ if (!this.scsServoSDK) return;
324
+
325
+ try {
326
+ console.log(`[${this.name}] Unlocking all servos...`);
327
+
328
+ const servoIds = Object.values(this.jointToServoMap);
329
+
330
+ for (const servoId of servoIds) {
331
+ try {
332
+ // Check if writeTorqueEnable method exists before calling
333
+ if (typeof this.scsServoSDK.writeTorqueEnable !== 'function') {
334
+ console.warn(`[${this.name}] writeTorqueEnable method not available on SDK instance for servo ${servoId}`);
335
+ continue;
336
+ }
337
+
338
+ await this.scsServoSDK.writeTorqueEnable(servoId, false);
339
+ await new Promise(resolve => setTimeout(resolve, ROBOT_CONFIG.usb.servoWriteDelay));
340
+ } catch (error) {
341
+ console.warn(`[${this.name}] Failed to unlock servo ${servoId}:`, error);
342
+ }
343
+ }
344
+
345
+ console.log(`[${this.name}] All servos unlocked`);
346
+
347
+ } catch (error) {
348
+ console.error(`[${this.name}] Error unlocking servos:`, error);
349
+ }
350
+ }
351
+
352
+ // Event handlers
353
+ onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
354
+ this.statusCallbacks.push(callback);
355
+ return () => {
356
+ const index = this.statusCallbacks.indexOf(callback);
357
+ if (index >= 0) {
358
+ this.statusCallbacks.splice(index, 1);
359
+ }
360
+ };
361
+ }
362
+
363
+ protected notifyStatusChange(): void {
364
+ this.statusCallbacks.forEach(callback => {
365
+ try {
366
+ callback(this._status);
367
+ } catch (error) {
368
+ console.error(`[${this.name}] Error in status callback:`, error);
369
+ }
370
+ });
371
+ }
372
+
373
+ // Register callback for calibration completion with positions
374
+ onCalibrationCompleteWithPositions(callback: (positions: Record<string, number>) => void): () => void {
375
+ return this.calibrationState.onCalibrationCompleteWithPositions(callback);
376
+ }
377
+
378
+ // Sync robot joint positions using normalized values from calibration
379
+ syncRobotPositions(finalPositions: Record<string, number>, updateRobotCallback?: (jointName: string, normalizedValue: number) => void): void {
380
+ if (!updateRobotCallback) return;
381
+
382
+ console.log(`[${this.name}] 🔄 Syncing robot to final calibration positions...`);
383
+
384
+ Object.entries(finalPositions).forEach(([jointName, rawPosition]) => {
385
+ try {
386
+ // Convert raw servo position to normalized value using calibration
387
+ const normalizedValue = this.normalizeValue(rawPosition, jointName);
388
+
389
+ // Clamp to appropriate normalized range based on joint type
390
+ let clampedValue: number;
391
+ if (jointName.toLowerCase() === 'jaw' || jointName.toLowerCase() === 'gripper') {
392
+ clampedValue = Math.max(0, Math.min(100, normalizedValue));
393
+ } else {
394
+ clampedValue = Math.max(-100, Math.min(100, normalizedValue));
395
+ }
396
+
397
+ console.log(`[${this.name}] ${jointName}: ${rawPosition} (raw) -> ${normalizedValue.toFixed(1)} -> ${clampedValue.toFixed(1)} (normalized)`);
398
+
399
+ // Update robot joint through callback
400
+ updateRobotCallback(jointName, clampedValue);
401
+ } catch (error) {
402
+ console.warn(`[${this.name}] Failed to sync position for joint ${jointName}:`, error);
403
+ }
404
+ });
405
+
406
+ console.log(`[${this.name}] ✅ Robot synced to calibration positions`);
407
+ }
408
+
409
+ // Cleanup
410
+ async destroy(): Promise<void> {
411
+ this.stopCalibrationPolling();
412
+ await this.disconnect();
413
+ this.calibrationState.destroy();
414
+ }
415
+ }